#include "GwXDRMappings.h"
#include "N2kMessages.h"

double PtoBar(double v)
{
    if (N2kIsNA(v))
        return v;
    return v / 100000L;
}
double BarToP(double v)
{
    if (N2kIsNA(v))
        return v;
    return v * 100000L;
}
double ltrTom3(double v)
{
    if (N2kIsNA(v))
        return v;
    return v / 1000.0;
}
double m3ToL(double v)
{
    if (N2kIsNA(v))
        return v;
    return v * 1000.0;
}
double ph2ps(double v)
{
    if (N2kIsNA(v))
        return v;
    return v * 3600.0;
}
double ps2ph(double v)
{
    if (N2kIsNA(v))
        return v;
    return v / 3600.0;
}
//https://www.nmea.org/Assets/20190916%20Standards%20Update%20NMEA%20Conferencev2.pdf
GwXDRType *types[] = {
    new GwXDRType(GwXDRType::PRESS, "P", "P"),
    new GwXDRType(GwXDRType::PRESS, "P", "B",
                  PtoBar,
                  BarToP),
    new GwXDRType(GwXDRType::VOLT, "U", "V"),
    new GwXDRType(GwXDRType::AMP, "I", "A"),
    new GwXDRType(GwXDRType::TEMP, "C", "K"),
    new GwXDRType(GwXDRType::TEMP, "C", "C", CToKelvin, KelvinToC),
    new GwXDRType(GwXDRType::HUMID, "H", "P"), //percent
    new GwXDRType(GwXDRType::VOLPERCENT, "V", "P"),
    new GwXDRType(GwXDRType::VOLUME, "V", "M", m3ToL, ltrTom3),
    new GwXDRType(GwXDRType::FLOW, "R", "I", ps2ph, ph2ps),
    new GwXDRType(GwXDRType::GENERIC, "G", ""),
    new GwXDRType(GwXDRType::DISPLACEMENT, "A", "P"),
    new GwXDRType(GwXDRType::RPM,"T","R"),
    //important to have 2x NULL!
    NULL,
    NULL};

static GwXDRType *findType(GwXDRType::TypeCode type, int *start = NULL)
{
    int from = 0;
    if (start != NULL)
        from = *start;
    if (types[from] == NULL)
        return NULL;
    int i = from;
    for (; types[i] != NULL; i++)
    {
        if (types[i]->code == type)
        {
            if (start != NULL)
                *start = i + 1;
            return types[i];
        }
    }
    if (start != NULL)
        *start = i;
    return NULL;
}

#include "GwXdrTypeMappings.h"

static GwXDRType::TypeCode findTypeMapping(GwXDRCategory category, int field)
{
    for (int i = 0; typeMappings[i] != NULL; i++)
    {
        if (typeMappings[i]->fieldIndex == field &&
            typeMappings[i]->category == category)
        {
            return typeMappings[i]->type;
        }
    }
    return GwXDRType::UNKNOWN;
}
//category,direction,selector,field,instanceMode,instance,name
String GwXDRMappingDef::toString()
{
    String rt = "";
    rt += String((int)category);
    rt += ",";
    rt += String((int)direction);
    rt += ",";
    rt += String(selector);
    rt += ",";
    rt += String(field);
    rt += ",";
    rt += String(instanceMode);
    rt += ",";
    rt += String(instanceId);
    rt += ",";
    rt += xdrName;
    return rt;
}
bool GwXDRMappingDef::handleToken(String tok, int index, GwXDRMappingDef *def)
{
    switch (index)
    {
        int i;
    case 0:
        //category
        i = tok.toInt();
        if (i < XDRTEMP || i > XDRENGINE)
            return false;
        def->category = (GwXDRCategory)i;
        return true;
    case 1:
        //direction
        i = tok.toInt();
        if (i < GwXDRMappingDef::M_DISABLED ||
            i >= GwXDRMappingDef::M_LAST)
            return false;
        def->direction = (GwXDRMappingDef::Direction)i;
        return true;
    case 2:
        //selector
        //TODO: check selector?
        i = tok.toInt();
        def->selector = i;
        return true;
    case 3:
        //field
        i = tok.toInt();
        def->field = i;
        return true;
    case 4:
        //instance mode
        i = tok.toInt();
        if (i < GwXDRMappingDef::IS_SINGLE ||
            i >= GwXDRMappingDef::IS_LAST)
            return false;
        def->instanceMode = (GwXDRMappingDef::InstanceMode)i;
        return true;
    case 5:
        //instance id
        i = tok.toInt();
        def->instanceId = i;
        return true;
    case 6:
        def->xdrName=tok;
        return true;
    default:
        break;
    }
    return true; //ignore unknown trailing stuff
}
GwXDRMappingDef *GwXDRMappingDef::fromString(String s)
{
    int found = 0;
    int last = 0;
    int index = 0;
    GwXDRMappingDef *rt = new GwXDRMappingDef();
    while ((found = s.indexOf(',', last)) >= 0)
    {
        String tok = s.substring(last, found);
        if (!handleToken(tok, index, rt))
        {
            delete rt;
            return NULL;
        }
        last = found + 1;
        index++;
    }
    if (last < s.length())
    {
        String tok = s.substring(last);
        if (!handleToken(tok, index, rt))
        {
            delete rt;
            return NULL;
        }
    }
    if (rt->direction == GwXDRMappingDef::M_DISABLED || rt->xdrName == ""){
        delete rt;
        return NULL;
    }
    return rt;
}
String GwXDRMappingDef::getTransducerName(int instance)
{
    String name = xdrName;
    if (instanceMode == GwXDRMappingDef::IS_AUTO)
    {
        name += "#";
        name += String(instance);
    }
    return name;
}

