const express = require('express')
const mariadb = require('mariadb')
const fs = require('fs');

let db_settings = JSON.parse(fs.readFileSync('resources/db_settings.json', 'utf8'));

const app = express()
const pool = mariadb.createPool({
    host: db_settings.host,
    port: db_settings.port,
    user: db_settings.user,
    password: db_settings.password,
    database: db_settings.database,
    supportBigNumbers: true,
})

const PORT = 9000

function convert_rights(rights) {
    switch (rights) {
        case "Super-User":
            return 0;

        case "PTS-User":
            return 1;

        case "Premium-User":
            return 2;

        case "Standard-User":
            return 3;

        case "Test-User":
            return 4;

        default:
            throw new Error("Could not parse Rights (" + rights + ")");
    }
}

// encapsulate db access
async function queryDB(query) {
    let conn;
    let rows;

    try {
        conn = await pool.getConnection();
        rows = await conn.query(query);
    } catch (err) {
        throw err;
    } finally {
        if (conn) {
            conn.end();
        }
    }

    return rows;
}

async function queryDevice(device, incoming_rights) {
    let config_ids = await queryDB(
        "SELECT config_id " +
        "FROM api_info " +
        "WHERE name=\"Gerät\" AND value LIKE \"" + device + "\""
    );

    let meta_map = {};

    for (let i = 0; i < config_ids.length; i++) {
        const config_id = config_ids[i].config_id;

        let metas = await queryDB(
            "SELECT name, value " +
            "FROM api_info " +
            "WHERE config_id=\"" + config_id + "\""
        );

        let m = {};
        m["data"] = {};

        for (let j = 0; j < metas.length; j++) {
            let value = metas[j].value;
            let name = metas[j].name;

            if (name == "Gerät") {
                m["Gerät"] = value;
            } else {
                let values = JSON.parse(value);

                for (let [key, value] of Object.entries(values)) {
                    if (Array.isArray(value)) {
                        for (let i = 0; i < value.length; i++) {
                            let v = value[i];
                            let rights = convert_rights(v.right);

                            if (rights < incoming_rights) {
                                delete values[key];
                                break;
                            }
                        }
                    } else {
                        if (value.right === undefined) {
                            for (let [inner_key, inner_value] of Object.entries(value)) {
                                if (Array.isArray(inner_value)) {
                                    for (let i = 0; i < inner_value.length; i++) {
                                        let v = inner_value[i];

                                        for (let [inner_inner_key, inner_inner_value] of Object.entries(v)) {
                                            let rights = convert_rights(inner_inner_value.right);

                                            if (rights < incoming_rights) {
                                                delete v[inner_inner_key];
                                            }
                                        }
                                    }
                                }
                                else if (inner_value.right !== undefined) {
                                    let rights = convert_rights(inner_value.right);

                                    if (rights < incoming_rights) {
                                        delete value[inner_key];
                                    }
                                }
                                else {
                                    for (let [inner_inner_key, inner_inner_value] of Object.entries(inner_value)) {
                                        let rights = convert_rights(inner_inner_value.right);

                                        if (rights < incoming_rights) {
                                            delete inner_value[inner_inner_key];
                                        }
                                    }
                                }
                            }
                        } else {
                            let rights = convert_rights(value.right);

                            if (rights < incoming_rights) {
                                delete values[key];
                            }
                        }
                    }
                }

                let d = m["data"];
                let l = Object.keys(d).length;

                if (l != 0) {
                    m["data"] = { ...d, ...values };
                } else {
                    m["data"] = values;
                }

            }
        }

        meta_map[config_id] = m;
    }

    for (let config_id of Object.keys(meta_map)) {
        let results = await queryDB(
            "SELECT id " +
            "FROM api_results " +
            "WHERE config_id=\"" + config_id + "\""
        );

        for (let j = 0; j < results.length; j++) {
            let measurements = await queryDB(
                "SELECT name, value, rights " +
                "FROM api_measurements " +
                "WHERE result_id=\"" + results[j].id + "\""
            );

            m = {}

            for (let k = 0; k < measurements.length; k++) {
                let measurement = measurements[k];

                let rights = measurement.rights.replace(/['"]+/g, '');
                let rights_value = convert_rights(rights);

                if (rights_value >= incoming_rights) {
                    m[measurement.name] = { name: measurement.name, value: measurement.value, rights: rights };
                }
            }

            meta_map[config_id][results[j].id] = m;
        }
    }

    return meta_map;
}

async function calculate(elements, parameter, operation) {
    switch (operation) {
        case "average":
            let tmp = 0;
            let count = 0;

            for (let param of parameter) {
                const value = elements.find(element => element.name == param);

                if (value === undefined) {
                    continue;
                }

                count += 1;
                tmp += value.value;
            }

            if (count == 0) {
                return null;
            }

            return tmp / count;

        case "sum":
            let sum = 0;

            for (let param of parameter) {
                const value = elements.find(element => element.name == param);

                if (value === undefined) {
                    return null;
                }

                sum += value.value;
            }

            return sum;

        case "divide":
            const v1 = elements.find(element => element.name == parameter[0]);
            const v2 = elements.find(element => element.name == parameter[1]);

            if (v1 === undefined || v2 === undefined || v2 == 0) {
                return null;
            }

            return v1.value / v2.value;

        case "multiply":
            let p = 1.0;

            for (let param of parameter) {
                const value = elements.find(element => element.name == param);

                if (value === undefined) {
                    return null;
                }

                p *= value.value;
            }

            return p;

        case "subtract":
            const s1 = elements.find(element => element.name == parameter[0]);
            const s2 = elements.find(element => element.name == parameter[1]);

            if (s1 === undefined || s2 === undefined) {
                return null;
            }

            return s1.value - s2.value;

        case "discard":
            const value = elements.find(element => element.name == parameter[0]);
            const res = elements.find(element => element.name == parameter[1]);

            if (value === undefined || res === undefined) {
                return null;
            }

            if (value.value !== null && !value.value) {
                return res.value;
            } else {
                return 0;
            }

        default:
            throw "unknown operation " + operation;
    }
}

async function createResultForEntry(parameters, descriptions, calculations) {
    let result = {};

    for (let [key, data] of Object.entries(parameters)) {
        if (Object.keys(data).length == 0) {
            return {};
        }

        if (key == "Gerät") {
            continue;
        }

        let elements = findAll(data);

        for (let [calc_key, calc_value] of Object.entries(calculations)) {
            let val = null;

            if (calc_value.const !== undefined) {
                val = parseFloat(calc_value.const[0]);
            } else {
                val = await calculate(elements, calc_value.source, calc_value.operation);
            }

            if (val !== null) {
                elements.push({ name: calc_key, value: val });
            }
        }

        console.info(elements);

        for (let element of elements) {
            let key = String(element.name);
            let value = element.value;

            let description = descriptions[key];

            if (description !== undefined) {
                if (value !== null) {
                    result[key] = {
                        name: description.name,
                        value: value,
                        description: description.description,
                        unit: description.unit,
                    }
                }
            } else {
                for (let [description_key, description] of Object.entries(descriptions)) {
                    if (key.startsWith(description_key)) {
                        result[key] = {
                            name: description.name,
                            value: value,
                            description: description.description,
                            unit: description.unit,
                        }

                        break;
                    }
                }
            }
        }
    }

    return result;
}

async function createResultList(parameters, descriptions, calculations) {
    let result = [];

    for (let entry of Object.values(parameters)) {
        let entry_result = await createResultForEntry(entry, descriptions, calculations);

        if (Object.keys(entry_result).length != 0) {
            result.push(entry_result);
        }
    }

    return result;
}

function findAll(object) {
    let list = [];

    if (object === null) {
        return list;
    }

    if (object.name !== undefined) {
        if (object.value !== undefined) {
            list.push({ name: object.name, value: object.value });
        } else {
            for (let [key, value] of Object.entries(object)) {
                list = list.concat(findAll(value));
            }
        }
    } else {
        for (let [key, value] of Object.entries(object)) {
            list = list.concat(findAll(value));
        }
    }

    return list;
}

async function resultList(cb_filter, rights) {
    // read parameters from DB and only return accessible
    let deviceParameters = await queryDevice(cb_filter, convert_rights(rights));

    // read paremeter description file
    let descriptions = JSON.parse(fs.readFileSync('resources/variable_description.json', 'utf8'));

    // read parameter calculation file,
    let calculations = JSON.parse(fs.readFileSync('resources/variable_calculation.json', 'utf8'));

    // combine description information with parameters queried from DB
    let result = await createResultList(deviceParameters, descriptions, calculations);

    return result;
}

function createFilters() {
    // column | variable name     | values
    // ---------------------------|-----------------------
    // B      | finished_product  | without-finishing, partial-finishing, intermediate, final, finished
    // C      | base_material     | paper, carton, corrugated, solid, fibre, mix, other
    // D      | coating           | no, one-side, two-side
    // E      | fillers           | no, yes
    // F      | polymers          | no, low, high
    // G      | artificial_fibre  | no, yes
    // H      | ???               |
    // I      | printed           | no, yes
    // J      | varnish           | no, yes
    // K      | glue              | no, yes
    // L      | sealing           | no, yes

    let filters = [
        {
            key: "finished_product",
            values: ["without-finishing", "partial-finishing", "intermediate", "final", "finished"]
        },
        {
            key: "base_material",
            values: ["paper", "carton", "corrugated", "solid", "fibre", "mix", "other"]
        },
        {
            key: "coating",
            values: ["no", "one-side", "two-side"]
        },
        {
            key: "fillers",
            values: ["no", "yes"]
        },
        {
            key: "polymers",
            values: ["no", "low", "high"]
        },
        {
            key: "artificial_fibre",
            values: ["no", "yes"]
        },
        {
            key: "printed",
            values: ["no", "yes"]
        },
        {
            key: "varnish",
            values: ["no", "yes"]
        },
        {
            key: "glue",
            values: ["no", "yes"]
        },
        {
            key: "sealing",
            values: ["no", "yes"]
        }
    ];

    return filters
}

// searches through the provided filter and returns a list of them
function getFilter(query) {
    // get static list of implemented filters
    let filters = createFilters();

    let result = [];

    // iterate over every filter and check for correct syntax
    filters.forEach(function (filter) {
        let res = query[filter.key];

        if (res) {
            if (filter.values.find(element => element == res)) {
                result.push({ key: filter.key, value: res });
            } else {
                throw "unknown value (" + res + ") for key (" + filter.key + "). Possible values: " + filter.values;
            }
        }
    });

    return result;
}

function applyFilter(result_list, filters) {
    let results = [];

    // check every result if every filter can be applied
    result_list.forEach(function (result) {
        let filter_success = true;

        // check all filter
        filters.forEach(function (filter) {
            let value = result[filter.key];

            // check if result even holds the key (and value for it)
            if (value) {
                if (value !== filter.value) {
                    filter_success = false;
                }
            } else {
                filter_success = false;
            }
        });

        // only add result if filters could successfully be applied
        if (filter_success == true) {
            results.push(result);
        }
    });

    return results;
}

app.get('/cepi/list/:rights', async (request, response) => {
    let result_list = await resultList("%Cepi%", request.params.rights);

    response.json(result_list)
})

app.get('/cepi/filters', async (_request, response) => {
    response.json(createFilters())
})

app.get('/cepi/list/:rights/filter', async (request, response) => {
    let results;

    try {
        let result_list = await resultList("%Cepi%", request.params.rights);
        let filters = getFilter(request.query);

        results = applyFilter(result_list, filters);
    }
    catch (err) {
        return response.status(400).send({
            message: err
        });
    }

    response.json(results)
})

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`)
})
