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); }
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
(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", "../Atom", "../core/AtomBinder", "../core/AtomDisposableList", "../core/AtomWatcher", "../core/BindableProperty", "../di/Inject", "./baseTypes"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.Validate = exports.CachedWatch = exports.Watch = exports.BindableBroadcast = exports.BindableReceive = exports.Receive = exports.AtomViewModel = exports.waitForReady = void 0;
    const App_1 = require("../App");
    const Atom_1 = require("../Atom");
    const AtomBinder_1 = require("../core/AtomBinder");
    const AtomDisposableList_1 = require("../core/AtomDisposableList");
    const AtomWatcher_1 = require("../core/AtomWatcher");
    const BindableProperty_1 = require("../core/BindableProperty");
    const Inject_1 = require("../di/Inject");
    const baseTypes_1 = require("./baseTypes");
    function runDecoratorInits() {
        const v = this.constructor.prototype;
        if (!v) {
            return;
        }
        const ris = v._$_inits;
        if (ris) {
            for (const ri of ris) {
                ri.call(this, this);
            }
        }
    }
    function privateInit() {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                yield Atom_1.Atom.postAsync(this.app, () => __awaiter(this, void 0, void 0, function* () {
                    runDecoratorInits.apply(this);
                    // this.registerWatchers();
                }));
                yield Atom_1.Atom.postAsync(this.app, () => __awaiter(this, void 0, void 0, function* () {
                    yield this.init();
                    this.onReady();
                }));
                if (this.postInit) {
                    for (const i of this.postInit) {
                        i();
                    }
                    this.postInit = null;
                }
            }
            finally {
                const pi = this.pendingInits;
                this.pendingInits = null;
                for (const iterator of pi) {
                    iterator();
                }
            }
        });
    }
    /**
     * Useful only for Unit testing, this function will await till initialization is
     * complete and all pending functions are executed
     */
    function waitForReady(vm) {
        return __awaiter(this, void 0, void 0, function* () {
            while (vm.pendingInits) {
                yield Atom_1.Atom.delay(100);
            }
        });
    }
    exports.waitForReady = waitForReady;
    /**
     * ViewModel class supports initialization and supports {@link IDisposable} dispose pattern.
     * @export
     * @class AtomViewModel
     */
    let AtomViewModel = class AtomViewModel {
        constructor(app) {
            this.app = app;
            this.disposables = null;
            this.validations = [];
            this.pendingInits = [];
            this.mShouldValidate = false;
            this.app.runAsync(() => privateInit.apply(this));
        }
        /**
         * If it returns true, it means all pending initializations have finished
         */
        get isReady() {
            return this.pendingInits === null;
        }
        get errors() {
            const e = [];
            if (!this.mShouldValidate) {
                return e;
            }
            for (const v of this.validations) {
                if (!v.initialized) {
                    return e;
                }
                const error = this[v.name];
                if (error) {
                    e.push({ name: v.name, error });
                }
            }
            return e;
        }
        /**
         * Returns parent AtomViewModel if it was initialized with one. This property is also
         * useful when you open an popup or window. Whenever a popup/window is opened, ViewModel
         * associated with the UI element that opened this popup/window becomes parent of ViewModel
         * of popup/window.
         */
        get parent() {
            return this.mParent;
        }
        set parent(v) {
            if (this.mParent && this.mParent.mChildren) {
                this.mParent.mChildren.remove(this);
            }
            this.mParent = v;
            if (v) {
                const c = v.mChildren || (v.mChildren = []);
                c.add(this);
                this.registerDisposable({
                    dispose: () => {
                        c.remove(this);
                    }
                });
            }
        }
        /**
         * Returns true if all validations didn't return any error. All validations
         * are decorated with @{@link Validate} decorator.
         */
        get isValid() {
            let valid = true;
            const validateWasFalse = this.mShouldValidate === false;
            this.mShouldValidate = true;
            for (const v of this.validations) {
                if (!v.initialized) {
                    v.watcher.init(true);
                    v.initialized = true;
                }
                if (this[v.name]) {
                    if (validateWasFalse) {
                        AtomBinder_1.AtomBinder.refreshValue(this, v.name);
                    }
                    valid = false;
                }
            }
            if (this.mChildren) {
                for (const child of this.mChildren) {
                    if (!child.isValid) {
                        valid = false;
                    }
                }
            }
            AtomBinder_1.AtomBinder.refreshValue(this, "errors");
            return valid;
        }
        /**
         * Resets validations and all errors are removed.
         * @param resetChildren reset child view models as well. Default is true.
         */
        resetValidations(resetChildren = true) {
            this.mShouldValidate = false;
            for (const v of this.validations) {
                this.refresh(v.name);
            }
            if (resetChildren && this.mChildren) {
                for (const iterator of this.mChildren) {
                    iterator.resetValidations(resetChildren);
                }
            }
        }
        /**
         * Runs function after initialization is complete.
         * @param f function to execute
         */
        runAfterInit(f) {
            if (this.pendingInits) {
                this.pendingInits.push(f);
                return;
            }
            f();
        }
        // /**
        //  * Binds source property to target property with optional two ways
        //  * @param target target whose property will be set
        //  * @param propertyName name of target property
        //  * @param source source to read property from
        //  * @param path property path of source
        //  * @param twoWays optional, two ways {@link IValueConverter}
        //  */
        // public bind(
        //     target: any,
        //     propertyName: string,
        //     source: any,
        //     path: string[][],
        //     twoWays?: IValueConverter | ((v: any) => any) ): IDisposable {
        //     const pb = new PropertyBinding(
        //         target,
        //         null,
        //         propertyName,
        //         path,
        //         (twoWays && typeof twoWays !== "function") ? true : false , twoWays, source);
        //     return this.registerDisposable(pb);
        // }
        /**
         * Refreshes bindings associated with given property name
         * @param name name of property
         */
        refresh(name) {
            AtomBinder_1.AtomBinder.refreshValue(this, name);
        }
        /**
         * Put your asynchronous initialization here
         *
         * @returns {Promise<any>}
         * @memberof AtomViewModel
         */
        // tslint:disable-next-line:no-empty
        init() {
            return __awaiter(this, void 0, void 0, function* () {
            });
        }
        /**
         * dispose method will be called when attached view will be disposed or
         * when a new view model will be assigned to view, old view model will be disposed.
         *
         * @memberof AtomViewModel
         */
        dispose() {
            if (this.disposables) {
                this.disposables.dispose();
            }
        }
        // /**
        //  * Internal method, do not use, instead use errors.hasErrors()
        //  *
        //  * @memberof AtomViewModel
        //  */
        // public runValidation(): void {
        //     for (const v of this.validations) {
        //         v.watcher.evaluate(true);
        //     }
        // }
        /**
         * Register a disposable to be disposed when view model will be disposed.
         *
         * @protected
         * @param {IDisposable} d
         * @memberof AtomViewModel
         */
        registerDisposable(d) {
            this.disposables = this.disposables || new AtomDisposableList_1.AtomDisposableList();
            return this.disposables.add(d);
        }
        // tslint:disable-next-line:no-empty
        onReady() { }
        /**
         * Execute given expression whenever any bindable expression changes
         * in the expression.
         *
         * For correct generic type resolution, target must always be `this`.
         *
         *      this.setupWatch(() => {
         *          if(!this.data.fullName){
         *              this.data.fullName = `${this.data.firstName} ${this.data.lastName}`;
         *          }
         *      });
         *
         * @protected
         * @template T
         * @param {() => any} ft
         * @returns {IDisposable}
         * @memberof AtomViewModel
         */
        setupWatch(ft, proxy, forValidation, name) {
            const d = new AtomWatcher_1.AtomWatcher(this, ft, proxy, this);
            if (forValidation) {
                this.validations = this.validations || [];
                this.validations.push({ name, watcher: d, initialized: false });
            }
            else {
                d.init();
            }
            return this.registerDisposable(d);
        }
        // tslint:disable-next-line:no-empty
        onPropertyChanged(name) { }
    };
    AtomViewModel = __decorate([
        __param(0, Inject_1.Inject),
        __metadata("design:paramtypes", [App_1.App])
    ], AtomViewModel);
    exports.AtomViewModel = AtomViewModel;
    /**
     * Receive messages for given channel
     * @param {(string | RegExp)} channel
     * @returns {Function}
     */
    function Receive(...channel) {
        return (target, key) => {
            baseTypes_1.registerInit(target, (vm) => {
                // tslint:disable-next-line:ban-types
                const fx = vm[key];
                const a = (ch, d) => {
                    const p = fx.call(vm, ch, d);
                    if (p && p.then && p.catch) {
                        p.catch((e) => {
                            // tslint:disable-next-line: no-console
                            console.warn(e);
                        });
                    }
                };
                const ivm = vm;
                for (const c of channel) {
                    ivm.registerDisposable(ivm.app.subscribe(c, a));
                }
            });
        };
    }
    exports.Receive = Receive;
    function BindableReceive(...channel) {
        return (target, key) => {
            const bp = BindableProperty_1.BindableProperty(target, key);
            baseTypes_1.registerInit(target, (vm) => {
                const fx = (cx, m) => {
                    vm[key] = m;
                };
                const ivm = vm;
                for (const c of channel) {
                    ivm.registerDisposable(ivm.app.subscribe(c, fx));
                }
            });
            return bp;
        };
    }
    exports.BindableReceive = BindableReceive;
    function BindableBroadcast(...channel) {
        return (target, key) => {
            const bp = BindableProperty_1.BindableProperty(target, key);
            baseTypes_1.registerInit(target, (vm) => {
                const fx = (t) => {
                    const v = vm[key];
                    for (const c of channel) {
                        vm.app.broadcast(c, v);
                    }
                };
                const d = new AtomWatcher_1.AtomWatcher(vm, [key.split(".")], fx);
                d.init();
                vm.registerDisposable(d);
            });
            return bp;
        };
    }
    exports.BindableBroadcast = BindableBroadcast;
    function Watch(target, key, descriptor) {
        baseTypes_1.registerInit(target, (vm) => {
            const ivm = vm;
            if (descriptor && descriptor.get) {
                ivm.setupWatch(descriptor.get, () => {
                    vm.refresh(key.toString());
                });
                return;
            }
            ivm.setupWatch(vm[key]);
        });
    }
    exports.Watch = Watch;
    /**
     * Cached watch must be used with async getters to avoid reloading of
     * resources unless the properties referenced are changed
     * @param target ViewModel
     * @param key name of property
     * @param descriptor descriptor of property
     */
    function CachedWatch(target, key, descriptor) {
        const getMethod = descriptor.get;
        descriptor.get = (() => null);
        baseTypes_1.registerInit(target, (vm) => {
            const ivm = vm;
            const fieldName = `_${key}`;
            Object.defineProperty(ivm, key, {
                enumerable: true,
                configurable: true,
                get() {
                    const c = ivm[fieldName] || (ivm[fieldName] = {
                        value: getMethod.apply(ivm)
                    });
                    return c.value;
                }
            });
            ivm.setupWatch(getMethod, () => {
                ivm[fieldName] = null;
                AtomBinder_1.AtomBinder.refreshValue(ivm, key);
            });
        });
    }
    exports.CachedWatch = CachedWatch;
    function Validate(target, key, descriptor) {
        // tslint:disable-next-line:ban-types
        const getMethod = descriptor.get;
        // // trick is to change property descriptor...
        // delete target[key];
        descriptor.get = () => null;
        // // replace it with dummy descriptor...
        // Object.defineProperty(target, key, descriptor);
        baseTypes_1.registerInit(target, (vm) => {
            const initialized = { i: false };
            const ivm = vm;
            Object.defineProperty(ivm, key, {
                enumerable: true,
                configurable: true,
                get() {
                    if (vm.mShouldValidate && initialized.i) {
                        return getMethod.apply(this);
                    }
                    return null;
                }
            });
            ivm.setupWatch(getMethod, () => {
                // descriptor.get = getMethod;
                // Object.defineProperty(target, key, descriptor);
                initialized.i = true;
                vm.refresh(key.toString());
            }, true, key.toString());
            return;
        });
    }
    exports.Validate = Validate;
});
//# sourceMappingURL=AtomViewModel.js.map