import { AbstractExpressionElement, ColumnReference, Expression, NotExpression, UnknownExpressionElement } from "../../../ast";
import { CacheContext } from "../CacheContext";
import { TableReference } from "../../../database/schema/TableReference";
import { flatMap } from "lodash";
import { buildNoReferenceCondition, buildHasReferenceCondition } from "./buildHasReferenceCondition";
import { replaceOperatorAnyToIndexedOperator } from "./replaceOperatorAnyToIndexedOperator";
import { replaceAmpArrayToAny } from "./replaceAmpArrayToAny";
import { findJoinsMeta } from "../../processor/findJoinsMeta";
import { buildJoinVariables } from "../../processor/buildJoinVariables";
import { Table } from "../../../database/schema/Table";
import { Column } from "../../../database/schema/Column";
import { CoalesceFalseExpression } from "../../../ast/expression/CoalesceFalseExpression";
import { fixArraySearchForDifferentArrayTypesInCondition } from "./fixArraySearchForDifferentArrayTypes";


export type RowType = "new" | "old";

export class ConditionBuilder {
    private readonly context: CacheContext;
    constructor(context: CacheContext) {
        this.context = context;
    }

    hasMutableColumns() {
        return this.getMutableColumns().length > 0;
    }

    noReferenceChanges() {
        const importantColumnsRefs = this.triggerTableColumnsToRefs(
            this.context.referenceMeta.columns
        );

        for (const filter of this.context.referenceMeta.filters) {
            const filterColumnsRefs = filter.getColumnReferences();
            importantColumnsRefs.push( ...filterColumnsRefs );
        }

        return this.buildNoChanges( importantColumnsRefs );
    }

    noChanges() {
        const triggerTableColumnsRefs = this.triggerTableColumnsToRefs(
            this.context.triggerTableColumns
        );

        return this.buildNoChanges( triggerTableColumnsRefs );
    }

    buildNoReference(row: string) {
        const condition = buildNoReferenceCondition(this.context);
        const output = this.replaceTriggerTableRefsTo(
            condition,
            row
        );
        return output;
    }

    hasReferenceWithoutJoins(row: string) {
        const needUpdate = this.buildHasReferenceWithoutJoins();
        const output = this.replaceTriggerTableRefsTo(needUpdate, row);
        return output;
    }

    filtersWithJoins(row: string) {
        let conditions: Expression[] = this.context.referenceMeta.filters.slice();

        const aggFilters = this.matchedAllAggFilters();
        if ( aggFilters ) {
            conditions.push(aggFilters);
        }
        
        conditions = conditions.filter(condition =>
            this.hasJoinsInside(condition)
        );

        conditions = conditions.map(condition =>
            this.replaceTriggerTableRefsTo(condition, row) as Expression
        );

        const output = conditions.length === 1 ? conditions[0] : Expression.and(conditions);
        if ( !output.isEmpty() ) {
            return output;
        }
    }

    needUpdateConditionOnUpdate(row: string) {
        const needUpdate = this.buildNeedUpdateConditionOnUpdate();
        const output = this.replaceTriggerTableRefsTo(needUpdate, row);
        return output;
    }

    simpleWhere(row: string) {
        const simpleWhere = this.buildSimpleWhere();
        const output = this.replaceTriggerTableRefsTo(simpleWhere, row);
        return output;
    }

    simpleWhereOnUpdate(row: string) {
        const simpleWhere = this.buildSimpleWhere();
        const output = this.replaceTriggerTableRefsTo(simpleWhere, row);
        return output;
    }

    exitFromDeltaUpdateIf(): Expression | undefined {
        const conditions: (Expression | NotExpression)[] = this.context.referenceMeta.filters.map(filter =>
            new NotExpression(
                this.replaceTriggerTableRefsTo(filter, "new")!
            )
        );

        const hasNoReference = this.buildNoReference("new");
        if ( hasNoReference && !hasNoReference.isEmpty() ) {
            conditions.unshift( hasNoReference );
        }

        if ( !conditions.length ) {
            return;
        }

        return Expression.or(conditions);
    }

    private buildNoChanges(columns: ColumnReference[]) {
        const mutableColumns = columns.filter(column =>
            column.name !== "id"
        );

        const conditions: string[] = [];
        for (const columnRef of mutableColumns) {

            const tableStructure = this.context.database.getTable(
                columnRef.tableReference.table
            ) as Table;
        
            const column = (
                tableStructure &&
                tableStructure.getColumn(columnRef.name)
            ) as Column;

            const columnRefExpression = new Expression([ columnRef ]);
            let oldColumn = this.replaceTriggerTableRefsTo(
                columnRefExpression,
                "old"
            ) as Expression;
            let newColumn = this.replaceTriggerTableRefsTo(
                columnRefExpression,
                "new"
            ) as Expression;

            oldColumn = oldColumn.replaceTable(
                this.context.triggerTable,
                new TableReference(
                    this.context.triggerTable,
                    "old"
                )
            );
            newColumn = newColumn.replaceTable(
                this.context.triggerTable,
                new TableReference(
                    this.context.triggerTable,
                    "new"
                )
            );
    
            if ( column && column.type.isArray() ) {
                conditions.push(`cm_equal_arrays(${newColumn}, ${oldColumn})`);
            }
            else {
                conditions.push(`${ newColumn } is not distinct from ${ oldColumn }`);
            }
        }
        
        const noChangesCondition = Expression.and(conditions);
        return noChangesCondition;
    }

