import { IElement, IControllerArray, CONSTANTS, IBindingContext } from '../interface/exported';
import { View } from './view';
import { ViewController } from './viewController';
import { ContainerClasses } from '../container/exported';
import { BindingEngine, createBindingContext } from '../binding/exported';
import { Logger } from '../utils/exported';
import { DOM } from '../utils/exported';


/**
 * Element controller adds view and controls the lifecycle of element
 *
 */
export class ElementController {
    public classInstance: IElement;
    private templateString: string;
    private template: Node;
    private _process = true;
    private logger: Logger;
    private internalAttached = false;
    private elements: Node[];
    private anchorNode: Node;
    private viewController: ViewController;
    private shadowDom: boolean;

    /**
     * Creates instance of ElementController
     * @param bindingContext - Binding context to use
     * @param htmlNode - html node
     * @param classInstance - class instance, optional..
     * @param elementName - name of custom element
     * @param templateString - optional template string
     * @param parentViewController - parent viewController
     */
    constructor(
        private bindingContext: IBindingContext,
        private htmlNode: Node,
        classInstance: IElement | null,
        elementName: string,
        templateString: string | null,
        parentViewController: ViewController) {

        this.logger = Logger.getLogger((htmlNode as any).tagName, 'element');
        this.templateString = templateString;
        if (!classInstance) {
            this.logger.log('constructor');
        } else {
            this.logger.log('constructor - using supplied class');
        }
        this.classInstance = classInstance || ContainerClasses.get(elementName);

        // every element is a controller in it self, so create a new view
        this.viewController = new ViewController(htmlNode, parentViewController);
        this.viewController.addElement(this);


        if ((this.classInstance as any).__proto__[CONSTANTS.CONTAINER_FREE]) {
            this.anchorNode = DOM.document.createComment('containerless-anchor');
            this.elements = [];
        }

        if ((this.classInstance as any).__proto__[CONSTANTS.SHADOW_DOM]) {
            this.shadowDom = true;
        }
    }



    /**
     * Get class instance from parent
     * @param _customElement custom element class you want to try and search for
     */
    public searchForInstance<T>(_customElement: T): T | null {
        return this.viewController.searchForInstance(_customElement);
    }



    /**
     * get $view
     *
     */
    public getView(): ViewController {
        return this.viewController;
    }



    /**
     * start element life cycle
     *
     */
    public init() {


        this.classInstance.$element = this.htmlNode;
        this.classInstance.$bindingContext = this.bindingContext;
        this.classInstance.$attributes = (this.htmlNode as any).attributes;
        this.classInstance.$controller = this;

        if (this.shadowDom) {
            if ((this.classInstance as any).__proto__[CONSTANTS.CONTAINER_FREE]) {
                console.warn('containerfree can not be used with showdow dom');
            } else {
                const mode = (this.classInstance as any).__proto__[CONSTANTS.SHADOW_DOM].mode;
                try {
                    this.htmlNode = this.classInstance.$shadowdom;
                    this.classInstance.$shadowdom = (this.htmlNode as any).attachShadow({ mode: mode });
                } catch (e) {
                    console.warn('error adding showdow dom:', e);
                }
            }

        }

        BindingEngine.subscribeClassMetaBinding(this.classInstance);

        const result = this.loadTemplate();

        Promise.resolve(result).then((template: string) => {

            this.templateString = this.templateString || template || '<template></template>';
            let arr: IControllerArray = [];
            this.template = View.createTemplate(this.templateString);

            this.create();
            this.processContent();

            if (this._process) {
                arr = View.parseTemplate(this.template, createBindingContext(this.classInstance), this.viewController);
            }

            // task is started, lets look children and append them, if we have process content
            if (this._process) {
                while (this.template.childNodes.length) {
                    if (this.anchorNode) {
                        this.htmlNode.parentNode.insertBefore(this.anchorNode, this.htmlNode.nextSibling);

                        this.elements.push(this.template.firstChild);
                        this.anchorNode.parentNode.insertBefore(this.template.firstChild, this.anchorNode.nextSibling);


                    } else {
                        this.htmlNode.appendChild(this.template.firstChild);
                    }
                }
                if (this.anchorNode) {
                    this.htmlNode.parentNode.removeChild(this.htmlNode);
                }
            }
            arr.reverse();

            this.contentProcessed(arr);
            arr.forEach((a) => {
                if (a.attached) {
                    a.attached();
                }
            });
            this.internalAttached = true;
            this.attached();


        });


        return this;

    }



    /**
     * calls loadtemplate if stemplate string isnt set
     * @internal
     *
     */
    public loadTemplate(): string | Promise<string> {
        this.logger.log('loadTemplate');
        if (this.templateString) {
            return this.templateString;
        } else {
            return this.classInstance.loadTemplate && this.classInstance.loadTemplate();
        }
    }



    /**
     * sets vars and calls create on element
     * @internal
     *
     */
    public create(): void {
        this.logger.log('create');
        if (this.classInstance.created) {
            this.classInstance.created();
        }
    }



    /**
     * calls elements processContent() if it exist
     * here someone can modify the template
     * @internal
     *
     */
    public processContent(): void {
        this.logger.log('processContent');

        if (this.classInstance.processContent) {
            this._process = this.classInstance.processContent(this.template);
        } else {
            while (this.htmlNode.childNodes[0]) {
                this.template.appendChild(this.htmlNode.childNodes[0]);
            }
        }
    }



    /**
     * calls elements contentProcessed() if it exist
     * is called after template is processed and added to element
     * @internal
     *
     */
    public contentProcessed(controllers: IControllerArray): void {
        this.logger.log('contentProcessed');

        if (this.classInstance.contentProcessed && this._process) {
            this.classInstance.contentProcessed(controllers);
        }
    }



    /**
     * calls elements attached() if it exist
     * when attached to parent
     * @public
     *
     */
    public attached(): void {

        if (this.internalAttached) {
            if (!this.classInstance.$element.parentNode) {
                this.logger.log('attached', 'missing parent node');
            } else {
                this.logger.log('attached');
            }
        }

        if (this.classInstance.attached && this.internalAttached) {
            this.internalAttached = false;
            this.classInstance.attached();
        }
    }



    /**
     * calls elements detached() if it exist and clears it
     * when about to be detached
     * @public
     *
     */
    public detached(): void {
        this.logger.log('detached');

        if (this.classInstance.detached) {
            this.classInstance.detached();
        }

        BindingEngine.unSubscribeClassMetaBinding(this.classInstance);
        if (this.anchorNode) {
            this.elements.forEach((element: any) => {
                if (element.parentNode) { // if repeat or if.bind is here it might just be a dummy ref
                    element.parentNode.removeChild(element);
                }
            });
            if (this.anchorNode.parentNode) {
                this.anchorNode.parentNode.removeChild(this.anchorNode);
            }
        }
        this.anchorNode = null;
        this.elements = null;
        this.bindingContext = null;
        this.htmlNode = null;
        this.classInstance.$element = null;
        this.classInstance.$bindingContext = null;
        this.classInstance.$attributes = null;
        this.classInstance.$controller = null;
        this.classInstance = null;
        this.viewController = null;


    }

}
