/*
 Copyright (C) 2019 Jacek Tomasiak (gpl-3.0-or-later)
 Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)

 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 3 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, see <http://www.gnu.org/licenses/>.
*/

#include<cstring>
#include<set>
#include"manufacturers.h"
#include"manufacturer_specificities.h"
#include"meters.h"

std::set<int> diehl_manufacturers = {
    MANUFACTURER_DME,
    MANUFACTURER_EWT,
    MANUFACTURER_HYD,
    MANUFACTURER_SAP,
    MANUFACTURER_SPL
};

// Default keys for Izar/PRIOS and Sharky meters
#define PRIOS_DEFAULT_KEY1 "39BC8A10E66D83F8"
#define PRIOS_DEFAULT_KEY2 "51728910E66D83F8"

// Diehl: Is "A field" coded differently from standard?
DiehlAddressTransformMethod mustTransformDiehlAddress(DiehlFrameInterpretation interpretation)
{
    switch (interpretation) {
        case DiehlFrameInterpretation::PRIOS:
        case DiehlFrameInterpretation::PRIOS_SCR:
        case DiehlFrameInterpretation::REAL_DATA:
            return DiehlAddressTransformMethod::SWAPPING;

        case DiehlFrameInterpretation::SAP_PRIOS:
            return DiehlAddressTransformMethod::SAP_PRIOS;

        case DiehlFrameInterpretation::SAP_PRIOS_STD:
            return DiehlAddressTransformMethod::SAP_PRIOS_STANDARD;

        case DiehlFrameInterpretation::RESERVED:
        case DiehlFrameInterpretation::OMS:
        case DiehlFrameInterpretation::NA:
        default:
            return DiehlAddressTransformMethod::NONE;
    }
}

// Diehl: Determines how to interpret frame
DiehlFrameInterpretation detectDiehlFrameInterpretation(uchar c_field, int m_field, uchar ci_field, int tpl_cfg)
{
    if (diehl_manufacturers.find(m_field) != diehl_manufacturers.end())
    {
        if (c_field == 0x44 || c_field == 0x46) // SND_NR / SND_IR
        {
            switch (ci_field) {
                case 0x71: // Alarm
                    return DiehlFrameInterpretation::REAL_DATA;
                case 0x7A: // EN 13757-3 Application Layer (short tplh)
                    if (((tpl_cfg >> 8) & 0x10) == 0x10) // Bit 12 from MMMMM bits of CFG field
                    {
                        return DiehlFrameInterpretation::REAL_DATA;
                    }
                    return DiehlFrameInterpretation::OMS;
                case 0xA0: // Manufacturer specific
                case 0xA1: // Manufacturer specific
                case 0xA2: // Manufacturer specific
                case 0xA3: // Manufacturer specific
                case 0xA4: // Manufacturer specific
                case 0xA5: // Manufacturer specific
                case 0xA6: // Manufacturer specific
                case 0xA7: // Manufacturer specific
                    if (m_field == MANUFACTURER_SAP)
                    {
                        return DiehlFrameInterpretation::SAP_PRIOS;
                    }
                    return DiehlFrameInterpretation::PRIOS;
                case 0xB0: // Manufacturer specific
                    if (m_field == MANUFACTURER_SAP)
                    {
                        return DiehlFrameInterpretation::SAP_PRIOS_STD;
                    }
                    return DiehlFrameInterpretation::RESERVED;
                case 0xA8: // Manufacturer specific
                case 0xA9: // Manufacturer specific
                case 0xAA: // Manufacturer specific
                case 0xAB: // Manufacturer specific
                case 0xAC: // Manufacturer specific
                case 0xAD: // Manufacturer specific
                case 0xAE: // Manufacturer specific
                case 0xAF: // Manufacturer specific
                case 0xB4: // Manufacturer specific
                case 0xB5: // Manufacturer specific
                case 0xB6: // Manufacturer specific
                case 0xB7: // Manufacturer specific
                    return DiehlFrameInterpretation::RESERVED;
                case 0xB1: // Manufacturer specific
                case 0xB2: // Manufacturer specific
                case 0xB3: // Manufacturer specific
                    return DiehlFrameInterpretation::PRIOS_SCR;
                default:
                    return DiehlFrameInterpretation::OMS;
            }
        }
    }
    return DiehlFrameInterpretation::NA;
}

// Diehl: Determines how to interpret frame
DiehlFrameInterpretation detectDiehlFrameInterpretation(const vector<uchar>& frame)
{
    if (frame.size() < 15)
        return DiehlFrameInterpretation::NA;

    uchar c_field = frame[1];
    int m_field = frame[3] <<8 | frame[2];
    uchar ci_field = frame[10];
    int tpl_cfg = frame[14] <<8 | frame[13];
    return detectDiehlFrameInterpretation(c_field, m_field, ci_field, tpl_cfg);
}

