// fg_serial.cxx -- higher level serial port management routines
//
// Written by Curtis Olson, started November 1998.
//
// Copyright (C) 1998  Curtis L. Olson - curt@flightgear.org
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
// $Id: fg_serial.cxx,v 1.6 1999/05/08 02:33:19 curt Exp $


#include <Include/compiler.h>

#ifdef FG_HAVE_STD_INCLUDES
#  include <cmath>
#  include <cstdlib>    // atoi()
#else
#  include <math.h>
#  include <stdlib.h>   // atoi()
#endif

#include STL_STRING
#include STL_IOSTREAM                                           
#include <vector>                                                               

#include <Debug/logstream.hxx>
#include <Aircraft/aircraft.hxx>
#include <Include/fg_constants.h>
#include <Serial/serial.hxx>
#include <Time/fg_time.hxx>

#include "options.hxx"

#include "fg_serial.hxx"

FG_USING_STD(string);
FG_USING_STD(vector);

// support an arbitrary number of serial channels.  Each channel can
// be assigned to an arbitrary port.  Bi-directional communication is
// supported by the underlying layer, but probably will never be
// needed by FGFS?

typedef vector < fgIOCHANNEL > io_container;
typedef io_container::iterator io_iterator;
typedef io_container::const_iterator const_io_iterator;

// define the four channels
io_container port_list;


fgIOCHANNEL::fgIOCHANNEL() :
    kind( FG_SERIAL_DISABLED ),
    valid_config( false )
{
}


fgIOCHANNEL::~fgIOCHANNEL() {
}


// configure a port based on the config string
static fgIOCHANNEL parse_port_config( const string& config )
{
    fgIOCHANNEL p;

    string::size_type begin, end;

    begin = 0;

    FG_LOG( FG_SERIAL, FG_INFO, "Parse serial port config: " << config );

    // device name
    end = config.find(",", begin);
    if ( end == string::npos ) {
	return p;
    }
    
    p.device = config.substr(begin, end - begin);
    begin = end + 1;
    FG_LOG( FG_SERIAL, FG_INFO, "  device = " << p.device );

    // format
    end = config.find(",", begin);
    if ( end == string::npos ) {
	return p;
    }
    
    p.format = config.substr(begin, end - begin);
    begin = end + 1;
    FG_LOG( FG_SERIAL, FG_INFO, "  format = " << p.format );

    // baud
    end = config.find(",", begin);
    if ( end == string::npos ) {
	return p;
    }
    
    p.baud = config.substr(begin, end - begin);
    begin = end + 1;
    FG_LOG( FG_SERIAL, FG_INFO, "  baud = " << p.baud );

    // direction
    p.direction = config.substr(begin);
    FG_LOG( FG_SERIAL, FG_INFO, "  direction = " << p.direction );

    p.valid_config = true;

    return p;
}


// configure a port based on the config info
static bool config_port( fgIOCHANNEL &p )
{
    if ( p.port.is_enabled() ) {
	FG_LOG( FG_SERIAL, FG_ALERT, "This shouldn't happen, but the port " 
		<< "is already in use, ignoring" );
	return false;
    }

    if ( ! p.port.open_port( p.device ) ) {
	FG_LOG( FG_SERIAL, FG_ALERT, "Error opening device: " << p.device );
	return false;
    }

    // cout << "fd = " << p.port.fd << endl;

    if ( ! p.port.set_baud( atoi( p.baud.c_str() ) ) ) {
	FG_LOG( FG_SERIAL, FG_ALERT, "Error setting baud: " << p.baud );
	return false;
    }

    if ( p.format == "nmea" ) {
	if ( p.direction == "out" ) {
	    p.kind = fgIOCHANNEL::FG_SERIAL_NMEA_OUT;
	} else if ( p.direction == "in" ) {
	    p.kind = fgIOCHANNEL::FG_SERIAL_NMEA_IN;
	} else {
	    FG_LOG( FG_SERIAL, FG_ALERT, "Unknown direction" );
	    return false;
	}
    } else if ( p.format == "garmin" ) {
	if ( p.direction == "out" ) {
	    p.kind = fgIOCHANNEL::FG_SERIAL_GARMIN_OUT;
	} else if ( p.direction == "in" ) {
	    p.kind = fgIOCHANNEL::FG_SERIAL_GARMIN_IN;
	} else {
	    FG_LOG( FG_SERIAL, FG_ALERT, "Unknown direction" );
	    return false;
	}
    } else if ( p.format == "fgfs" ) {
	if ( p.direction == "out" ) {
	    p.kind = fgIOCHANNEL::FG_SERIAL_FGFS_OUT;
	} else if ( p.direction == "in" ) {
	    p.kind = fgIOCHANNEL::FG_SERIAL_FGFS_IN;
	} else {
	    FG_LOG( FG_SERIAL, FG_ALERT, "Unknown direction" );
	    return false;
	}
    } else if ( p.format == "rul" ) {
	if ( p.direction == "out" ) {
	    p.kind = fgIOCHANNEL::FG_SERIAL_RUL_OUT;
	} else if ( p.direction == "in" ) {
	    FG_LOG( FG_SERIAL, FG_ALERT, "RUL format is outgoing only" );
	    return false;
	} else {
	    FG_LOG( FG_SERIAL, FG_ALERT, "Unknown direction" );
	    return false;
	}
    } else {
	FG_LOG( FG_SERIAL, FG_ALERT, "Unknown format" );
	return false;
    }

    return true;
}


