import { Disposer } from "./Disposer";
import { EventDispatcher } from "./EventDispatcher";
import { AnimationState, getInterpolate } from "./Animation";
import { States } from "./States";
import { registry } from "../Registry";
import * as $object from "./Object";
import * as $ease from "./Ease";
import * as $array from "./Array";
import * as $order from "./Order";
/**
 * Allows to dynamically modify setting value of its target element.
 *
 * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/adapters/} for more info
 */
export class Adapters {
    constructor(entity) {
        this._callbacks = {};
        this._disabled = {};
        this._entity = entity;
    }
    /**
     * Add a function (`callback`) that will modify value for setting `key`.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/adapters/} for more info
     */
    add(key, callback) {
        let callbacks = this._callbacks[key];
        if (callbacks === undefined) {
            callbacks = this._callbacks[key] = [];
        }
        callbacks.push(callback);
        this._entity._markDirtyKey(key);
        return new Disposer(() => {
            if ($array.removeFirst(callbacks, callback)) {
                this._entity._markDirtyKey(key);
            }
        });
    }
    /**
     * Removes all adapters for the specific key.
     *
     * @since 5.1.0
     */
    remove(key) {
        const callbacks = this._callbacks[key];
        if (callbacks !== undefined) {
            delete this._callbacks[key];
            if (callbacks.length !== 0) {
                this._entity._markDirtyKey(key);
            }
        }
    }
    /**
     * Enables (previously disabled) adapters for specific key.
     *
     * @since 5.1.0
     */
    enable(key) {
        if (this._disabled[key]) {
            delete this._disabled[key];
            this._entity._markDirtyKey(key);
        }
    }
    /**
     * Disables all adapters for specific key.
     *
     * @since 5.1.0
     */
    disable(key) {
        if (!this._disabled[key]) {
            this._disabled[key] = true;
            this._entity._markDirtyKey(key);
        }
    }
    /**
     * @ignore
     */
    fold(key, value) {
        if (!this._disabled[key]) {
            const callbacks = this._callbacks[key];
            if (callbacks !== undefined) {
                for (let i = 0, len = callbacks.length; i < len; ++i) {
                    value = callbacks[i](value, this._entity, key);
                }
            }
        }
        return value;
    }
}
/**
 * Animation object.
 *
 * @see {@link https://www.amcharts.com/docs/v5/concepts/animations/} for more info
 */
export class Animation {
    constructor(animation, from, to, duration, easing, loops, startingTime) {
        this._time = 0;
        this._stopped = false;
        this._playing = true;
        this.events = new EventDispatcher();
        this._animation = animation;
        this._from = from;
        this._to = to;
        this._duration = duration;
        this._easing = easing;
        this._loops = loops;
        this._interpolate = getInterpolate(from, to);
        this._oldTime = startingTime;
    }
    get to() {
        return this._to;
    }
    get from() {
        return this._from;
    }
    get playing() {
        return this._playing;
    }
    get stopped() {
        return this._stopped;
    }
    stop() {
        if (!this._stopped) {
            this._stopped = true;
            this._playing = false;
            if (this.events.isEnabled("stopped")) {
                this.events.dispatch("stopped", {
                    type: "stopped",
                    target: this,
                });
            }
        }
    }
    pause() {
        this._playing = false;
        this._oldTime = null;
    }
    play() {
        if (!this._stopped && !this._playing) {
            this._playing = true;
            this._animation._startAnimation();
        }
    }
    get percentage() {
        return this._time / this._duration;
    }
    waitForStop() {
        return new Promise((resolve, _reject) => {
            if (this._stopped) {
                resolve();
            }
            else {
                const listener = () => {
                    stopped.dispose();
                    resolve();
                };
                const stopped = this.events.on("stopped", listener);
            }
        });
    }
    _checkEnded() {
        if (this._loops > 1) {
            --this._loops;
            return false;
        }
        else {
            return true;
        }
    }
    _run(currentTime) {
        if (this._oldTime !== null) {
            this._time += currentTime - this._oldTime;
            if (this._time > this._duration) {
                this._time = this._duration;
            }
        }
        this._oldTime = currentTime;
    }
    _reset(currentTime) {
        this._oldTime = currentTime;
        this._time = 0;
    }
    _value(diff) {
        if (diff >= 1) {
            return this._to;
        }
        if (diff <= 0) {
            return this._from;
        }
        return this._interpolate(this._easing(diff), this._from, this._to);
    }
}
/**
 * @ignore
 */