String GwXDRFoundMapping::buildXdrEntry(double value)
{
    char buffer[40];
    String name = getTransducerName();
    if (type->tonmea)
    {
        value = (*(type->tonmea))(value);
    }
    snprintf(buffer, 39, "%s,%.3f,%s,%s",
             type->xdrtype.c_str(),
             value,
             type->xdrunit.c_str(),
             name.c_str());
    buffer[39] = 0;
    return String(buffer);
}

GwXDRMappings::GwXDRMappings(GwLog *logger, GwConfigHandler *config)
{
    this->logger = logger;
    this->config = config;
}

#define MAX_MAPPINGS 100
void GwXDRMappings::begin()
{
    char namebuf[10];
    /*
        build our mappings
        for each defined mapping we fetch the type code and type definition
        and create a mapping entries in our 2 maps:
        n2kmap: map category,selector and field index to a mapping
        n183map: map transducer name, transducer type and transducer unit to a mapping
                 a #nnn will be stripped from the transducer name
        entries in the map are lists of mappings as potentially
        we have different mappings for different instances         
    */
    for (int i = 0; i < MAX_MAPPINGS; i++)
    {
        snprintf(namebuf, 9, "XDR%d", i);
        namebuf[9] = 0;
        GwConfigInterface *cfg = config->getConfigItem(String(namebuf));
        if (cfg)
        {
            GwXDRMappingDef *def = GwXDRMappingDef::fromString(cfg->asCString());
            if (def)
            {
                int typeIndex = 0;
                LOG_DEBUG(GwLog::DEBUG, "read xdr mapping %s from %s", 
                    def->toString().c_str(),namebuf);
                //n2k: find first matching type mapping
                GwXDRType::TypeCode code = findTypeMapping(def->category, def->field);
                if (code == GwXDRType::UNKNOWN)
                {
                    LOG_DEBUG(GwLog::DEBUG, "no type mapping for %s", def->toString().c_str());
                    continue;
                }
                GwXDRType *type = findType(code, &typeIndex);
                if (!type)
                {
                    LOG_DEBUG(GwLog::DEBUG, "no type definition for %s", def->toString().c_str());
                    continue;
                }
                long n2kkey = def->n2kKey();
                auto it = n2kMap.find(n2kkey);
                GwXDRMapping *mapping = new GwXDRMapping(def, type);
                if (it == n2kMap.end())
                {
                    LOG_DEBUG(GwLog::DEBUG, "insert mapping with key %ld", n2kkey);
                    GwXDRMapping::MappingList mappings;
                    mappings.push_back(mapping);
                    n2kMap[n2kkey] = mappings;
                }
                else
                {
                    LOG_DEBUG(GwLog::DEBUG, "append mapping with key %ld", n2kkey);
                    it->second.push_back(mapping);
                }
                //for nmea0183 there could be multiple entries
                //as potentially there are different units that we can handle
                //so after we inserted the definition we do additional type lookups
                while (type != NULL)
                {
                    String n183key = GwXDRMappingDef::n183key(def->xdrName,
                                                              type->xdrtype, type->xdrunit);
                    auto it = n183Map.find(n183key);
                    if (it == n183Map.end())
                    {
                        LOG_DEBUG(GwLog::DEBUG, "insert mapping with n183key %s", n183key.c_str());
                        GwXDRMapping::MappingList mappings;
                        mappings.push_back(mapping);
                        n183Map[n183key] = mappings;
                    }
                    else
                    {
                        LOG_DEBUG(GwLog::DEBUG, "append mapping with n183key %s", n183key.c_str());
                        it->second.push_back(mapping);
                    }
                    type = findType(code, &typeIndex);
                    if (!type)
                        break;
                    mapping=new GwXDRMapping(def,type);    
                }
            }
            else{
                LOG_DEBUG(GwLog::DEBUG,"unable to parse mapping %s",cfg->asCString());
            }
        }
    }
}

/**
 * select the best matching mapping
 * depending on the instance id
 */