// step through the port config streams (from fgOPTIONS) and setup
// serial port channels for each
void fgSerialInit() {
    fgIOCHANNEL port;
    bool result;
    str_container port_options_list = current_options.get_port_options_list();

    // we could almost do this in a single step except pushing a valid
    // port onto the port list copies the structure and destroys the
    // original, which closes the port and frees up the fd ... doh!!!

    // parse the configuration strings and store the results in stub
    // fgIOCHANNEL structures
    const_str_iterator current_str = port_options_list.begin();
    const_str_iterator last_str = port_options_list.end();
    for ( ; current_str != last_str; ++current_str ) {
	port = parse_port_config( *current_str );
	if ( port.valid_config ) {
	    result = config_port( port );
	    if ( result ) {
		port_list.push_back( port );
	    }
	}
    }
}


char calc_nmea_cksum(char *sentence) {
    unsigned char sum = 0;
    int i, len;

    // printf("%s\n", sentence);

    len = strlen(sentence);
    sum = sentence[0];
    for ( i = 1; i < len; i++ ) {
        // printf("%c", sentence[i]);
        sum ^= sentence[i];
    }
    // printf("\n");

    // printf("sum = %02x\n", sum);
    return sum;
}


static void send_nmea_out( fgIOCHANNEL *p ) {
    char rmc[256], gga[256];
    char rmc_sum[10], gga_sum[10];
    char dir;
    int deg;
    double min;
    FGInterface *f;
    FGTime *t;

    f = current_aircraft.fdm_state;
    t = FGTime::cur_time_params;

    // run once every two seconds
    if ( p->last_time == t->get_cur_time() ) {
	return;
    }
    p->last_time = t->get_cur_time();
    if ( t->get_cur_time() % 2 != 0 ) {
	return;
    }

    char utc[10];
    sprintf( utc, "%02d%02d%02d", 
	     t->getGmt()->tm_hour, t->getGmt()->tm_min, t->getGmt()->tm_sec );

    char lat[20];
    double latd = f->get_Latitude() * RAD_TO_DEG;
    if ( latd < 0.0 ) {
	latd *= -1.0;
	dir = 'S';
    } else {
	dir = 'N';
    }
    deg = (int)(latd);
    min = (latd - (double)deg) * 60.0;
    sprintf( lat, "%02d%06.3f,%c", abs(deg), min, dir);

    char lon[20];
    double lond = f->get_Longitude() * RAD_TO_DEG;
    if ( lond < 0.0 ) {
	lond *= -1.0;
	dir = 'W';
    } else {
	dir = 'E';
    }
    deg = (int)(lond);
    min = (lond - (double)deg) * 60.0;
    sprintf( lon, "%03d%06.3f,%c", abs(deg), min, dir);

    char speed[10];
    sprintf( speed, "%05.1f", f->get_V_equiv_kts() );

    char heading[10];
    sprintf( heading, "%05.1f", f->get_Psi() * RAD_TO_DEG );

    char altitude_m[10];
    sprintf( altitude_m, "%02d", (int)(f->get_Altitude() * FEET_TO_METER) );

    char altitude_ft[10];
    sprintf( altitude_ft, "%02d", (int)f->get_Altitude() );

    char date[10];
    sprintf( date, "%02d%02d%02d", t->getGmt()->tm_mday, 
	     t->getGmt()->tm_mon+1, t->getGmt()->tm_year );

    // $GPRMC,HHMMSS,A,DDMM.MMM,N,DDDMM.MMM,W,XXX.X,XXX.X,DDMMYY,XXX.X,E*XX
    sprintf( rmc, "GPRMC,%s,A,%s,%s,%s,%s,%s,0.000,E",
	     utc, lat, lon, speed, heading, date );
    sprintf( rmc_sum, "%02X", calc_nmea_cksum(rmc) );

    sprintf( gga, "GPGGA,%s,%s,%s,1,,,%s,F,,,,",
	     utc, lat, lon, altitude_ft );
    sprintf( gga_sum, "%02X", calc_nmea_cksum(gga) );


    FG_LOG( FG_SERIAL, FG_DEBUG, rmc );
    FG_LOG( FG_SERIAL, FG_DEBUG, gga );

    // RMC sentence
    string rmc_sentence = "$";
    rmc_sentence += rmc;
    rmc_sentence += "*";
    rmc_sentence += rmc_sum;
    rmc_sentence += "\n";
    p->port.write_port(rmc_sentence);
    // cout << rmc_sentence;

    // GGA sentence
    string gga_sentence = "$";
    gga_sentence += gga;
    gga_sentence += "*";
    gga_sentence += gga_sum;
    gga_sentence += "\n";
    p->port.write_port(gga_sentence);
    // cout << gga_sentence;
}