// Diehl: Is "A field" coded differently from standard?
DiehlAddressTransformMethod mustTransformDiehlAddress(const vector<uchar>& frame)
{
    return mustTransformDiehlAddress(detectDiehlFrameInterpretation(frame));
}

// Diehl: transform "A field" to make it compliant to standard
void transformDiehlAddress(vector<uchar>& frame, DiehlAddressTransformMethod transform_method)
{
    if (transform_method == DiehlAddressTransformMethod::SWAPPING)
    {
        debug("(diehl) Pre-processing: swapping address field\n");
        uchar version = frame[4];
        uchar type    = frame[5];
        for (int i = 4; i < 8; i++)
        {
            frame[i] = frame[i+2];
        }
        frame[8] = version;
        frame[9] = type;
    }
    else if (transform_method == DiehlAddressTransformMethod::SAP_PRIOS)
    {
        debug("(diehl) Pre-processing: setting device type to water meter for SAP PRIOS\n");
        frame[8] = 0x00; // version field is used by IZAR as part of meter id on 5 bytes instead of 4
        frame[9] = 0x07; // water meter
    }
    else if (transform_method == DiehlAddressTransformMethod::SAP_PRIOS_STANDARD)
    {
        warning("(diehl) Pre-processing: SAP PRIOS STANDARD transformation not implemented!\n"); // TODO
    }
}

// Diehl: decode LFSR encrypted data used in Izar/PRIOS and Sharky meters
vector<uchar> decodeDiehlLfsr(const vector<uchar> &origin, const vector<uchar> &frame, uint32_t key, DiehlLfsrCheckMethod check_method, uint32_t check_value)
{
    // modify seed key with header values
    key ^= uint32FromBytes(origin, 2); // manufacturer + address[0-1]
    key ^= uint32FromBytes(origin, 6); // address[2-3] + version + type
    key ^= uint32FromBytes(frame, 10); // ci + some more bytes from the telegram...

    int size = frame.size() - 15;
    vector<uchar> decoded(size);

    for (int i = 0; i < size; ++i) {
        // calculate new key (LFSR)
        // https://en.wikipedia.org/wiki/Linear-feedback_shift_register
        for (int j = 0; j < 8; ++j) {
            // calculate new bit value (xor of selected bits from previous key)
            uchar bit = ((key & 0x2) != 0) ^ ((key & 0x4) != 0) ^ ((key & 0x800) != 0) ^ ((key & 0x80000000) != 0);
            // shift key bits and add new one at the end
            key = (key << 1) | bit;
        }
        // decode i-th content byte with fresh/last 8-bits of key
        decoded[i] = frame[i + 15] ^ (key & 0xFF);

        if (check_method == DiehlLfsrCheckMethod::HEADER_1_BYTE)
        {
            // check-byte doesn't match?
            if (decoded[0] != 0x4B) {
                decoded.clear();
                return decoded;
            }
        }
        else if (check_method == DiehlLfsrCheckMethod::CHECKSUM_AND_0XEF)
        {
            if (i == size - 1) {
                uint32_t checksum = 0;
                for (int index = 0; index < size; index++) {
                    checksum += (decoded[index] & 0xFF);
                }
                if ((checksum & 0xEF) != check_value) {
                    decoded.clear();
                    return decoded;
                }
            }
        }
    }

    return decoded;
}

uint32_t uint32FromBytes(const vector<uchar> &data, int offset, bool reverse)
{
    if (reverse)
        return ((uint32_t)data[offset + 3] << 24) |
            ((uint32_t)data[offset + 2] << 16) |
            ((uint32_t)data[offset + 1] << 8) |
            (uint32_t)data[offset];
    else
        return ((uint32_t)data[offset] << 24) |
            ((uint32_t)data[offset + 1] << 16) |
            ((uint32_t)data[offset + 2] << 8) |
            (uint32_t)data[offset + 3];
}

uint32_t convertKey(const vector<uchar> &bytes)
{
    uint32_t key1 = uint32FromBytes(bytes, 0);
    uint32_t key2 = uint32FromBytes(bytes, 4);
    uint32_t key = key1 ^ key2;
    return key;
}

uint32_t convertKey(const char *hex)
{
    vector<uchar> bytes;
    hex2bin(hex, &bytes);
    return convertKey(bytes);
}

// Common: add default manufacturers key if none specified and we know one for the given frame
void addDefaultManufacturerKeyIfAny(const vector<uchar> &frame, TPLSecurityMode tpl_sec_mode, MeterKeys *meter_keys)
{
    if (!meter_keys->hasConfidentialityKey()
        && tpl_sec_mode == TPLSecurityMode::AES_CBC_IV
        && detectDiehlFrameInterpretation(frame) == DiehlFrameInterpretation::OMS)
    {
        vector<uchar> half;
        hex2bin(PRIOS_DEFAULT_KEY2, &half);
        meter_keys->confidentiality_key = vector<uchar>(half.begin(), half.end());
        meter_keys->confidentiality_key.insert(meter_keys->confidentiality_key.end(), half.begin(), half.end());
        debug("(mfct) added default key\n");
    }
}