let counter = 0;
/**
 * Base class for [[Entity]] objects that support Settings.
 *
 * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
 */
export class Settings {
    constructor(settings) {
        /**
         * Unique ID.
         */
        this.uid = ++counter;
        this._privateSettings = {};
        this._settingEvents = {};
        this._privateSettingEvents = {};
        this._debouncedSettingEvents = {};
        this._debouncedPrivateSettingEvents = {};
        this._prevSettings = {};
        this._prevPrivateSettings = {};
        this._animatingSettings = {};
        this._animatingPrivateSettings = {};
        this._disposed = false;
        // TODO move this into Entity
        this._userProperties = {};
        /**
         * If this is set to `false` then disposing does nothing, it's a no-op.
         */
        this.enableDispose = true;
        this._settings = settings;
    }
    _checkDirty() {
        $object.keys(this._settings).forEach((key) => {
            this._userProperties[key] = true;
            this._markDirtyKey(key);
        });
    }
    /**
     * @ignore
     */
    resetUserSettings() {
        this._userProperties = {};
    }
    _runAnimation(currentTime) {
        let state = AnimationState.Stopped;
        if (!this.isDisposed()) {
            let playing = false;
            let paused = false;
            $object.each(this._animatingSettings, (key, animation) => {
                if (animation.stopped) {
                    this._stopAnimation(key);
                }
                else if (animation.playing) {
                    animation._run(currentTime);
                    const diff = animation.percentage;
                    if (diff >= 1) {
                        if (animation._checkEnded()) {
                            this.set(key, animation._value(1));
                        }
                        else {
                            playing = true;
                            animation._reset(currentTime);
                            this._set(key, animation._value(1));
                        }
                    }
                    else {
                        playing = true;
                        this._set(key, animation._value(diff));
                    }
                }
                else {
                    paused = true;
                }
            });
            $object.each(this._animatingPrivateSettings, (key, animation) => {
                if (animation.stopped) {
                    this._stopAnimationPrivate(key);
                }
                else if (animation.playing) {
                    animation._run(currentTime);
                    const diff = animation.percentage;
                    if (diff >= 1) {
                        if (animation._checkEnded()) {
                            this.setPrivate(key, animation._value(1));
                        }
                        else {
                            playing = true;
                            animation._reset(currentTime);
                            this._setPrivate(key, animation._value(1));
                        }
                    }
                    else {
                        playing = true;
                        this._setPrivate(key, animation._value(diff));
                    }
                }
                else {
                    paused = true;
                }
            });
            if (playing) {
                state = AnimationState.Playing;
            }
            else if (paused) {
                state = AnimationState.Paused;
            }
        }
        return state;
    }
    _markDirtyKey(_key) {
        this.markDirty();
    }
    _markDirtyPrivateKey(_key) {
        this.markDirty();
    }
    /**
     * Sets a callback function to invoke when specific key of settings changes
     * or is set.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key       Settings key
     * @param   callback  Callback
     * @return            Disposer for event
     */
    on(key, callback) {
        let events = this._settingEvents[key];
        if (events === undefined) {
            events = this._settingEvents[key] = [];
        }
        events.push(callback);
        return new Disposer(() => {
            $array.removeFirst(events, callback);
            if (events.length === 0) {
                delete this._settingEvents[key];
            }
        });
    }
    /**
     * Removes a callback for when value of a setting changes.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key       Private settings key
     * @param   callback  Callback
     * @since 5.9.2
     */
    off(key, callback) {
        let events = this._settingEvents[key];
        if (events !== undefined && callback !== undefined) {
            $array.removeFirst(events, callback);
        }
        else {
            delete this._settingEvents[key];
        }
    }
    _onDebouncedHelper(map, key, callback, delay) {
        let events = map[key];
        if (events === undefined) {
            events = map[key] = [];
        }
        const entry = { cb: callback, delay };
        events.push(entry);
        return new Disposer(() => {
            if (entry.timeout) {
                window.clearTimeout(entry.timeout);
            }
            $array.removeFirst(events, entry);
            if (events.length === 0) {
                delete map[key];
            }
        });
    }
    _offDebouncedHelper(map, key, callback) {
        const events = map[key];
        if (events !== undefined) {
            if (callback !== undefined) {
                const index = $array.findIndex(events, (e) => e.cb === callback);
                if (index !== -1) {
                    const entry = events[index];
                    if (entry.timeout) {
                        window.clearTimeout(entry.timeout);
                    }
                    events.splice(index, 1);
                    if (events.length === 0) {
                        delete map[key];
                    }
                }
            }
            else {
                $array.each(events, (entry) => {
                    if (entry.timeout) {
                        window.clearTimeout(entry.timeout);
                    }
                });
                delete map[key];
            }
        }
    }
    /**
     * Sets a debounced callback function to invoke when specific key of settings
     * changes or is set. The callback fires only once even if the setting is changed
     * multiple times within the debounce delay.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key            Settings key
     * @param   callback       Callback
     * @param   debounceDelay  Debounce delay in milliseconds
     * @return                 Disposer for event
     */
    onDebounced(key, callback, debounceDelay) {
        return this._onDebouncedHelper(this._debouncedSettingEvents, key, callback, debounceDelay);
    }
    /**
     * Removes a debounced callback for when value of a setting changes.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key       Settings key
     * @param   callback  Callback
     */
    offDebounced(key, callback) {
        this._offDebouncedHelper(this._debouncedSettingEvents, key, callback);
    }
    /**
     * Sets a callback function to invoke when specific key of private settings
     * changes or is set.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key       Private settings key
     * @param   callback  Callback
     * @return            Disposer for event
     */
    onPrivate(key, callback) {
        let events = this._privateSettingEvents[key];
        if (events === undefined) {
            events = this._privateSettingEvents[key] = [];
        }
        events.push(callback);
        return new Disposer(() => {
            $array.removeFirst(events, callback);
            if (events.length === 0) {
                delete this._privateSettingEvents[key];
            }
        });
    }
    /**
     * Removes a callback for when value of a private setting changes.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key       Private settings key
     * @param   callback  Callback
     * @since 5.9.2
     */
    offPrivate(key, callback) {
        let events = this._privateSettingEvents[key];
        if (events !== undefined && callback !== undefined) {
            $array.removeFirst(events, callback);
        }
        else {
            delete this._privateSettingEvents[key];
        }
    }
    /**
     * Sets a debounced callback function to invoke when specific key of private
     * settings changes or is set. The callback fires only once even if the setting
     * is changed multiple times within the debounce delay.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key            Private settings key
     * @param   callback       Callback
     * @param   debounceDelay  Debounce delay in milliseconds
     * @return                 Disposer for event
     */
    onPrivateDebounced(key, callback, debounceDelay) {
        return this._onDebouncedHelper(this._debouncedPrivateSettingEvents, key, callback, debounceDelay);
    }
    /**
     * Removes a debounced callback for when value of a private setting changes.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/events/#Settings_value_change} for more info
     * @param   key       Private settings key
     * @param   callback  Callback
     */
    offDebouncedPrivate(key, callback) {
        this._offDebouncedHelper(this._debouncedPrivateSettingEvents, key, callback);
    }
    /**
     * @ignore
     */
    getRaw(key, fallback) {
        const value = this._settings[key];
        if (value !== undefined) {
            return value;
        }
        else {
            return fallback;
        }
    }
    /**
     * Returns `true` if the setting exists.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     * @param   key        Settings key
     * @return  {boolean}  Key exists
     */
    has(key) {
        return key in this._settings;
    }
    get(key, fallback) {
        return this.getRaw(key, fallback);
    }
    _sendKeyEvent(key, value) {
        const events = this._settingEvents[key];
        if (events !== undefined) {
            $array.each(events, (callback) => {
                callback(value, this, key);
            });
        }
        const debouncedEvents = this._debouncedSettingEvents[key];
        if (debouncedEvents !== undefined) {
            $array.each(debouncedEvents, (entry) => {
                if (entry.timeout) {
                    window.clearTimeout(entry.timeout);
                }
                entry.timeout = window.setTimeout(() => {
                    entry.timeout = undefined;
                    entry.cb(value, this, key);
                }, entry.delay);
            });
        }
    }
    _sendPrivateKeyEvent(key, value) {
        const events = this._privateSettingEvents[key];
        if (events !== undefined) {
            $array.each(events, (callback) => {
                callback(value, this, key);
            });
        }
        const debouncedEvents = this._debouncedPrivateSettingEvents[key];
        if (debouncedEvents !== undefined) {
            $array.each(debouncedEvents, (entry) => {
                if (entry.timeout) {
                    window.clearTimeout(entry.timeout);
                }
                entry.timeout = window.setTimeout(() => {
                    entry.timeout = undefined;
                    entry.cb(value, this, key);
                }, entry.delay);
            });
        }
    }
    /**
     * @ignore
     */
    _setRaw(key, old, value) {
        this._prevSettings[key] = old;
        this._sendKeyEvent(key, value);
    }
    /**
     * @ignore
     */
    setRaw(key, value) {
        const old = this._settings[key];
        this._settings[key] = value;
        if (old !== value) {
            this._setRaw(key, old, value);
        }
    }
    /**
     * @ignore
     */
    _set(key, value) {
        const old = this._settings[key];
        this._settings[key] = value;
        if (old !== value) {
            this._setRaw(key, old, value);
            this._markDirtyKey(key);
        }
    }
    _stopAnimation(key) {
        const animation = this._animatingSettings[key];
        if (animation) {
            delete this._animatingSettings[key];
            animation.stop();
        }
    }
    /**
     * Sets a setting `value` for the specified `key`, and returns the same `value`.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     * @param   key       Setting key
     * @param   value     Setting value
     * @return            Setting value
     */
    set(key, value) {
        this._set(key, value);
        this._stopAnimation(key);
        return value;
    }
    /**
     * Removes a setting value for the specified `key`;
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     * @param   key       Setting key
     */
    remove(key) {
        if (key in this._settings) {
            this._prevSettings[key] = this._settings[key];
            delete this._settings[key];
            this._sendKeyEvent(key, undefined);
            this._markDirtyKey(key);
        }
        this._stopAnimation(key);
    }
    /**
     * Removes all keys;
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     */
    removeAll() {
        $array.each($object.keys(this._settings), (key) => {
            this.remove(key);
        });
    }
    /**
     * Returns a value of a private setting.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/#Private_settings} for more info
     */
    getPrivate(key, fallback) {
        const value = this._privateSettings[key];
        if (value !== undefined) {
            return value;
        }
        else {
            return fallback;
        }
    }
    /**
     * @ignore
     */
    _setPrivateRaw(key, old, value) {
        this._prevPrivateSettings[key] = old;
        this._sendPrivateKeyEvent(key, value);
    }
    /**
     * @ignore
     */
    setPrivateRaw(key, value) {
        const old = this._privateSettings[key];
        this._privateSettings[key] = value;
        if (old !== value) {
            this._setPrivateRaw(key, old, value);
        }
    }
    /**
     * @ignore
     */
    _setPrivate(key, value) {
        const old = this._privateSettings[key];
        this._privateSettings[key] = value;
        if (old !== value) {
            this._setPrivateRaw(key, old, value);
            this._markDirtyPrivateKey(key);
        }
    }
    _stopAnimationPrivate(key) {
        const animation = this._animatingPrivateSettings[key];
        if (animation) {
            animation.stop();
            delete this._animatingPrivateSettings[key];
        }
    }
    /**
     * @ignore
     */
    setPrivate(key, value) {
        this._setPrivate(key, value);
        this._stopAnimationPrivate(key);
        return value;
    }
    /**
     * @ignore
     */
    removePrivate(key) {
        if (key in this._privateSettings) {
            this._prevPrivateSettings[key] = this._privateSettings[key];
            delete this._privateSettings[key];
            this._markDirtyPrivateKey(key);
        }
        this._stopAnimationPrivate(key);
    }
    /**
     * Sets multiple settings at once.
     *
     * `settings` must be an object with key: value pairs.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     * @param settings Settings
     */
    setAll(settings) {
        $object.each(settings, (key, value) => {
            this.set(key, value);
        });
    }
    /**
     * Animates setting values from current/start values to new ones.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/animations/#Animating_settings} for more info
     * @param   options  Animation options
     * @return           Animation object
     */
    animate(options) {
        const key = options.key;
        const to = options.to;
        const duration = options.duration || 0;
        const loops = options.loops || 1;
        const from = (options.from === undefined ? this.get(key) : options.from);
        const easing = (options.easing === undefined ? $ease.linear : options.easing);
        if (duration === 0) {
            this.set(key, to);
        }
        else {
            if (from === undefined || from === to) {
                this.set(key, to);
            }
            else {
                this.set(key, from);
                const animation = this._animatingSettings[key] = new Animation(this, from, to, duration, easing, loops, this._animationTime());
                this._startAnimation();
                return animation;
            }
        }
        const animation = new Animation(this, from, to, duration, easing, loops, null);
        animation.stop();
        return animation;
    }
    /**
     * @ignore
     */
    animatePrivate(options) {
        const key = options.key;
        const to = options.to;
        const duration = options.duration || 0;
        const loops = options.loops || 1;
        const from = (options.from === undefined ? this.getPrivate(key) : options.from);
        const easing = (options.easing === undefined ? $ease.linear : options.easing);
        if (duration === 0) {
            this.setPrivate(key, to);
        }
        else {
            if (from === undefined || from === to) {
                this.setPrivate(key, to);
            }
            else {
                this.setPrivate(key, from);
                const animation = this._animatingPrivateSettings[key] = new Animation(this, from, to, duration, easing, loops, this._animationTime());
                this._startAnimation();
                return animation;
            }
        }
        const animation = new Animation(this, from, to, duration, easing, loops, null);
        animation.stop();
        return animation;
    }
    _dispose() { }
    /**
     * Returns `true` if this element is disposed.
     *
     * @return Disposed
     */
    isDisposed() {
        return this._disposed;
    }
    /**
     * Disposes this object.
     */
    dispose() {
        if (this.enableDispose && !this._disposed) {
            this._disposed = true;
            this._dispose();
        }
    }
}
/**
 * Base class.
 *
 * @important
 */
