/**
	@file
	boardin - a max object (Arduino UNO board inputs)
 */


// Firmata-for-Max Library headers
#include "maxlib.h"

// required Max core headers
#include "ext.h"							// standard Max include, always required
#include "ext_obex.h"						// required for new style Max object


////////////////////////// object struct
typedef struct boardin
{
    // the Max object itself (must be first)
    t_object                    ob;
    
    // Arduino board connection
    board_t                     *board_ob;
    
    // selected pins to read
    void                        **outlets;
    long                        pinsCount;  //todo - remove? should be the sum of input attributes
    long                        pinsArray[TOTAL_IN_PINS];
    long                        valuesArray[TOTAL_IN_PINS];
    long                        modesArray[TOTAL_IN_PINS];
    
    // boardin attributes:
    t_symbol                    *att_portname;
    t_symbol                    *att_baudrate;
    long                        att_digitalPins[TOTAL_IN_PINS], digitalPinsCount;
    long                        att_analogPins[TOTAL_IN_PINS], analogPinsCount;
    
} t_boardin;

///////////////////////// function prototypes
//// standard set
void *boardin_new(t_symbol *s, long argc, t_atom *argv);
void boardin_free(t_boardin *x);
void boardin_assist(t_boardin *x, void *b, long m, long a, char *s);
void boardin_print(t_boardin *x);
void boardin_bang(t_boardin *x);

//privates
void boardin_printAvailPorts(t_boardin *x);
void boardin_updatePin(t_boardin *x, int index, int val);
int  boardin_parseArgs(t_boardin *x, long argc, t_atom *argv);
int  boardin_getBaudRate(t_boardin *x);
bool boardin_pinNumAlreadyExists(t_boardin *x, int pin);    // todo - check with boardout pins (handle duplicates)?
void boardin_init(t_boardin *x);
void boardin_connectBoard(t_boardin *x, int len, int *pins, int *modes);
int boardin_pinDigitalRange(int pin);
int boardin_pinAnalogRange(int pin);

//internals
t_max_err boardout_att_portname_get(t_boardin *x, t_object *attr, long argc, t_atom *argv);
t_max_err boardout_att_portname_set(t_boardin *x, t_object *attr, long argc, t_atom *argv);
t_max_err boardin_digitalPins_set(t_boardin *x, t_object *attr, long argc, t_atom *argv);   // Disable editing attribute in Inspector - restrict to Read Only!
t_max_err boardin_analogPins_set(t_boardin *x, t_object *attr, long argc, t_atom *argv);    // Disable editing attribute in Inspector - restrict to Read Only!


//////////////////////// global class pointer variable
void *boardin_class;

