var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "../App", "../core/AtomBridge", "../core/types", "../di/Inject", "./AtomDisposableList", "./AtomOnce", "./AtomWatcher", "./Bind", "./InheritedProperty", "./PropertyMap", "./XNode"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.PropertyBinding = exports.AtomComponent = void 0;
    const App_1 = require("../App");
    const AtomBridge_1 = require("../core/AtomBridge");
    // tslint:disable-next-line:import-spacing
    const types_1 = require("../core/types");
    const Inject_1 = require("../di/Inject");
    const AtomDisposableList_1 = require("./AtomDisposableList");
    const AtomOnce_1 = require("./AtomOnce");
    const AtomWatcher_1 = require("./AtomWatcher");
    const Bind_1 = require("./Bind");
    const InheritedProperty_1 = require("./InheritedProperty");
    const PropertyMap_1 = require("./PropertyMap");
    const XNode_1 = require("./XNode");
    const objectHasOwnProperty = Object.prototype.hasOwnProperty;
    const localBindSymbol = Bind_1.bindSymbol;
    const localXNodeSymbol = XNode_1.xnodeSymbol;
    const localBridge = AtomBridge_1.AtomBridge.instance;
    let AtomComponent = class AtomComponent {
        constructor(app, element = null) {
            this.app = app;
            this.mInvalidated = 0;
            this.mPendingPromises = {};
            this.disposables = new AtomDisposableList_1.AtomDisposableList();
            this.bindings = [];
            this.eventHandlers = [];
            this.element = element;
            AtomBridge_1.AtomBridge.instance.attachControl(this.element, this);
            const a = this.beginEdit();
            this.preCreate();
            this.create();
            app.callLater(() => a.dispose());
        }
        /** Do not ever use, only available as intellisense feature for
         * vs code editor.
         */
        get vsProps() {
            return undefined;
        }
        bind(element, name, path, twoWays, valueFunc, source) {
            // remove existing binding if any
            // let binding = this.bindings.find( (x) => x.name === name && (element ? x.element === element : true));
            // if (binding) {
            //     binding.dispose();
            //     ArrayHelper.remove(this.bindings, (x) => x === binding);
            // }
            const binding = new PropertyBinding(this, element, name, path, twoWays, valueFunc, source);
            this.bindings.push(binding);
            return {
                dispose: () => {
                    binding.dispose();
                    types_1.ArrayHelper.remove(this.bindings, (x) => x === binding);
                }
            };
        }
        /**
         * Remove all bindings associated with given element and optional name
         * @param element T
         * @param name string
         */
        unbind(element, name) {
            const toDelete = this.bindings.filter((x) => x.element === element && (!name || (x.name === name)));
            for (const iterator of toDelete) {
                iterator.dispose();
                types_1.ArrayHelper.remove(this.bindings, (x) => x === iterator);
            }
        }
        bindEvent(element, name, method, key) {
            if (!element) {
                return;
            }
            if (!method) {
                return;
            }
            const be = {
                element,
                name,
                handler: method
            };
            if (key) {
                be.key = key;
            }
            be.disposable = AtomBridge_1.AtomBridge.instance.addEventHandler(element, name, method, false);
            this.eventHandlers.push(be);
            return {
                dispose: () => {
                    be.disposable.dispose();
                    types_1.ArrayHelper.remove(this.eventHandlers, (e) => e.disposable === be.disposable);
                }
            };
        }
        unbindEvent(element, name, method, key) {
            const deleted = [];
            for (const be of this.eventHandlers) {
                if (element && be.element !== element) {
                    return;
                }
                if (key && be.key !== key) {
                    return;
                }
                if (name && be.name !== name) {
                    return;
                }
                if (method && be.handler !== method) {
                    return;
                }
                be.disposable.dispose();
                be.handler = null;
                be.element = null;
                be.name = null;
                be.key = null;
                deleted.push(() => this.eventHandlers.remove(be));
            }
            for (const iterator of deleted) {
                iterator();
            }
        }
        /**
         * Control checks if property is declared on the control or not.
         * Since TypeScript no longer creates enumerable properties, we have
         * to inspect name and PropertyMap which is generated by `@BindableProperty`
         * or the value is not set to undefined.
         * @param name name of Property
         */
        hasProperty(name) {
            if (/^(data|viewModel|localViewModel|element)$/.test(name)) {
                return true;
            }
            const map = PropertyMap_1.PropertyMap.from(this);
            if (map.map[name]) {
                return true;
            }
            if (this[name] !== undefined) {
                return true;
            }
            return false;
        }
        /**
         * Use this method if you want to set attribute on HTMLElement immediately but
         * defer atom control property
         * @param element HTMLElement
         * @param name string
         * @param value any
         */
        setPrimitiveValue(element, name, value) {
            const p = value;
            if (p && p.then && p.catch) {
                this.mPendingPromises[name] = p;
                p.then((r) => {
                    if (this.mPendingPromises[name] !== p) {
                        return;
                    }
                    this.mPendingPromises[name] = null;
                    this.setPrimitiveValue(element, name, r);
                }).catch((e) => {
                    if (this.mPendingPromises[name] !== p) {
                        return;
                    }
                    this.mPendingPromises[name] = null;
                    // tslint:disable-next-line:no-console
                    console.error(e);
                });
                return;
            }
            if (/^(viewModel|localViewModel)$/.test(name)) {
                this[name] = value;
                return;
            }
            if ((!element || element === this.element) && this.hasProperty(name)) {
                this.runAfterInit(() => {
                    this[name] = value;
                });
            }
            else {
                this.setElementValue(element, name, value);
            }
        }
        setLocalValue(element, name, value) {
            // if value is a promise
            const p = value;
            if (p && p.then && p.catch) {
                this.mPendingPromises[name] = p;
                p.then((r) => {
                    if (this.mPendingPromises[name] !== p) {
                        return;
                    }
                    this.mPendingPromises[name] = null;
                    this.setLocalValue(element, name, r);
                }).catch((e) => {
                    if (this.mPendingPromises[name] !== p) {
                        return;
                    }
                    this.mPendingPromises[name] = null;
                    // tslint:disable-next-line:no-console
                    console.error(e);
                });
                return;
            }
            if ((!element || element === this.element) && this.hasProperty(name)) {
                this[name] = value;
            }
            else {
                this.setElementValue(element, name, value);
            }
        }
        dispose(e) {
            if (this.mInvalidated) {
                clearTimeout(this.mInvalidated);
                this.mInvalidated = 0;
            }
            AtomBridge_1.AtomBridge.instance.visitDescendents(e || this.element, (ex, ac) => {
                if (ac) {
                    ac.dispose();
                    return false;
                }
                return true;
            });
            if (!e) {
                this.unbindEvent(null, null, null);
                for (const binding of this.bindings) {
                    binding.dispose();
                }
                this.bindings.length = 0;
                this.bindings = null;
                AtomBridge_1.AtomBridge.instance.dispose(this.element);
                this.element = null;
                const lvm = this.localViewModel;
                if (lvm && lvm.dispose) {
                    lvm.dispose();
                    this.localViewModel = null;
                }
                const vm = this.viewModel;
                if (vm && vm.dispose) {
                    vm.dispose();
                    this.viewModel = null;
                }
                this.disposables.dispose();
                this.pendingInits = null;
            }
        }
        // tslint:disable-next-line:no-empty
        onPropertyChanged(name) {
        }
        beginEdit() {
            this.pendingInits = [];
            const a = this.pendingInits;
            return {
                dispose: () => {
                    if (this.pendingInits == null) {
                        // case where current control is disposed...
                        return;
                    }
                    this.pendingInits = null;
                    if (a) {
                        for (const iterator of a) {
                            iterator();
                        }
                    }
                    this.invalidate();
                }
            };
        }
        invalidate() {
            if (this.mInvalidated) {
                clearTimeout(this.mInvalidated);
            }
            this.mInvalidated = setTimeout(() => {
                this.mInvalidated = 0;
                this.app.callLater(() => {
                    this.onUpdateUI();
                });
            }, 5);
        }
        onUpdateUI() {
            // for implementors..
        }
        runAfterInit(f) {
            if (this.pendingInits) {
                this.pendingInits.push(f);
            }
            else {
                f();
            }
        }
        registerDisposable(d) {
            return this.disposables.add(d);
        }
        render(node, e, creator) {
            creator = creator || this;
            const app = this.app;
            const renderFirst = AtomBridge_1.AtomBridge.platform === "xf";
            e = e || this.element;
            const attr = node.attributes;
            if (attr) {
                for (const key in attr) {
                    if (attr.hasOwnProperty(key)) {
                        const item = attr[key];
                        const isBind = objectHasOwnProperty.call(item, localBindSymbol);
                        if (isBind) {
                            isBind(key, this, e, creator);
                        }
                        else if (objectHasOwnProperty.call(item, localXNodeSymbol)) {
                            // this is template..
                            if (item.isTemplate) {
                                this.setLocalValue(e, key, AtomBridge_1.AtomBridge.toTemplate(app, item, creator));
                            }
                            else {
                                const child = AtomBridge_1.AtomBridge.createNode(item, app);
                                this.setLocalValue(e, key, child.element);
                            }
                        }
                        else {
                            this.setLocalValue(e, key, item);
                        }
                    }
                }
            }
            for (const iterator of node.children) {
                if (typeof iterator === "string") {
                    e.appendChild(document.createTextNode(iterator));
                    continue;
                }
                if (iterator.isTemplate) {
                    if (iterator.isProperty) {
                        this.setLocalValue(e, iterator.name, AtomBridge_1.AtomBridge.toTemplate(app, iterator.children[0], creator));
                    }
                    else {
                        e.appendChild(AtomBridge_1.AtomBridge.toTemplate(app, iterator, creator));
                    }
                    continue;
                }
                if (iterator.isProperty) {
                    for (const child of iterator.children) {
                        const pc = AtomBridge_1.AtomBridge.createNode(child, app);
                        (pc.control || this).render(child, pc.element, creator);
                        // in Xamarin.Forms certain properties are required to be
                        // set in advance, so we append the element after setting
                        // all children properties
                        localBridge.append(e, iterator.name, pc.element);
                    }
                    continue;
                }
                const t = iterator.attributes && iterator.attributes.template;
                if (t) {
                    this.setLocalValue(e, t, AtomBridge_1.AtomBridge.toTemplate(app, iterator, creator));
                    continue;
                }
                const c = AtomBridge_1.AtomBridge.createNode(iterator, app);
                if (renderFirst) {
                    (c.control || this).render(iterator, c.element, creator);
                }
                if (this.element === e) {
                    this.append(c.control || c.element);
                }
                else {
                    e.appendChild(c.element);
                }
                if (!renderFirst) {
                    (c.control || this).render(iterator, c.element, creator);
                }
            }
        }
        // tslint:disable-next-line:no-empty
        create() {
        }
        // tslint:disable-next-line:no-empty
        preCreate() {
        }
        setElementValue(element, name, value) {
            AtomBridge_1.AtomBridge.instance.setValue(element, name, value);
        }
        resolve(c, selfName) {
            const result = this.app.resolve(c, true);
            if (selfName) {
                if (typeof selfName === "function") {
                    // this is required as parent is not available
                    // in items control so binding becomes difficult
                    this.runAfterInit(() => {
                        const v = selfName();
                        if (v) {
                            for (const key in v) {
                                if (v.hasOwnProperty(key)) {
                                    const element = v[key];
                                    result[key] = element;
                                }
                            }
                        }
                    });
                }
                else {
                    result[selfName] = this;
                }
            }
            return result;
        }
    };
    AtomComponent.isControl = true;
    __decorate([
        InheritedProperty_1.InheritedProperty,
        __metadata("design:type", Object)
    ], AtomComponent.prototype, "data", void 0);
    __decorate([
        InheritedProperty_1.InheritedProperty,
        __metadata("design:type", Object)
    ], AtomComponent.prototype, "viewModel", void 0);
    __decorate([
        InheritedProperty_1.InheritedProperty,
        __metadata("design:type", Object)
    ], AtomComponent.prototype, "localViewModel", void 0);
    AtomComponent = __decorate([
        __param(0, Inject_1.Inject),
        __metadata("design:paramtypes", [App_1.App, Object])
    ], AtomComponent);
    exports.AtomComponent = AtomComponent;
    class PropertyBinding {
        constructor(target, element, name, path, twoWays, valueFunc, source) {
            this.target = target;
            this.element = element;
            this.name = name;
            this.twoWays = twoWays;
            this.source = source;
            this.isTwoWaySetup = false;
            this.name = name;
            this.twoWays = twoWays;
            this.target = target;
            this.element = element;
            this.updaterOnce = new AtomOnce_1.AtomOnce();
            if (valueFunc) {
                if (typeof valueFunc !== "function") {
                    this.fromSourceToTarget = valueFunc.fromSource;
                    this.fromTargetToSource = valueFunc.fromTarget;
                }
                else {
                    this.fromSourceToTarget = valueFunc;
                }
            }
            this.watcher = new AtomWatcher_1.AtomWatcher(target, path, (...v) => {
                this.updaterOnce.run(() => {
                    if (this.disposed) {
                        return;
                    }
                    // set value
                    for (const iterator of v) {
                        if (iterator === undefined) {
                            return;
                        }
                    }
                    const cv = this.fromSourceToTarget ? this.fromSourceToTarget.apply(this, v) : v[0];
                    if (this.target instanceof AtomComponent) {
                        this.target.setLocalValue(this.element, this.name, cv);
                    }
                    else {
                        this.target[name] = cv;
                    }
                });
            }, source);
            this.path = this.watcher.path;
            if (this.target instanceof AtomComponent) {
                this.target.runAfterInit(() => {
                    if (!this.watcher) {
                        // this is disposed ...
                        return;
                    }
                    this.watcher.init(true);
                    if (twoWays) {
                        this.setupTwoWayBinding();
                    }
                });
            }
            else {
                this.watcher.init(true);
                if (twoWays) {
                    this.setupTwoWayBinding();
                }
            }
        }
        setupTwoWayBinding() {
            if (this.target instanceof AtomComponent) {
                if (this.element
                    && (this.element !== this.target.element || !this.target.hasProperty(this.name))) {
                    // most likely it has change event..
                    let events = [];
                    if (typeof this.twoWays !== "boolean") {
                        events = this.twoWays;
                    }
                    this.twoWaysDisposable = AtomBridge_1.AtomBridge.instance.watchProperty(this.element, this.name, events, (v) => {
                        this.setInverseValue(v);
                    });
                    return;
                }
            }
            const watcher = new AtomWatcher_1.AtomWatcher(this.target, [[this.name]], (...values) => {
                if (this.isTwoWaySetup) {
                    this.setInverseValue(values[0]);
                }
            });
            watcher.init(true);
            this.isTwoWaySetup = true;
            this.twoWaysDisposable = watcher;
        }
        setInverseValue(value) {
            if (!this.twoWays) {
                throw new Error("This Binding is not two ways.");
            }
            this.updaterOnce.run(() => {
                if (this.disposed) {
                    return;
                }
                const first = this.path[0];
                const length = first.length;
                let v = this.target;
                let i = 0;
                let name;
                for (i = 0; i < length - 1; i++) {
                    name = first[i].name;
                    if (name === "this") {
                        v = this.source || this.target;
                    }
                    else {
                        v = v[name];
                    }
                    if (!v) {
                        return;
                    }
                }
                name = first[i].name;
                v[name] = this.fromTargetToSource ? this.fromTargetToSource.call(this, value) : value;
            });
        }
        dispose() {
            if (this.twoWaysDisposable) {
                this.twoWaysDisposable.dispose();
                this.twoWaysDisposable = null;
            }
            this.watcher.dispose();
            this.disposed = true;
            this.watcher = null;
        }
    }
    exports.PropertyBinding = PropertyBinding;
});
//# sourceMappingURL=AtomComponent.js.map