import ngModule from '../module';
import EventEmitter from './event-emitter';
import {kebabToCamel} from './string';

/**
 * Base class for component controllers.
 */
export default class Component extends EventEmitter {
    /**
     * Contructor.
     *
     * @param {HTMLElement} $element The main component element
     * @param {$rootScope.Scope} $scope The element scope
     * @param {Function} $transclude The transclusion function
     */
    constructor($element, $scope, $transclude) {
        super();
        this.$ = $scope;

        if (!$element) return;
        this.element = $element[0];
        this.element.$ctrl = this;
        this.$element = $element;
        this.$transclude = $transclude;
        this.classList = this.element.classList;

        const constructor = this.constructor;
        const $options = constructor.$options;

        if ($options && $options.installClasses)
            this.classList.add(...this.constructor.$classNames);

        if ($transclude && constructor.slotTemplates) {
            for (let slotTemplate of constructor.slotTemplates)
                this.fillSlots(slotTemplate);
        }
    }

    $postLink() {
        if (!this.$element) return;
        let attrs = this.$element[0].attributes;
        let $scope = this.$;
        for (let attr of attrs) {
            if (!attr.name.startsWith('on-')) continue;
            let eventName = kebabToCamel(attr.name.substr(3));
            let callback = locals => $scope.$parent.$eval(attr.nodeValue, locals);
            this.on(eventName, callback);
        }
    }

    /**
     * The component owner window.
     */
    get window() {
        return this.document.defaultView;
    }

    /**
     * The component owner document.
     */
    get document() {
        return this.element.ownerDocument;
    }

    /**
     * Translates an string.
     *
     * @param {String} string String to translate
     * @param {Array} params Translate parameters
     * @return {String} The translated string
     */
    $t(string, params) {
        return this.$translate.instant(string, params);
    }

    /**
     * Fills the default transclude slot.
     *
     * @param {JQElement|String} template The slot template
     */
    fillDefaultSlot(template) {
        const linkFn = this.$compile(template);
        this.$transclude.$$boundTransclude = this.createBoundTranscludeFn(linkFn);
    }

    /**
     * Fills a named transclude slot.
     *
     * @param {String} slot The trasnclude slot name
     * @param {JQElement|String} template The slot template
     */
    fillSlot(slot, template) {
        const linkFn = this.$compile(template);
        const slots = this.$transclude.$$boundTransclude.$$slots;
        slots[slot] = this.createBoundTranscludeFn(linkFn);
    }

    /**
     * Fills component transclude slots using the passed HTML template string
     * as source.
     *
     * @param {String} template The HTML template string
     */
    fillSlots(template) {
        const name = this.constructor.$options.name;
        const transclude = this.constructor.$options.transclude;

        if (!transclude)
            throw new Error(`No transclusion option defined in '${name}'`);
        if (!this.$transclude)
            throw new Error(`No $transclude injected in '${name}'`);

        let slotMap = {};
        for (let slotName in transclude) {
            let slotTag = transclude[slotName].match(/\w+$/)[0];
            slotMap[slotTag] = slotName;
        }

        const $template = angular.element(template);
        for (let i = 0; i < $template.length; i++) {
            let slotElement = $template[i];
            if (slotElement.nodeType != Node.ELEMENT_NODE) continue;
            let tagName = kebabToCamel(slotElement.tagName.toLowerCase());

            if (tagName == 'default')
                this.fillDefaultSlot(slotElement.childNodes);
            else {
                let slotName = slotMap[tagName];
                if (!slotName)
                    throw new Error(`No slot found for '${tagName}' in '${name}'`);
                this.fillSlot(slotName, slotElement);
            }
        }
    }

    /**
     * Creates a bounded transclude function from a linking function.
     *
     * @param {Function} linkFn The linking function
     * @return {Function} The bounded transclude function
     */
    createBoundTranscludeFn(linkFn) {
        let scope = this.$;
        let previousBoundTranscludeFn = this.$transclude.$$boundTransclude;

        function vnBoundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
            if (!transcludedScope) {
                transcludedScope = scope.$new(false, containingScope);
                transcludedScope.$$transcluded = true;
            }
            return linkFn(transcludedScope, cloneFn, {
                parentBoundTranscludeFn: previousBoundTranscludeFn,
                transcludeControllers: controllers,
                futureParentElement: futureParentElement
            });
        }
        vnBoundTranscludeFn.$$slots = previousBoundTranscludeFn.$$slots;

        return vnBoundTranscludeFn;
    }

    copySlot(slot, $transclude) {
        this.$transclude.$$boundTransclude.$$slots[slot] =
            $transclude.$$boundTransclude.$$slots[slot];
    }
}
Component.$inject = ['$element', '$scope'];

/*
 * Automatically adds the most used services to the prototype, so they are
 * available as component properties.
 */
function runFn(...args) {
    const proto = Component.prototype;

    for (let i = 0; i < runFn.$inject.length; i++)
        proto[runFn.$inject[i]] = args[i];

    Object.assign(proto, {
        $params: proto.$stateParams
    });
}
runFn.$inject = [
    '$translate',
    '$q',
    '$http',
    '$httpParamSerializer',
    '$state',
    '$stateParams',
    '$timeout',
    '$transitions',
    '$compile',
    '$filter',
    '$interpolate',
    '$window',
    'vnApp',
    'vnToken',
    'vnConfig',
    'vnModules',
    'aclService'
];

ngModule.run(runFn);