/*
 * Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana
 *
 * This file is part of blend.
 *
 * blend 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.
 *
 * blend 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 blend.  If not, see <http://www.gnu.org/licenses/>.
 */

import { Adapter } from "../core/adapter";
import { Metric } from "../core/metric";
import { Dimension } from "../core/dimension";
import { Clause } from "../core/clause";
import { Filter, FilterOperator } from "../core/filter";
import { AggregationType, RelationType } from "../common/types";
import { View } from "../core/view";
import { Pool, PoolConfig } from "pg";

interface DimInfo {
    dim: Dimension;
    views: View[];
}

export class PostgresAdapter extends Adapter {
    private pool: Pool;

    constructor (config: PoolConfig) {
        super();
        this.pool = new Pool(config);
    }
    public getDataFromView(view: View, cb: (error: Error, result?: any[]) => void): void {
        const materialized = this.searchMaterializedViews(view).sort((a, b) => {
            return (a.id < b.id) ? -1 : 1;
        });

        const unique = [materialized[0]];
        for (let i = 1; i < materialized.length; ++i) {
            if (materialized[i - 1].id !== materialized[i].id) {
                unique.push(materialized[i]);
            }
        }

        const query = this.buildQuery(view, unique);
        this.pool.connect((err, client, done) => {
            if (err) {
                cb (err);
                return;
            }
            client.query(query, [], (error, result) => {
                // call 'done()' to release client back to pool
                done();
                cb(error, (result) ? result.rows : null);
            });
        });
    }

    public materializeView(view: View): boolean {
        return false;
    }

    private searchMaterializedViews(view: View): View[] {
        let r: View[] = [];
        if (view.materialized) {
            return [view];
        }

        else {
            let children = view.childViews;
            for (let i = 0; i < children.length; ++i) {
                r = r.concat(this.searchMaterializedViews(children[i]));
            }
        }

        return r;
    }

    private buildQuery(target: View, views: View[]) {
        const metrics = target.metrics;
        const dimensions = target.dimensions;
        const clauses = target.clauses;

        let dimMap: {[key: string]: DimInfo} = {};
        let metMap: {[key: string]: View[]} = {};
        let nameMap: {[key: string]: View} = {};

        for (let i = 0; i < views.length; ++i) {
            const mets = views[i].metrics;
            const dims = views[i].dimensions;
            for (let j = 0; j < mets.length; ++j) {
                if (!metMap[mets[j].name])  {
                    metMap[mets[j].name] = [views[i]];
                    nameMap[mets[j].name] = views[i];
                }

                else {
                    metMap[mets[j].name].push(views[i]);
                }
            }

            for (let j = 0; j < dims.length; ++j) {
                if (!dimMap[dims[j].name])  {
                    dimMap[dims[j].name] = {
                        dim: dims[j],
                        views: [views[i]]
                    };
                    nameMap[dims[j].name] = views[i];
                }

                else {
                    dimMap[dims[j].name].views.push(views[i]);
                }
            }
        }

        // Projection
        const strMetrics = metrics.map((metric) => {
            const view = nameMap[metric.name];
            let func = this.getAggregateFunction(metric.aggregation, view.origin);
            let quotedName = "\"" + metric.name + "\"";
            let extMetric = func + "(" + this.buildColumn(metric, view.id) + ")";
            return extMetric + " AS " + quotedName;
        });

        const parsedDimensions = dimensions.map((dimension) => {
            let dim = dimension;
            while (!nameMap[dim.name]) {
                dim = dim.parent;
            }
            const view = nameMap[dim.name];
            const quotedName = "\"" + dimension.name + "\"";
            let extDimension = this.buildColumn(dim, view.id);
            let aux = dimension;
            while (aux.name !== dim.name) {
                extDimension = this.translateRelation(aux.relation, extDimension);
                aux = aux.parent;
            }
            return { aliased: extDimension + " AS " + quotedName, noalias: extDimension };
        });

        const strDimensions = parsedDimensions.map ((item) => item.aliased);
        const grouped = parsedDimensions.map((item) => item.noalias);
        const elements = strMetrics.concat(strDimensions);

        // Joins
        let conds: string[] = [];
        for (let i in dimMap) {
            let remainViews = dimMap[i].views.slice();
            let dim = dimMap[i].dim;
            let leftSide = this.buildColumn(dim, remainViews.shift().id);
            if (remainViews.length > 0) {
                while (remainViews.length > 0) {
                    const id = remainViews.shift().id;
                    const rightSide = this.buildColumn(dim, id);
                    conds.push(leftSide + " = " + rightSide);
                }
            }

        }

        // Selection
        let covered: Clause[] = [];
        for (let i = 0; i < views.length; ++i) {
            // Get the clauses that children already cover
            covered = covered.concat(views[i].clauses);
        }

        const toCover = clauses.filter((item) => !covered.some ((clause) => {
            return clause.id === item.id;
        }));

        toCover.forEach((item) => conds.push("(" + this.translateClause(item, nameMap) + ")"));

        // Assembly

        const projection = "SELECT " + elements.join(",");
        const source = " FROM " + views.map((view) => "view_" + view.id).join(",");
        const selection = (conds.length > 0) ? " WHERE " + conds.join(" AND ") : "";
        let grouping = "";
        if (grouped.length > 0) {
            grouping = " GROUP BY " + grouped.join(",");
        }

        return projection + source + selection + grouping + ";";

    }