void ext_main(void *r)
{
    t_class *c;
    
    c = class_new("cr.boardin", (method)boardin_new, (method)boardin_free,
                  (long)sizeof(t_boardin),
                  0L /* leave NULL!! */, A_GIMME, 0);
    
    /* you CAN'T call this from the patcher */
    class_addmethod(c, (method)boardin_assist,      "assist",  A_CANT,     0);
    class_addmethod(c, (method)boardin_print,       "print",               0);
    class_addmethod(c, (method)boardin_bang,        "bang",                0);
    
    class_register(CLASS_BOX, c); /* CLASS_NOBOX */
    boardin_class = c;
    
   
    // set attributes

    CLASS_ATTR_SYM(c, "portname", 0, t_boardin, att_portname);
    CLASS_ATTR_LABEL(c, "portname", 0, "Serial Port");
    //CLASS_ATTR_ENUM(c, "portname", 0, "");
    // Disable editing attribute in Inspector - Read Only!
    CLASS_ATTR_ACCESSORS(c, "portname", NULL, NULL);
    
    //char* baudratestr = "\"300\" \"1200\" \"2400\" \"4800\" \"9600\" \"14400\" \"19200\" \"28800\" \"38400\" \"57600\" \"115200\"";
    CLASS_ATTR_SYM(c, "baud", 0, t_boardin, att_baudrate);
    CLASS_ATTR_LABEL(c, "baud", 0, "Baud Rate");
    //CLASS_ATTR_ENUM(c, "baud", 0, baudratestr);
    // Disable editing attribute in Inspector - Read Only!
    CLASS_ATTR_ACCESSORS(c, "baud", NULL /*boardin_att_baudrate_get*/, NULL /*boardin_att_baudrate_set*/);
    
    // pins modes attributes (for the inspector window, read only!)
    CLASS_ATTR_LONG_VARSIZE(c, "digitalPins", 0, t_boardin, att_digitalPins, digitalPinsCount, TOTAL_IN_PINS);
    CLASS_ATTR_LABEL(c, "digitalPins", 0, "Digital Pins");
    // Disable editing attribute in Inspector - Read Only!
    CLASS_ATTR_ACCESSORS(c, "digitalPins", NULL /*boardout_digitalPins_get*/, boardin_digitalPins_set);
    
    CLASS_ATTR_LONG_VARSIZE(c, "analogPins", 0, t_boardin, att_analogPins, analogPinsCount, TOTAL_IN_PINS);
    CLASS_ATTR_LABEL(c, "analogPins", 0, "Analog Pins");
    // Disable editing attribute in Inspector - Read Only!
    CLASS_ATTR_ACCESSORS(c, "analogPins", NULL /*boardout_analogPins_get*/, boardin_analogPins_set);
    
    // attributes order of appearance  in the inspector
    CLASS_ATTR_ORDER(c, "portname", 0, "1");
    CLASS_ATTR_ORDER(c, "baud", 0, "2");
    CLASS_ATTR_ORDER(c, "digitalPins", 0, "3");
    CLASS_ATTR_ORDER(c, "analogPins", 0, "4");
    
    post("Arduino UNO boardin object created (Reader).");
}

void boardin_assist(t_boardin *x, void *b, long m, long index, char *s)
{
    int pin = x->pinsArray[index];

    if (m == ASSIST_OUTLET) { // outlet messages
    
        switch(x->modesArray[index]) {
            case MODE_INPUT:
                sprintf(s, "pin D%d mode DIGITAL", pin);
                break;
            case MODE_ANALOG:
                pin -= 14;
                // analog pins are numbered as:
                // Max: 20-25
                // Arduino: A0-A5
                // Firmata: (#14-#19)
                sprintf(s, "pin %d (A%d) mode ANALOG", pin+20, pin);
                break;
            default:
                sprintf(s, "pin %d is INVALID", pin);
        }

    }
    else {	// inlet messages
        sprintf(s, "msg inlet");
    }
    
}

void boardin_free(t_boardin *x) {

    if(x == NULL) return;
    
    if(x->att_portname) {
    
        if(x->att_portname->s_thing) {
            board_mgr_t *mgr = ((board_mgr_t *)(x->att_portname->s_thing));
            if(mgr->board_in != NULL) { // && mgr->board_in == x->board_ob) {
                
                //release the boardin connection
                //post("disconnecting boardin object"); //debug
                mgr->board_in = NULL;
                int val = maxclient_disconnect(x->board_ob); // kill listener
                if(val == -1) {
                    post("error disconnecting, %s", x->board_ob->err_msg);
                    return;
                }
                //debug
                //else {
                 //   post("boardin disconnected");
                //}
            }
            //debug
            //else {
               // post("error, boardin wasn't found");
            //}
        
            if(mgr->board_out == NULL) {
                
                // no writer board is connected, release the port altogether
                post("closing the port"); //debug
                int val = maxclient_shutdown(x->board_ob);  // close port
                if(val == -1) {
                    post("error shutting down, %s", x->board_ob->err_msg);
                    return;
                }
                //debug
                //else {
                //    post("boardin shut down");
                //}
                x->att_portname->s_thing = NULL;
                free(mgr);
                //mgr->board = NULL;
                //mgr->board_in = NULL;
            }
            //debug
            //else {
            //    post("boardout object found");
            //}
        }
        
        //debug
        //else {
            //post("port is not in use by any other board object");
        //}
    }
    
    //debug
    //else {
        //post("no portname found");
    //}
    
    /*
    if (x->board_ob != NULL) {
        free(x->board_ob);
        post("board object deleted");
    }*/
    
    // free allocated outlets
    if (x->outlets) {
        free(x->outlets);
    }
    
    //post("boardin object deleted"); //debug
}