GwXDRFoundMapping GwXDRMappings::selectMapping(GwXDRMapping::MappingList *list,int instance,const char * key){
    GwXDRMapping *candidate=NULL;
    for (auto mit=list->begin();mit != list->end();mit++){
        GwXDRMappingDef *def=(*mit)->definition;
        //if there is no instance we take a mapping with instance type ignore
        //otherwise we prefer a matching instance before we use auto/ignore
        if (instance < 0){
            if (def->instanceMode != GwXDRMappingDef::IS_IGNORE) continue;
            LOG_DEBUG(GwLog::DEBUG,"selected mapping %s for %s",def->toString().c_str(),key);
            return GwXDRFoundMapping(*mit);
        }
        else{
            switch(def->instanceMode){
                case GwXDRMappingDef::IS_SINGLE:
                    if (def->instanceId == instance){
                        LOG_DEBUG(GwLog::DEBUG+1,"selected mapping %s for %s, i=%d",
                            def->toString().c_str(),key,instance);
                        return GwXDRFoundMapping(*mit,instance); 
                    }
                case GwXDRMappingDef::IS_AUTO:
                    candidate=*mit;
                    break;
                case GwXDRMappingDef::IS_IGNORE:
                    if (candidate == NULL) candidate=*mit;
                    break;
                    
                default:
                    break;    
            }
        }
    }
    if (candidate != NULL){
        LOG_DEBUG(GwLog::DEBUG+1,"selected mapping %s for %s, i=%d",
            candidate->definition->toString().c_str(),key,instance);
        return GwXDRFoundMapping(candidate,instance);
    }
    LOG_DEBUG(GwLog::DEBUG+1,"no instance mapping found for key=%s, i=%d",key,instance);
    return GwXDRFoundMapping();
}
GwXDRFoundMapping GwXDRMappings::getMapping(String xName,String xType,String xUnit){
    //get an instance suffix from the name and separate it
    int sepIdx=xName.indexOf('#');
    int instance=-1;
    if (sepIdx>=0){
        String istr=xName.substring(sepIdx+1);
        xName=xName.substring(0,sepIdx);
        instance=istr.toInt();
    }
    if (xName == "") return GwXDRFoundMapping();
    String n183Key=GwXDRMappingDef::n183key(xName,xType,xUnit);
    auto it=n183Map.find(n183Key);
    if (it == n183Map.end()) {
        LOG_DEBUG(GwLog::DEBUG+1,"find n183mapping for %s,i=%d - nothing found",n183Key.c_str(),instance);
        return GwXDRFoundMapping();
    }
    return selectMapping(&(it->second),instance,n183Key.c_str());
}
GwXDRFoundMapping GwXDRMappings::getMapping(GwXDRCategory category,int selector,int field,int instance){
    unsigned long n2kKey=GwXDRMappingDef::n2kKey(category,selector,field);
    auto it=n2kMap.find(n2kKey);
    if (it == n2kMap.end()){
        LOG_DEBUG(GwLog::DEBUG+1,"find n2kmapping for c=%d,s=%d,f=%d,i=%d - nothing found",
            (int)category,selector,field,instance);
        addUnknown(category,selector,field,instance);    
        return GwXDRFoundMapping();
    }
    char kbuf[20];
    snprintf(kbuf,19,"%lu",n2kKey);
    kbuf[19]=0;
    GwXDRFoundMapping rt=selectMapping(&(it->second),instance,kbuf);
    if (rt.empty){
        addUnknown(category,selector,field,instance);    
    }
    return rt;
}

bool GwXDRMappings::addUnknown(GwXDRCategory category,int selector,int field,int instance){
    if (unknown.size() >= 200) return false;
    unsigned long uk=((int)category) &0x7f;
    if (selector < 0) selector=0;
    uk=(uk<<7) + (selector &0x7f);
    if (field < 0) field=0;
    uk=(uk<<7) + (field & 0x7f);
    if (instance < 0 || instance > 255) instance=256; //unknown
    uk=(uk << 9) + (instance & 0x1ff);
    unknown.insert(uk);
    return true;
}

const char * GwXDRMappings::getUnMapped(){
    if (unknowAsString == NULL) unknowAsString=new char[(MAX_MAPPINGS+1)*ESIZE];
    *unknowAsString=0;
    char *ptr=unknowAsString;
    for (auto it=unknown.begin();it!=unknown.end();it++){
        snprintf(ptr,ESIZE-1,"%lu\n",*it);
        *(ptr+ESIZE-1)=0;
        while (*ptr != 0) ptr++;
    }
    return unknowAsString;
}

String GwXDRMappings::getXdrEntry(String mapping, double value,int instance){
    String rt;
    GwXDRMappingDef *def=GwXDRMappingDef::fromString(mapping);
    if (! def) return rt;
    int typeIndex=0;
    GwXDRType::TypeCode code = findTypeMapping(def->category, def->field);
    if (code == GwXDRType::UNKNOWN)
    {
        return rt;
    }
    GwXDRType *type = findType(code, &typeIndex);
    bool first=true;
    while (type){
        GwXDRFoundMapping found(def,type);
        found.instanceId=instance;
        if (first) first=false;
        else rt+=",";
        rt+=found.buildXdrEntry(value);
        type = findType(code, &typeIndex);
    }
    delete def;
    return rt;
}