// Diehl: initialize support of default keys in a meter
void initializeDiehlDefaultKeySupport(const vector<uchar> &confidentiality_key, vector<uint32_t>& keys)
{
    if (!confidentiality_key.empty())
        keys.push_back(convertKey(confidentiality_key));

    // fallback to default keys if no custom key provided
    if (keys.empty())
    {
        keys.push_back(convertKey(PRIOS_DEFAULT_KEY1));
        keys.push_back(convertKey(PRIOS_DEFAULT_KEY2));
    }
}

// Diehl: Is payload real data crypted (LFSR)?
bool mustDecryptDiehlRealData(const vector<uchar>& frame)
{
    DiehlFrameInterpretation fi = detectDiehlFrameInterpretation(frame);
    debug("(diehl) frame %s\n", toString(fi));
    return fi == DiehlFrameInterpretation::REAL_DATA;
}

// Diehl: decrypt real data payload (LFSR)
bool decryptDielhRealData(Telegram *t, vector<uchar> &frame, vector<uchar>::iterator &pos, const vector<uchar> &confidentiality_key)
{
    vector<uint32_t> keys;
    initializeDiehlDefaultKeySupport(confidentiality_key, keys);

    vector<uchar> decoded_content;
    for (auto& key : keys) {
        decoded_content = decodeDiehlLfsr(t->original.empty() ? frame : t->original, frame, key, DiehlLfsrCheckMethod::CHECKSUM_AND_0XEF, frame[14] & 0xEF);
        if (!decoded_content.empty())
            break;
    }

    if (decoded_content.empty())
    {
        if (!t->isSimulated() && t->parserWarns())
        {
            // Do not warn when decryption is not needed since we are running from a simulated file.
            // Or if the telegram cannot be decrypted when listening to all.
            warning("(diehl) Decoding LFSR real data failed.\n");
        }
        else
        {
            if (t->isSimulated())
            {
                debug("(diehl) Decoding LFSR real data failed, probably already decoded since telegram is simulated.\n");
            }
            else
            {
                debug("(diehl) Decoding LFSR real data failed.\n");
            }
        }
        return false;
    }

    debug("(diehl) Decoded LFSR real data: %s\n", bin2hex(decoded_content).c_str());

    frame.erase(pos, frame.end());
    frame.insert(frame.end(), decoded_content.begin(), decoded_content.end());

    return true;
}

const char *toString(DiehlFrameInterpretation i)
{
    switch (i)
    {
    case DiehlFrameInterpretation::NA: return "N/A";
    case DiehlFrameInterpretation::REAL_DATA: return "REAL_DATA";
    case DiehlFrameInterpretation::OMS: return "OMS";
    case DiehlFrameInterpretation::PRIOS: return "PRIOS";
    case DiehlFrameInterpretation::SAP_PRIOS: return "SAP_PRIOS";
    case DiehlFrameInterpretation::SAP_PRIOS_STD: return "SAP_PRIOS_STD";
    case DiehlFrameInterpretation::PRIOS_SCR: return "PRIOS_SCR";
    case DiehlFrameInterpretation::RESERVED: return "RESERVED";
    }
    return "?";
}

const char *toString(DiehlAddressTransformMethod m)
{
    switch (m)
    {
    case DiehlAddressTransformMethod::NONE: return "NONE";
    case DiehlAddressTransformMethod::SWAPPING: return "SWAPPING";
    case DiehlAddressTransformMethod::SAP_PRIOS: return "SAP_PRIOS";
    case DiehlAddressTransformMethod::SAP_PRIOS_STANDARD: return "SAP_PRIOS_STANDARD";
    }
    return "?";
}

void qdsExtractWalkByField(Telegram *t, Meter *driver, DVEntry &mfctEntry, int pos, int n, const string &key_s, const string &fieldName, Quantity quantity) {
    string bytes = mfctEntry.value.substr(pos, n);

    DifVifKey key(key_s);
    DVEntry fieldEntry(0,
                       key,
                       MeasurementType::Instantaneous,
                       key.vif(),
                       set<VIFCombinable>(),
                       set<uint16_t>(),
                       AnyStorageNr,
                       AnyTariffNr,
                       SubUnitNr(0),
                       bytes);

    FieldInfo *fieldInfo = driver->findFieldInfo(fieldName, quantity);
    if (fieldInfo == nullptr) {
        error("(qds) field info not found: %s\n", fieldName.c_str());
    }

    fieldInfo->performExtraction(driver, t, &fieldEntry);
    string info = "*** " + bytes + " (" + fieldInfo->renderJson(driver, &fieldEntry) + ")";

    t->addSpecialExplanation(mfctEntry.offset + pos / 2, n / 2, KindOfData::CONTENT, Understanding::FULL, info.c_str());
}