void *boardin_new(t_symbol *s, long argc, t_atom *argv)
{
    t_boardin *x = (t_boardin *)object_alloc(boardin_class);
    
    // verify memory allocated successfully
    if (x == NULL ) {
        post( "Max is out of memory, could not create new boardin object.");
        return NULL;
    }
   
    boardin_init(x); // todo - remove, for debug
    
    x->board_ob = NULL;
    
    // parse input arguments to set up the board configuration
    if (boardin_parseArgs(x, argc, argv) == -1) {
        //Error - Invalid arguments
        object_error((t_object *)x, "Boardin args are invalid, could not create new boardin object.");
        //object_free(x);
        return (x);
    }

    // debug:
    //if(x->att_portname && x->att_baudrate) {
    //    object_post((t_object *)x, "port name: %s\t rate: %d", x->att_portname->s_name, atoi(x->att_baudrate->s_name));
    //}
    
    // setup pins configuration as requested
    int len_config = x->digitalPinsCount + x->analogPinsCount;
    int pins[len_config], modes[len_config];
    
    int len = 0;
    // digital pins
    for(int i=0; i<x->digitalPinsCount; ++i) {
        pins[len] = x->att_digitalPins[i];
        modes[len] = MODE_INPUT;
        len++;
    }
    
    // analog pins
    for(int i=0; i<x->analogPinsCount; ++i) {
        pins[len] = x->att_analogPins[i] + 14;  // analog pins are numbered 14-19 corresponding to A0-A5
        modes[len] = MODE_ANALOG;
        len++;
    }
    

    // connect to the board, and start listening thread
    if(x->att_portname)
        boardin_connectBoard(x, len, pins, modes);
    
    
    // create int outlets from right to left (highest-right to 1-left)
    x->outlets = (void **)malloc(x->pinsCount*sizeof(void *));
    // verify memory allocated successfully
    if (x->outlets == NULL ) {
        object_error((t_object *)x, "Max is out of memory, could not create new boardin object.");
        //boardin_free(x);
        return (x);
    }
    for (int i = x->pinsCount; i > 0;) {
        --i;
        x->outlets[i] = intout(x);
    }
    
    // boardin object is now all set up
    return (x);
}