static void read_nmea_in( fgIOCHANNEL *p ) {
}

static void send_garmin_out( fgIOCHANNEL *p ) {
    char rmc[256], rmc_sum[256], rmz[256], rmz_sum[256];
    char dir;
    int deg;
    double min;
    FGInterface *f;
    FGTime *t;

    f = current_aircraft.fdm_state;
    t = FGTime::cur_time_params;

    // run once per second
    if ( p->last_time == t->get_cur_time() ) {
	return;
    }
    p->last_time = t->get_cur_time();
    if ( t->get_cur_time() % 2 != 0 ) {
	return;
    }
    
    char utc[10];
    sprintf( utc, "%02d%02d%02d", 
	     t->getGmt()->tm_hour, t->getGmt()->tm_min, t->getGmt()->tm_sec );

    char lat[20];
    double latd = f->get_Latitude() * RAD_TO_DEG;
    if ( latd < 0.0 ) {
	latd *= -1.0;
	dir = 'S';
    } else {
	dir = 'N';
    }
    deg = (int)(latd);
    min = (latd - (double)deg) * 60.0;
    sprintf( lat, "%02d%06.3f,%c", abs(deg), min, dir);

    char lon[20];
    double lond = f->get_Longitude() * RAD_TO_DEG;
    if ( lond < 0.0 ) {
	lond *= -1.0;
	dir = 'W';
    } else {
	dir = 'E';
    }
    deg = (int)(lond);
    min = (lond - (double)deg) * 60.0;
    sprintf( lon, "%03d%06.3f,%c", abs(deg), min, dir);

    char speed[10];
    sprintf( speed, "%05.1f", f->get_V_equiv_kts() );

    char heading[10];
    sprintf( heading, "%05.1f", f->get_Psi() * RAD_TO_DEG );

    char altitude_m[10];
    sprintf( altitude_m, "%02d", (int)(f->get_Altitude() * FEET_TO_METER) );

    char altitude_ft[10];
    sprintf( altitude_ft, "%02d", (int)f->get_Altitude() );

    char date[10];
    sprintf( date, "%02d%02d%02d", t->getGmt()->tm_mday, 
	     t->getGmt()->tm_mon+1, t->getGmt()->tm_year );

    // $GPRMC,HHMMSS,A,DDMM.MMM,N,DDDMM.MMM,W,XXX.X,XXX.X,DDMMYY,XXX.X,E*XX
    sprintf( rmc, "GPRMC,%s,A,%s,%s,%s,%s,%s,000.0,E",
	     utc, lat, lon, speed, heading, date );
    sprintf( rmc_sum, "%02X", calc_nmea_cksum(rmc) );

    // sprintf( gga, "$GPGGA,%s,%s,%s,1,04,0.0,%s,M,00.0,M,,*00\r\n",
    //          utc, lat, lon, altitude_m );

    sprintf( rmz, "PGRMZ,%s,f,3", altitude_ft );
    sprintf( rmz_sum, "%02X", calc_nmea_cksum(rmz) );

    FG_LOG( FG_SERIAL, FG_DEBUG, rmc );
    FG_LOG( FG_SERIAL, FG_DEBUG, rmz );

    // RMC sentence
    string rmc_sentence = "$";
    rmc_sentence += rmc;
    rmc_sentence += "*";
    rmc_sentence += rmc_sum;
    rmc_sentence += "\n";
    p->port.write_port(rmc_sentence);
    // cout << rmc_sentence;

    // RMZ sentence (garmin proprietary)
    string rmz_sentence = "$";
    rmz_sentence += rmz;
    rmz_sentence += "*";
    rmz_sentence += rmz_sum;
    rmz_sentence += "\n";
    p->port.write_port(rmz_sentence);
    // cout << rmz_sentence;
}

