import { ContainerValueConverters, ContainerClasses } from '../../container/exported';
import { getCorrectContext } from '../contextOfObject';
import { IBindingContext } from '../../interface/exported';
import { setValue } from '../setValue';



let fromView: any;

const Traverser = class {



    /**
     * operator calls
     *
     */
    public static operator = function (val: string, a: any, b: any) {
        switch (val) {
            case '+': return a + b;
            case '-': return b ? a - b : -a;
            case '*': return a * b;
            case '!': return !a;
            case '%': return a % b;
            case '>': return a > b;
            case '<': return a < b;
            case '>=': return a >= b;
            case '&&': return a && b;
            case '||': return a || b;
            case '<=': return a <= b;
            case '+=': return a += b;
            case '-=': return a -= b;
            case '*=': return a *= b;
            case '/=': return a /= b;
            case '===': return a === b;
            case '!==': return a !== b;
            case '/': return a / b;
            // tslint:disable-next-line:no-bitwise
            case '^': return a ^ b;
        }
    };



    /**
     * finds out what arity it is
     *
     */
    public static checkArity = function (ast: any, ctx: any, mainctx: any) {
        let value;
        if (ast && ast.arity === 'literal') {
            value = ast.value;
        }

        if (ast && ast.arity === 'variable') {
            value = Traverser.variable(ast, ctx, mainctx);
        }

        if (ast && ast.arity === 'binary') {
            value = Traverser.binary(ast, ctx, mainctx);
        }

        if (ast && ast.arity === 'unary') {
            value = Traverser.binary(ast, ctx, mainctx);
        }

        if (ast && ast.arity === 'ternary') {
            value = Traverser.ternary(ast, ctx, mainctx);
        }

        if (ast && ast.arity === 'valueConverter') {
            value = Traverser.valueConverter(ast, ctx, mainctx);
        }

        return value;
    };



    /**
     * called if arity is variable
     * todo: this one is kinda messy atm
     *
     */
    public static variable = function (ast: any, ctx: any, _mainctx: IBindingContext) {
        let result;
        try {


            switch (true) {
                case ast.value === 'null' && ast.arity === 'variable':
                    return null;

                case ast.value === 'undefined' && ast.arity === 'variable':
                    return undefined;

                case ast.value === '$this' && ast.arity === 'variable' && ast.root:
                    return ctx.$context;

                case ast.value === '$event' && ast.arity === 'variable' && ast.root:
                    return ctx.$event;

                case ast.arity === 'variable' && typeof ast.value === 'string' && ast.value.toUpperCase() === 'TRUE':
                    return true;

                case ast.arity === 'variable' && typeof ast.value === 'string' && ast.value.toUpperCase() === 'FALSE':
                    return false;

                default:
            }

            let curObj: any;
            let curCtx = ctx;
            if (ctx.__bindingContext) {
                curCtx = ctx.$context;
            }


            // check if it varibale starts with $
            let localVariable = false;
            if (ast.value && ast.value[0] === 'string' && ast.value[0][0] !== '$') {
                localVariable = true;
            }

            if (localVariable) {
                curObj = curCtx[ast.value];
            } else {

                if (ast.root) {
                    curCtx = getCorrectContext(ast.value, ctx);
                    if (curCtx.__bindingContext) {
                        curCtx = curCtx.$context;
                    }
                }

                curObj = curCtx[ast.value];
            }

            result = curObj;
        } catch (e) {
            // we dont want to display this just nothing if there is no var
            // console.warn('failed in variable');
            // nothing
        }

        return result;
    };



    /**
     * called if arity is binary
     *
     */
    public static binary = function (ast: any, ctx: any, mainctx: any) {
        let second: any;
        let value: any;
        let first: any;

        switch (true) {
            case ast.assignment === true:

                // maybe a bit weird, but works and is dynamic for simple expression assignments
                try {
                    if (ast.first.arity === 'variable') {
                        const $ctx = ctx.__bindingContext ? ctx.$context : ctx;
                        if (ast.value === '+=' || ast.value === '-=') {
                            $ctx[ast.first.value] =
                                Traverser.operator(
                                    ast.value,
                                    $ctx[ast.first.value],
                                    Traverser.checkArity(ast.second, ctx, mainctx)
                                );
                        } else {
                            $ctx[ast.first.value] = Traverser.checkArity(ast.second, ctx, mainctx);
                        }
                    } else {
                        // so it will return object
                        let done = false;
                        let astFirst = ast.first;
                        const x: any = [];
                        while (!done) {
                            if (astFirst.second.arity !== 'binary') {
                                x.push(astFirst.second.value);
                            } else {
                                x.push(`[${Traverser.checkArity(astFirst.second, ctx, mainctx)}]`);
                            }

                            if (astFirst.first.arity === 'variable') {
                                x.push(astFirst.first.value);
                                done = true;
                            } else {
                                astFirst = astFirst.first;
                            }
                        }
                        x.reverse();
                        const objectString = x.join('.').split('.[').join('['); // obj.something.cool[5]
                        if (ast.value === '=') {
                            setValue(ctx, objectString, Traverser.checkArity(ast.second, ctx, mainctx));
                        } else {

                            first = Traverser.checkArity(ast.first, ctx, mainctx);
                            setValue(ctx, objectString, Traverser.operator(
                                ast.value,
                                first,
                                Traverser.checkArity(ast.second, ctx, mainctx)
                            ));

                        }
                    }

                } catch (x) {
                    console.error('only simple assigments are supported, sorry', ast);
                }
                break;
            case ast.value === '{':

                const x = {};
                ast.first.forEach((y: any) => {
                    x[y.key] = Traverser.checkArity(y, ctx, mainctx);
                });
                value = x;
                break;


            case ast.checkArray:

                first = Traverser.checkArity(ast.first, ctx, mainctx);
                if (first && ast.contextReset) {
                    second = Traverser.checkArity(ast.second, mainctx, mainctx);
                    second = first[second];
                } else {
                    second = Traverser.checkArity(ast.second, first, mainctx);
                }

                value = second ? second : first;

                break;

            case ast.isObject:

                first = Traverser.checkArity(ast.first, ctx, mainctx);
                if (first && ast.contextReset) {
                    second = Traverser.checkArity(ast.second, mainctx, mainctx);
                    second = first[second];
                } else {
                    second = Traverser.checkArity(ast.second, first, mainctx);
                }

                value = second;

                break;
            case !ast.isFunction:

                first = Traverser.checkArity(ast.first, ctx, mainctx);
                second = Traverser.checkArity(ast.second, ctx, mainctx);
                value = Traverser.operator(ast.value, first, second);
                break;

            default:

                // function calls
                const results: any[] = [];
                ast.second.forEach(function (res: any) {
                    results.push(Traverser.evaluate(res, mainctx));
                });

                let curCtx = ctx.$context;
                let valueTrigger: any;
                if (ast.first.arity === 'binary') {
                    valueTrigger = Traverser.binary(ast.first, ctx, mainctx);
                } else {
                    valueTrigger = curCtx[ast.first.value];
                }


                curCtx = getCorrectContext(ast.first.value, ctx);
                if (curCtx && curCtx.$context && typeof curCtx.$context[ast.first.value] === 'function') {
                    valueTrigger = curCtx.$context[ast.first.value];
                }

                if (valueTrigger) {
                    const typeObj = Traverser.binary({ first: ast.first.first, checkArray: true }, ctx, mainctx);
                    if (Array.isArray(typeObj)) {
                        value = valueTrigger.apply(typeObj, results);
                    } else {
                        value = valueTrigger.apply(curCtx.$context, results);
                    }


                } else {
                    console.warn(`method does not exist:${ast.first.value}`);
                }

        }

        return value;

    };



    /**
     * ternary
     * called if arity is ternary (conditional)
     * {condition ? expr1 : expr2}'
     */
    public static ternary = function (ast: any, ctx: any, mainctx: any) {

        const first = Traverser.checkArity(ast.first, ctx, mainctx);
        const second = Traverser.checkArity(ast.second, ctx, mainctx);
        const third = Traverser.checkArity(ast.third, ctx, mainctx);

        const value: any = first ? second : third;

        return value;
    };



    /**
     * called if arity is value converter
     *
     */
    public static valueConverter = function (ast: any, ctx: any, mainctx: any) {
        let result;

        const args: any[] = [];
        // loop args
        ast.args.forEach((arg: any) => {
            args.push(Traverser.checkArity(arg, ctx, mainctx));
        });

        // find value converter
        const valueConverterExist = ContainerValueConverters.findConverter(ast.value);
        let _class: any;

        // if it exist, then we execute it
        if (valueConverterExist) {
            _class = ContainerClasses.get(valueConverterExist);
            try {
                result = fromView ? _class.fromView.apply(ctx, args) : _class.toView.apply(ctx, args);
            } catch (e) {
                console.warn('value converter failed:' + ast.value);
            }
        } else {
            console.warn('value converter does not exist:' + ast.value);
        }

        return result;
    };



    /**
     * called to start evaluating ast (getting result)
     *
     */
    public static evaluate = function (astInput: any, ctx: any) {

        fromView = false;
        const astArray = Array.isArray(astInput) ? astInput : [astInput];
        const returnArray: any[] = [];
        for (let i = 0; i < astArray.length; i++) {
            const result = Traverser.checkArity(astArray[i], ctx, ctx);
            returnArray.push(result);
        }

        if (returnArray.length > 1) {
            return returnArray.join('');
        } else {
            return returnArray[0];
        }

    };



    /**
     * handle value converter fromView only
     *
     */
    public static traverseOnlyValueConverter = function (value: any, ast: any, ctx: any, mainctx: any) {
        let result;

        let args: any[] = [];
        // loop args
        ast.args.forEach((arg: any, i: number) => {
            if (i > 0) {
                args.push(Traverser.checkArity(arg, ctx, mainctx));
            }

        });

        args = [value, ...args];

        // find value converter
        const valueConverterExist = ContainerValueConverters.findConverter(ast.value);
        let _class: any;

        // if it exist, then we execute it
        if (valueConverterExist) {
            _class = ContainerClasses.get(valueConverterExist);
            try {
                result = fromView ? _class.fromView.apply(ctx, args) : _class.toView.apply(ctx, args);
            } catch (e) {
                console.warn('value converter failed:' + ast.value);
            }
        } else {
            console.warn('value converter does not exist:' + ast.value);
        }

        return result;
    };



    /**
     * evaluate ast
     *
     */
    public static evaluateConverterFromViewOnly = function (astInput: any, value: any, ctx: any, mainctx: any) {

        // if more then its
        const astArray = Array.isArray(astInput) ? astInput : [astInput];
        const returnArray: any[] = [];
        fromView = true;
        let valueConverted = false;

        for (let i = 0; i < astArray.length; i++) {

            let result;
            switch (true) {
                case astArray[i].arity === 'valueConverter':
                    // resolve args
                    valueConverted = true;
                    result = Traverser.traverseOnlyValueConverter(value, astArray[i], ctx, mainctx);
                    break;
            }
            if (result) {
                returnArray.push(result);
            }

        }

        if (returnArray.length > 1) {
            return returnArray.join('');
        } else {
            if (valueConverted) {
                return returnArray[0];
            } else {
                return value;
            }
        }

    };



    /**
     * gets the behaviors from ast
     *
     */
    public static getBehaviorFunctions = function (ast: any) {
        const astArray = Array.isArray(ast) ? ast : [ast];
        const returnArray: any[] = [];

        for (let i = 0; i < astArray.length; i++) {

            let result;
            switch (true) {
                case astArray[i].arity === 'behavior':
                    result = {
                        name: astArray[i].value,
                        args: astArray[i].args.map((a: any) => a.value)
                    };
                    // this will be used outside this scope for now
                    break;
            }
            if (result) {
                returnArray.push(result);
            }


        }

        return returnArray;

    };

};


// default for getting result
export const traverseAST = function (ast: any, ctx: IBindingContext) {
    return Traverser.evaluate(ast, ctx);
};

// used for getting valueConverter
export const valueConvert = function (ast: any, ctx: IBindingContext, value: any) {
    return Traverser.evaluateConverterFromViewOnly(ast, value, ctx, ctx);
};

// return behavior
export const getBehavior = function (ast: any) {
    return Traverser.getBehaviorFunctions(ast);
};