int boardin_parseArgs(t_boardin *x, long argc, t_atom *argv) {
    
    // get updated list of available ports
    char *ports_avail[MAX_PORTS];
    int num_ports = serial_portlist(ports_avail, MAX_PORTS);
    
    // set baud rate to default
    char str[20];
    sprintf(str, "%d", DEFAUTLT_BAUD);
    x->att_baudrate = gensym(str);
    
    // parse input arguments
    int i = 0;
    
    // check if the first argument is the port index
    if(argv->a_type == A_SYM){
        char *portCh = atom_getsym(argv)->s_name;
        //object_post((t_object *)x, "port char: %c", portCh[0]);
        int index = portCh[0]-'a';
        //object_post((t_object *)x, "port index: %d", index);
        
        if (index >= 0 && index < num_ports) {
            //strncpy(x->portname, ports_avail[index], MAX_PORT_NAME);
            x->att_portname = gensym(ports_avail[index]);
            if(x->att_portname->s_thing == NULL) {
                // the port is not in use by other boardin/out objects
                board_mgr_t *mgr = (board_mgr_t*)malloc(sizeof(board_mgr_t)); //newPortMgr(IN_BOARD);
                mgr->board_out = NULL;
                mgr->board_in = NULL;
                mgr->board = NULL;
                x->att_portname->s_thing = (t_object*)mgr;
            }
            //x->att_portname->s_thing = NULL;  //TODO add ref to the serial port
            //object_post((t_object *)x, "port name is: (%s)", x->att_portname->s_name); // debug
            //object_post((t_object *)x, "port thing is: (%s)", x->att_portname->s_thing); //debug
            i = 1;
        }
    }
    
    // Parse DIGITAL and ANALOG pins lists
    bool analogFound = false, digitalFound = false;
    x->pinsCount = 0;
    x->analogPinsCount = 0;
    x->digitalPinsCount = 0;
    
    for (; i < argc; i++) {
        
        if((argv+i)->a_type == A_SYM) {
            
            t_symbol *s = atom_getsym(argv+i);
            
            // if the symbol is digital
            if( ! strcmp_case_insensitive(s->s_name, "@digital")) {
                //post("Found DIGITAL");
                digitalFound = true;
                analogFound = false;
            }
            // if symbol is analog
            else if( ! strcmp_case_insensitive(s->s_name, "@analog")) {
                //post("Found ANALOG");
                analogFound = true;
                digitalFound = false;
            }
            // if symbol is baud
            else if( ! strcmp_case_insensitive(s->s_name, "@baud")) {
                //post("Found BAUD");
                digitalFound = false;
                analogFound = false;
                // check if the baud rate was entered as input
                if((i+1)< argc && (argv+i+1)->a_type == A_LONG) {
                    // move to the next input atom for the baud rate
                    i++;
                    //x->baudrate = atom_getlong(argv+i);
                }
            }
            
            else {
                // otherwise, unknown attribute symbol
                digitalFound = false;
                analogFound = false;
                // error
                object_error((t_object *)x, "forbidden argument %s", s->s_name);
                //return -1;
            }
        }
        
        else if((argv+i)->a_type == A_LONG) {
            
            int pin = atom_getlong(argv+i);
            
            if (analogFound) {
                if (!boardin_pinAnalogRange(pin)) {
                    // error
                    object_error((t_object *)x, "Invalid pin number %d, analog pins range [20-25]", pin);
                    return 0;
                }

                // if the pin is analog, then substruct PIN_OFFSET
                // Digital pins [D0-D13] are numbered  0-13 indexed as  0-13 in Firmata
                // Analog  pins [A0-A5]  are numbered 20-25 indexed as 14-19 in Firmata
                pin -= PIN_OFFSET;
            }
            
            if (digitalFound && !boardin_pinDigitalRange(pin)) {
                    // error
                    object_error((t_object *)x, "Invalid pin number %d, digital pins range [0-13]", pin);
                    return 0;
            }
            
            if((analogFound || digitalFound) && x->pinsCount < TOTAL_IN_PINS) {
            
                // first check if the requested pin already exists
                if(boardin_pinNumAlreadyExists(x, pin)) {
                    // error
                    object_error((t_object *)x, "forbidden argument, pin %d already exists", pin);
                }
                
                // we verified the pin does not exist, add it now
                else {
                    x->pinsArray[x->pinsCount] = pin;
                    x->valuesArray[x->pinsCount] = 0;
                    if(analogFound) {
                        x->modesArray[x->pinsCount] = MODE_ANALOG;
                        x->att_analogPins[x->analogPinsCount] = pin;
                        x->analogPinsCount++;
                    }
                    else if(digitalFound) {
                        x->modesArray[x->pinsCount] = MODE_INPUT;
                        x->att_digitalPins[x->digitalPinsCount] = pin;
                        x->digitalPinsCount++;
                    }
                    x->pinsCount++;
                }
            }
            else {
                // error
                if(x->pinsCount >= TOTAL_IN_PINS) {
                    object_error((t_object *)x, "forbidden argument, too many pins requested");
                    //return -1;
                }
                object_error((t_object *)x, "forbidden argument, number %d is out of context", pin);
                //return -1;
            }
        }
        
        else {
            // error
            object_error((t_object *)x, "forbidden argument, expected format:");
            object_error((t_object *)x, "[port] [@digital pins_list] [@analog pins_list]");
            //return -1;
            
        }
    }

    return 0;
}

void boardin_bang(t_boardin *x){
    
    // update input pin values
    maxclient_getInputValues(x->board_ob, x->pinsArray, x->valuesArray, x->pinsCount);
    
    /*
    // todo - remove (for debug only)
    object_post((t_object *)x, "bang!");
    for(int i=0; i<x->pinsCount; ++i) {
        //x->valuesArray[i] = (i+1);
        object_post((t_object *)x, "outlet %d=pin%d=%d", i, x->pinsArray[i], x->valuesArray[i]);
    }*/
    
    t_atom val;
    for(int i=0; i<x->pinsCount; ++i) {
        atom_setlong(&val, x->valuesArray[i]);
        outlet_int(x->outlets[i], atom_getlong(&val));
    }
}