export class Entity extends Settings {
    /**
     * IMPORTANT! Do not instantiate this class via `new Class()` syntax.
     *
     * Use static method `Class.new()` instead.
     *
     * @see {@link https://www.amcharts.com/docs/v5/getting-started/#New_element_syntax} for more info
     * @ignore
     */
    constructor(root, settings, isReal, templates = []) {
        super(settings);
        this.states = new States(this);
        this.adapters = new Adapters(this);
        this.events = this._createEvents();
        this._userPrivateProperties = {};
        this._dirty = {};
        this._dirtyPrivate = {};
        // Templates for the themes
        this._templates = [];
        // Default themes which can be overridden by the user's themes
        this._defaultThemes = [];
        // Disposers for all of the templates
        this._templateDisposers = [];
        this._disposers = [];
        // Whether the template setup function should be run
        this._runSetup = true;
        this._disposerProperties = {};
        if (!isReal) {
            throw new Error("You cannot use `new Class()`, instead use `Class.new()`");
        }
        this._root = root;
        this._internalTemplates = templates;
        if (settings.id) {
            this._registerId(settings.id);
        }
    }
    /**
     * Use this method to create an instance of this class.
     *
     * @see {@link https://www.amcharts.com/docs/v5/getting-started/#New_element_syntax} for more info
     * @param   root      Root element
     * @param   settings  Settings
     * @param   template  Template
     * @return            Instantiated object
     */
    static new(root, settings, template) {
        const x = (new this(root, settings, true));
        x._template = template;
        x._afterNew();
        return x;
    }
    static _new(root, settings, templates = []) {
        const x = (new this(root, settings, true, templates));
        x._afterNew();
        return x;
    }
    _afterNew() {
        this._checkDirty();
        let shouldApply = false;
        const template = this._template;
        if (template) {
            shouldApply = true;
            template._setObjectTemplate(this);
        }
        $array.each(this._internalTemplates, (template) => {
            shouldApply = true;
            template._setObjectTemplate(this);
        });
        if (shouldApply) {
            this._applyTemplates(false);
        }
        this.states.create("default", {});
        this._setDefaults();
    }
    // This is the same as _afterNew, except it also applies the themes.
    // This should only be used for classes which don't have a parent (because they extend from Entity and not Sprite).
    _afterNewApplyThemes() {
        this._checkDirty();
        const template = this._template;
        if (template) {
            template._setObjectTemplate(this);
        }
        $array.each(this._internalTemplates, (template) => {
            template._setObjectTemplate(this);
        });
        this.states.create("default", {});
        this._setDefaults();
        this._applyThemes();
    }
    _createEvents() {
        return new EventDispatcher();
    }
    /**
     * @ignore
     */
    get classNames() {
        return this.constructor.classNames;
    }
    /**
     * @ignore
     */
    get className() {
        return this.constructor.className;
    }
    _setDefaults() {
    }
    _setDefaultFn(key, f) {
        const value = this.get(key);
        if (value) {
            return value;
        }
        else {
            const value = f();
            this.set(key, value);
            return value;
        }
    }
    _setDefault(key, value) {
        if (!this.has(key)) {
            super.set(key, value);
        }
    }
    _setRawDefault(key, value) {
        if (!this.has(key)) {
            super.setRaw(key, value);
        }
    }
    _clearDirty() {
        $object.keys(this._dirty).forEach((key) => {
            this._dirty[key] = false;
        });
        $object.keys(this._dirtyPrivate).forEach((key) => {
            this._dirtyPrivate[key] = false;
        });
    }
    /**
     * @ignore
     */
    isDirty(key) {
        return !!this._dirty[key];
    }
    /**
     * @ignore
     */
    isPrivateDirty(key) {
        return !!this._dirtyPrivate[key];
    }
    _markDirtyKey(key) {
        this._dirty[key] = true;
        super._markDirtyKey(key);
    }
    _markDirtyPrivateKey(key) {
        this._dirtyPrivate[key] = true;
        super._markDirtyKey(key);
    }
    /**
     * Checks if element is of certain class (or inherits one).
     *
     * @param   type  Class name to check
     * @return {boolean} Is of class?
     */
    isType(type) {
        return this.classNames.indexOf(type) !== -1;
    }
    _pushPropertyDisposer(key, disposer) {
        let disposers = this._disposerProperties[key];
        if (disposers === undefined) {
            disposers = this._disposerProperties[key] = [];
        }
        disposers.push(disposer);
        return disposer;
    }
    _disposeProperty(key) {
        const disposers = this._disposerProperties[key];
        if (disposers !== undefined) {
            $array.each(disposers, (disposer) => {
                disposer.dispose();
            });
            delete this._disposerProperties[key];
        }
    }
    /**
     * @todo needs description
     * @param  value  Template
     */
    set template(value) {
        const template = this._template;
        if (template !== value) {
            this._template = value;
            if (template) {
                template._removeObjectTemplate(this);
            }
            if (value) {
                value._setObjectTemplate(this);
            }
            this._applyTemplates();
        }
    }
    get template() {
        return this._template;
    }
    /**
     * @ignore
     */
    markDirty() {
        this._root._addDirtyEntity(this);
    }
    _startAnimation() {
        this._root._addAnimation(this);
    }
    _animationTime() {
        return this._root.animationTime;
    }
    _applyState(_name) { }
    _applyStateAnimated(_name, _duration) { }
    get(key, fallback) {
        const value = this.adapters.fold(key, this._settings[key]);
        if (value !== undefined) {
            return value;
        }
        else {
            return fallback;
        }
    }
    /**
     * @ignore
     */
    isUserSetting(key) {
        return this._userProperties[key] || false;
    }
    /**
     * Sets a setting `value` for the specified `key`, and returns the same `value`.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     * @param   key       Setting key
     * @param   value     Setting value
     * @return            Setting value
     */
    set(key, value) {
        this._userProperties[key] = true;
        return super.set(key, value);
    }
    /**
     * @ignore
     */
    setRaw(key, value) {
        this._userProperties[key] = true;
        super.setRaw(key, value);
    }
    /**
     * Sets a setting `value` for the specified `key` only if the value for this key was not set previously using set method, and returns the same `value`.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     * @param   key       Setting key
     * @param   value     Setting value
     * @return            Setting value
     */
    _setSoft(key, value) {
        if (!this._userProperties[key]) {
            return super.set(key, value);
        }
        return value;
    }
    /**
     * Removes a setting value for the specified `key`.
     *
     * @see {@link https://www.amcharts.com/docs/v5/concepts/settings/} for more info
     * @param   key       Setting key
     */
    remove(key) {
        delete this._userProperties[key];
        this._removeTemplateProperty(key);
    }
    /**
     * @ignore
     */
    setPrivate(key, value) {
        this._userPrivateProperties[key] = true;
        return super.setPrivate(key, value);
    }
    /**
     * @ignore
     */
    setPrivateRaw(key, value) {
        this._userPrivateProperties[key] = true;
        super.setPrivateRaw(key, value);
    }
    /**
     * @ignore
     */
    removePrivate(key) {
        delete this._userPrivateProperties[key];
        this._removeTemplatePrivateProperty(key);
    }
    _setTemplateProperty(template, key, value) {
        if (!this._userProperties[key]) {
            const match = this._findTemplateByKey(key);
            if (template === match) {
                super.set(key, value);
            }
        }
    }
    _setTemplatePrivateProperty(template, key, value) {
        if (!this._userPrivateProperties[key]) {
            const match = this._findTemplateByPrivateKey(key);
            if (template === match) {
                super.setPrivate(key, value);
            }
        }
    }
    _removeTemplateProperty(key) {
        if (!this._userProperties[key]) {
            const match = this._findTemplateByKey(key);
            if (match) {
                // TODO don't stop the animation if the property didn't change
                super.set(key, match._settings[key]);
            }
            else {
                super.remove(key);
            }
        }
    }
    _removeTemplatePrivateProperty(key) {
        if (!this._userPrivateProperties[key]) {
            const match = this._findTemplateByPrivateKey(key);
            if (match) {
                // TODO don't stop the animation if the property didn't change
                super.setPrivate(key, match._privateSettings[key]);
            }
            else {
                super.removePrivate(key);
            }
        }
    }
    _walkParents(f) {
        f(this._root._rootContainer);
        f(this);
    }
    // TODO faster version of this method which is specialized to just 1 key
    _applyStateByKey(name) {
        const other = this.states.create(name, {});
        const seen = {};
        this._eachTemplate((template) => {
            const state = template.states.lookup(name);
            if (state) {
                state._apply(other, seen);
            }
        });
        $object.each(other._settings, (key) => {
            if (!seen[key] && !other._userSettings[key]) {
                other.remove(key);
            }
        });
    }
    _applyTemplate(template, state) {
        this._templateDisposers.push(template._apply(this, state));
        $object.each(template._settings, (key, value) => {
            if (!state.settings[key] && !this._userProperties[key]) {
                state.settings[key] = true;
                super.set(key, value);
            }
        });
        $object.each(template._privateSettings, (key, value) => {
            if (!state.privateSettings[key] && !this._userPrivateProperties[key]) {
                state.privateSettings[key] = true;
                super.setPrivate(key, value);
            }
        });
        if (this._runSetup && template.setup) {
            this._runSetup = false;
            template.setup(this);
        }
    }
    /**
     * Calls the closure with each template and returns the first template which is true
     */
    _findStaticTemplate(f) {
        if (this._template) {
            if (f(this._template)) {
                return this._template;
            }
        }
    }
    _eachTemplate(f) {
        this._findStaticTemplate((template) => {
            f(template);
            return false;
        });
        // _internalTemplates is sorted with most specific to the right
        $array.eachReverse(this._internalTemplates, f);
        // _templates is sorted with most specific to the left
        $array.each(this._templates, f);
    }
    _applyTemplates(remove = true) {
        if (remove) {
            this._disposeTemplates();
        }
        const state = {
            settings: {},
            privateSettings: {},
            states: {},
        };
        this._eachTemplate((template) => {
            this._applyTemplate(template, state);
        });
        if (remove) {
            $object.each(this._settings, (key) => {
                if (!this._userProperties[key] && !state.settings[key]) {
                    super.remove(key);
                }
            });
            $object.each(this._privateSettings, (key) => {
                if (!this._userPrivateProperties[key] && !state.privateSettings[key]) {
                    super.removePrivate(key);
                }
            });
        }
    }
    _findTemplate(f) {
        const value = this._findStaticTemplate(f);
        if (value === undefined) {
            // _internalTemplates is sorted with most specific to the right
            const value = $array.findReverse(this._internalTemplates, f);
            if (value === undefined) {
                // _templates is sorted with most specific to the left
                return $array.find(this._templates, f);
            }
            else {
                return value;
            }
        }
        else {
            return value;
        }
    }
    _findTemplateByKey(key) {
        return this._findTemplate((template) => {
            return key in template._settings;
        });
    }
    _findTemplateByPrivateKey(key) {
        return this._findTemplate((template) => {
            return key in template._privateSettings;
        });
    }
    _disposeTemplates() {
        $array.each(this._templateDisposers, (disposer) => {
            disposer.dispose();
        });
        this._templateDisposers.length = 0;
    }
    _removeTemplates() {
        $array.each(this._templates, (template) => {
            template._removeObjectTemplate(this);
        });
        this._templates.length = 0;
    }
    _applyThemes(force = false) {
        if (this.get("ignoreThemes")) {
            return false;
        }
        let isConnected = false;
        const defaults = [];
        let themes = [];
        const themeTags = new Set();
        const tags = this.get("themeTagsSelf");
        if (tags) {
            $array.each(tags, (tag) => {
                themeTags.add(tag);
            });
        }
        this._walkParents((entity) => {
            if (entity === this._root._rootContainer) {
                isConnected = true;
            }
            if (entity._defaultThemes.length > 0) {
                defaults.push(entity._defaultThemes);
            }
            const theme = entity.get("themes");
            if (theme) {
                themes.push(theme);
            }
            const tags = entity.get("themeTags");
            if (tags) {
                $array.each(tags, (tag) => {
                    themeTags.add(tag);
                });
            }
        });
        themes = defaults.concat(themes);
        this._removeTemplates();
        if (isConnected || force) {
            $array.eachReverse(this.classNames, (name) => {
                const allRules = [];
                $array.each(themes, (themes) => {
                    $array.each(themes, (theme) => {
                        const rules = theme._lookupRules(name);
                        if (rules) {
                            $array.eachReverse(rules, (rule) => {
                                const matches = rule.tags.every((tag) => {
                                    return themeTags.has(tag);
                                });
                                if (matches) {
                                    const result = $array.getFirstSortedIndex(allRules, (x) => {
                                        const order = $order.compare(rule.tags.length, x.tags.length);
                                        if (order === 0) {
                                            return $order.compareArray(rule.tags, x.tags, $order.compare);
                                        }
                                        else {
                                            return order;
                                        }
                                    });
                                    allRules.splice(result.index, 0, rule);
                                }
                            });
                        }
                    });
                });
                $array.each(allRules, (rule) => {
                    this._templates.push(rule.template);
                    rule.template._setObjectTemplate(this);
                });
            });
        }
        this._applyTemplates();
        if (isConnected || force) {
            // This causes it to only run the setup function the first time that the themes are applied
            this._runSetup = false;
        }
        return isConnected || force;
    }
    _changed() { }
    _beforeChanged() {
        if (this.isDirty("id")) {
            const id = this.get("id");
            if (id) {
                this._registerId(id);
            }
            const prevId = this._prevSettings.id;
            if (prevId) {
                delete this._root.entitiesById[prevId];
                delete registry.entitiesById[prevId];
            }
        }
    }
    _registerId(id) {
        if (this._root.entitiesById[id] && this._root.entitiesById[id] !== this) {
            throw new Error("An entity with id \"" + id + "\" already exists.");
        }
        this._root.entitiesById[id] = this;
        registry.entitiesById[id] = this;
    }
    _afterChanged() { }
    /**
     * @ignore
     */
    addDisposer(disposer) {
        this._disposers.push(disposer);
        return disposer;
    }
    _dispose() {
        super._dispose();
        const template = this._template;
        if (template) {
            template._removeObjectTemplate(this);
        }
        $array.each(this._internalTemplates, (template) => {
            template._removeObjectTemplate(this);
        });
        this._removeTemplates();
        this._disposeTemplates();
        this.events.dispose();
        this._disposers.forEach((x) => {
            x.dispose();
        });
        $object.each(this._disposerProperties, (_, disposers) => {
            $array.each(disposers, (disposer) => {
                disposer.dispose();
            });
        });
        const id = this.get("id");
        if (id) {
            delete this._root.entitiesById[id];
            delete registry.entitiesById[id];
        }
    }
    /**
     * Creates and returns a "disposable" timeout.
     *
     * @param   fn     Callback
     * @param   delay  Delay in milliseconds
     * @return         Timeout disposer
     */
    setTimeout(fn, delay) {
        const id = setTimeout(() => {
            this.removeDispose(disposer);
            fn();
        }, delay);
        const disposer = new Disposer(() => {
            clearTimeout(id);
        });
        this._disposers.push(disposer);
        return disposer;
    }
    /**
     * @ignore
     */
    removeDispose(target) {
        if (!this.isDisposed()) {
            let index = $array.indexOf(this._disposers, target);
            if (index > -1) {
                this._disposers.splice(index, 1);
            }
        }
        target.dispose();
    }
    /**
     * @ignore
     */
    hasTag(tag) {
        return $array.indexOf(this.get("themeTags", []), tag) !== -1;
    }
    /**
     * @ignore
     */
    addTag(tag) {
        if (!this.hasTag(tag)) {
            const tags = this.get("themeTags", []);
            tags.push(tag);
            this.set("themeTags", tags);
        }
    }
    /**
     * @ignore
     */
    removeTag(tag) {
        if (this.hasTag(tag)) {
            const tags = this.get("themeTags", []);
            $array.remove(tags, tag);
            this.set("themeTags", tags);
        }
    }
    _t(text, locale, ...rest) {
        return this._root.language.translate(text, locale, ...rest);
    }
    /**
     * An instance of [[Root]] object.
     *
     * @readonly
     * @since 5.0.6
     * @return Root object
     */
    get root() {
        return this._root;
    }
}
Entity.className = "Entity";
Entity.classNames = ["Entity"];
//# sourceMappingURL=Entity.js.map