    private getMutableColumns() {
        const mutableColumns = this.context.triggerTableColumns
            .filter(col => col !== "id");
        return mutableColumns;
    }

    private buildSimpleWhere() {
        const conditions = this.context.referenceMeta.expressions.map(expression => {

            // TODO: recursive
            const orExpressions = expression.extrude().splitBy("or").map(subExpression => {
                subExpression = subExpression.extrude();

                subExpression = replaceOperatorAnyToIndexedOperator(
                    this.context.cache.for,
                    subExpression
                );
                subExpression = replaceAmpArrayToAny(
                    this.context.cache.for,
                    subExpression
                );
                subExpression = fixArraySearchForDifferentArrayTypesInCondition(
                    this.context.cache.for,
                    subExpression
                );

                return subExpression;
            });

            return Expression.or(orExpressions);
        });

        conditions.push(
            ...this.context.referenceMeta.unknownExpressions
        );

        conditions.push(
            ...this.context.referenceMeta.cacheTableFilters
        );

        const where = Expression.and(conditions);
        if ( !where.isEmpty() ) {
            return where;
        }
    }

    private buildNeedUpdateConditionOnUpdate() {
        const conditions = [
            buildHasReferenceCondition(this.context),
            Expression.and(this.context.referenceMeta.filters),
            this.matchedAllAggFilters()
        ].filter(condition => 
            condition != null &&
            !condition.isEmpty()
        ) as Expression[];
    
        const needUpdate = Expression.and(conditions);
        if ( !needUpdate.isEmpty() ) {
            return needUpdate;
        }
    }

    private buildHasReferenceWithoutJoins() {
        let conditions: Expression[] = [];

        const refCondition = buildHasReferenceCondition(this.context);
        if ( refCondition ) {
            conditions.push(refCondition);
        }

        for (const where of this.context.referenceMeta.filters) {
            if ( !this.hasJoinsInside(where) ) {
                conditions.push(
                    where
                );
            }
        }

        const aggFilters = this.matchedAllAggFilters();
        if ( aggFilters && !this.hasJoinsInside(aggFilters) ) {
            conditions.push(
                aggFilters
            );
        }

        conditions = conditions.filter(condition => 
            condition != null &&
            !condition.isEmpty()
        );
    
        const needUpdate = Expression.and(conditions);
        if ( !needUpdate.isEmpty() ) {
            return needUpdate;
        }
    }

    private matchedAllAggFilters() {
    
        const allAggCalls = flatMap(
            this.context.cache.select.columns, 
            column => column.getAggregations(this.context.database.aggregators)
        );
        const everyAggCallHaveFilter = allAggCalls.every(aggCall => aggCall.where != null);
        if ( !everyAggCallHaveFilter ) {
            return;
        }
    
        const filterConditions = allAggCalls.map(aggCall => {
            const expression = aggCall.where as Expression;
            return new CoalesceFalseExpression(expression);
        });
    
        return Expression.or(filterConditions);
    }

    replaceTriggerTableRefsTo(
        expression: AbstractExpressionElement | undefined,
        row: string
    ) {
        if ( !expression ) {
            return;
        }
        let outputExpression = expression as Expression;

        const refsToTriggerTable = this.context.getTableReferencesToTriggerTable();

        const joinsMeta = findJoinsMeta(this.context.cache.select);

        if ( joinsMeta.length ) {
            const joins = buildJoinVariables(
                this.context.database,
                joinsMeta,
                row
            );
            
            joins.forEach((join) => {
                outputExpression = outputExpression.replaceColumn(
                    join.table.column,
                    UnknownExpressionElement.fromSql(join.variable.name)
                );
            });
        }

        refsToTriggerTable.forEach((triggerTableRef) => {

            outputExpression = outputExpression.replaceTable(
                triggerTableRef,
                new TableReference(
                    this.context.triggerTable,
                    row
                )
            );
        });

        return outputExpression;
    }

    private hasJoinsInside(condition: Expression) {
        const joinsMeta = findJoinsMeta(this.context.cache.select);
        if ( !joinsMeta.length ) {
            return false;
        }

        const columnsRefs = condition.getColumnReferences();
        const hasJoins = columnsRefs.some(columnRef =>
            !columnRef.tableReference.table.equal(this.context.triggerTable)
        );
        return hasJoins;
    }

    private triggerTableColumnsToRefs(columnsNames: string[]) {
        const triggerTableRef = new TableReference( this.context.triggerTable );
        const triggerTableColumnsRefs = columnsNames.map(columnName =>
            new ColumnReference( triggerTableRef, columnName )
        );
        return triggerTableColumnsRefs;
    }
}