void boardin_print(t_boardin *x){
    boardin_printAvailPorts(x);
}

void boardin_printAvailPorts(t_boardin *x) {
    
    char *ports_avail[MAX_PORTS];
    
    int num_ports = serial_portlist(ports_avail, MAX_PORTS);
    
    for(int i=0; i<num_ports; ++i) {
        object_post((t_object *)x, "port %c: %s", i+'a', ports_avail[i]);
    }
}

t_max_err boardin_digitalPins_set(t_boardin *x, t_object *attr, long argc, t_atom *argv) {
    return MAX_ERR_NONE;
}

t_max_err boardin_analogPins_set(t_boardin *x, t_object *attr, long argc, t_atom *argv) {
    return MAX_ERR_NONE;
}

bool boardin_pinNumAlreadyExists(t_boardin *x, int pin) {
    
    for (int i = 0; i<x->digitalPinsCount; ++i) {
        if(x->att_digitalPins[i] == pin)
            return true;
    }
    
    for (int i = 0; i<x->analogPinsCount; ++i) {
        if(x->att_analogPins[i] == pin)
            return true;
    }
    
    return false;
}

void boardin_init(t_boardin *x) {

    if(x == NULL)
        return;
    
    x->outlets = NULL;
    x->pinsCount = 0;
    x->att_portname = NULL;
    x->att_baudrate = NULL;
    x->digitalPinsCount = 0;
    x->analogPinsCount = 0;
}


void boardin_connectBoard(t_boardin *x, int len, int *pins, int *modes) {

    if(!x || !x->att_portname || !x->att_portname->s_thing) return;
    
    
    // search symbol port_name. if found, then board is already connected to by a boardout
    // object (writer object), then get the pointer to that board object. Otherwise, create
    // a new board object and initialise it with the requested baud rate and the rest of
    // the pins (read) configuration.
    
    board_mgr_t *mgr = ((board_mgr_t *)(x->att_portname->s_thing));
    if(mgr->board_in != NULL) {
        
        // error - there is already another boardin object connected
        object_error((t_object *)x, "connect: port name: %s is already occupied with another boardin object!", x->att_portname->s_name);
        return;
    }
    
    else if(mgr->board_out != NULL) {
            
        //object_post((t_object *)x, "connect: boardout found");   // debug
        //object_post((t_object *)x, "x->board_object=%s", x->board_ob); // debug
        x->board_ob = mgr->board;
        mgr->board_in = x;
    }
    
    else {
            
        // requested port is not connected to by any other board object, then
        // create a new Arduino board object,  initialise and connect to (serial) port
        
        //object_post((t_object *)x, "NEW BOARD IS BEING CREATED"); // debug
        
        char err_return[4000];
        x->board_ob = maxclient_newboard(x->att_portname->s_name, atoi(x->att_baudrate->s_name), err_return);
        // verify succeful memory allocation
        if(x->board_ob == NULL) {
            // error - there is already another boardin object connected
            object_error((t_object *)x, "Max could not create new boardin object.");
            object_error((t_object *)x, "%s", err_return);
            return;
        }
        // debug
        //else {
        //    post("maxclient_newboard succeedded");
        //}
        
        mgr->board = x->board_ob;
        mgr->board_in = x;
    }
    
    //object_post((t_object *)x, "CONNECTING TO BOARD"); //debug
    
    // start a new listener thread and
    // set board configuration (i.e., pin modes) as requested
    int result = maxclient_connect(x->board_ob, len, pins, modes, IN_BOARD);
    if( result != 0) {
        object_error((t_object *)x, "Boardin couldn't connect to board.");
        return;
    }
    
    // debug
    //else {
    //    object_post((t_object *)x, "Listening thread started.");
    //}
}

int boardin_pinDigitalRange(int pin) {
    return ((pin < 14 && pin >=0)? 1 : 0);
}

int boardin_pinAnalogRange(int pin) {
    // Analog pins range: 20 [A0] .. 25 [A5]
    return ((pin < 26 && pin >=20)? 1 : 0);
}