static void read_garmin_in( fgIOCHANNEL *p ) {
}

static void send_fgfs_out( fgIOCHANNEL *p ) {
}

static void read_fgfs_in( fgIOCHANNEL *p ) {
}


// "RUL" output format (for some sort of motion platform)
//
// The Baud rate is 2400 , one start bit, eight data bits, 1 stop bit,
// no parity.
//
// For position it requires a 3-byte data packet defined as follows:
//
// First bite: ascII character"p" ( 80 decimal )
// Second byte X pos. (1-255) 1 being 0* and 255 being 359*
// Third byte Y pos.( 1-255) 1 being 0* and 255 359*
//
// So sending 80 127 127 to the two axis motors will position on 180*
// The RS- 232 port is a nine pin connector and the only pins used are
// 3&5.

static void send_rul_out( fgIOCHANNEL *p ) {
    char rul[256];

    FGInterface *f;
    FGTime *t;

    f = current_aircraft.fdm_state;
    t = FGTime::cur_time_params;

    // run as often as possibleonce per second

    // this runs once per second
    // if ( p->last_time == t->get_cur_time() ) {
    //    return;
    // }
    // p->last_time = t->get_cur_time();
    // if ( t->get_cur_time() % 2 != 0 ) {
    //    return;
    // }
    
    // get roll and pitch, convert to degrees
    double roll_deg = f->get_Phi() * RAD_TO_DEG;
    while ( roll_deg < -180.0 ) {
	roll_deg += 360.0;
    }
    while ( roll_deg > 180.0 ) {
	roll_deg -= 360.0;
    }

    double pitch_deg = f->get_Theta() * RAD_TO_DEG;
    while ( pitch_deg < -180.0 ) {
	pitch_deg += 360.0;
    }
    while ( pitch_deg > 180.0 ) {
	pitch_deg -= 360.0;
    }

    // scale roll and pitch to output format (1 - 255)
    // straight && level == (128, 128)

    int roll = (int)( (roll_deg+180.0) * 255.0 / 360.0) + 1;
    int pitch = (int)( (pitch_deg+180.0) * 255.0 / 360.0) + 1;

    sprintf( rul, "p%c%c\n", roll, pitch);

    FG_LOG( FG_SERIAL, FG_INFO, "p " << roll << " " << pitch );

    string rul_sentence = rul;
    p->port.write_port(rul_sentence);
}


// one more level of indirection ...
static void process_port( fgIOCHANNEL *p ) {
    if ( p->kind == fgIOCHANNEL::FG_SERIAL_NMEA_OUT ) {
	send_nmea_out(p);
    } else if ( p->kind == fgIOCHANNEL::FG_SERIAL_NMEA_IN ) {
	read_nmea_in(p);
    } else if ( p->kind == fgIOCHANNEL::FG_SERIAL_GARMIN_OUT ) {
	send_garmin_out(p);
    } else if ( p->kind == fgIOCHANNEL::FG_SERIAL_GARMIN_IN ) {
	read_garmin_in(p);
    } else if ( p->kind == fgIOCHANNEL::FG_SERIAL_FGFS_OUT ) {
	send_fgfs_out(p);
    } else if ( p->kind == fgIOCHANNEL::FG_SERIAL_FGFS_IN ) {
	read_fgfs_in(p);
    } else if ( p->kind == fgIOCHANNEL::FG_SERIAL_RUL_OUT ) {
	send_rul_out(p);
    }
}


// process any serial port work
void fgSerialProcess() {
    fgIOCHANNEL *port;
    
    io_iterator current = port_list.begin();
    io_iterator last = port_list.end();

    for ( ; current != last; ++current ) {
	port = current;
	if ( port->kind != fgIOCHANNEL::FG_SERIAL_DISABLED ) {
	    process_port ( port );
	}
    }
}