    private getAggregateFunction(aggrType: AggregationType, origin: boolean): string {
        switch (aggrType) {
            case AggregationType.SUM:
                return "SUM";
            case AggregationType.AVG:
                return "AVG";
            case AggregationType.COUNT:
                return (origin) ? "COUNT" : "SUM";
            default:
                return  "";
        }

    }

    private translateRelation(relation: RelationType, arg: string): string {
        switch (relation) {
            case RelationType.DAY:
                return this.applyRelation("EXTRACT", ["DAY FROM "], [arg]);
            case RelationType.MONTH:
                return this.applyRelation("EXTRACT", ["MONTH FROM "], [arg]);
            case RelationType.YEAR:
                return this.applyRelation("EXTRACT", ["YEAR FROM "], [arg]);
            case RelationType.DAYOFWEEK:
                return this.applyRelation("EXTRACT", ["DOW FROM "], [arg]);
            default:
                return "";
        }

    }

    private applyRelation(name: string, args: string[], values: string[]): string {
        /*
            This adapter uses the concept of functions in Postgres to
            implement BLENDB sub-dimention relations, this functions
            applys the transformation to build the call of a Postgres
            funtion. Note that this function can be native from postgres,
            like EXTRACT, or even implemented on the database.
            This function is short and only used in the translateRelation
            method however is a bit complex and is possible to be used
            several times, because of that is puted appart to make easyer update
            and avoid problems
            Example
            applyRelation ("EXTRACT", "["DAY FROM"]", ["view_0.date"])
            output: EXTRACT(DAY FROM view_0.date)
        */
        return name + "(" + args.map((item, idx) => item + values[idx]).join(",") + ")";
    }

    private buildColumn (item: Metric|Dimension, id: string): string {
        const quotedName = "\"" + item.name + "\"";
        return "view_" + id + "." + quotedName;
    }

    private translateClause(clause: Clause, map: {[key: string]: View}): string {
        let r = clause.filters.map((item) => this.translateFilter(item, map));
        return r.join(" OR ");
    }

    private translateFilter(filter: Filter, map: {[key: string]: View}): string {
        const viewId = map[filter.target.name].id;
        const leftSide = this.buildColumn(filter.target, viewId);
        const op = this.translateOperator(filter.operator);
        const dataType = this.translateDataType(filter.target.dataType);
        const quotedValue = "'" + filter.value + "'";
        return leftSide + op + quotedValue + dataType;
    }

    private translateOperator(op: FilterOperator): string {
        switch (op) {
            case FilterOperator.EQUAL:
                return " = ";
            case FilterOperator.NOTEQUAL:
                return " != ";
            case FilterOperator.GREATER:
                return " > ";
            case FilterOperator.LOWER:
                return " < ";
            case FilterOperator.GREATEREQ:
                return " >= ";
            case FilterOperator.LOWEREQ:
                return " <= ";
            default:
                return  "";
        }

    }

    private translateDataType(dt: string ): string {
        switch (dt) {
            case "date":
                return "::DATE";
            case "integer":
                return "::INTEGER";
            case "boolean":
                return "::BOOLEAN";
            default:
                return "";
        }
    }
}