import { Signal0, Signal, Signalable } from "./signaling";
import * as property_mixins from "./property_mixins";
import { is_ref } from "./util/refs";
import * as p from "./properties";
import { uniqueId } from "./util/string";
import { max, copy } from "./util/array";
import { values, clone, isEmpty } from "./util/object";
import { isPlainObject, isObject, isArray, isFunction } from "./util/types";
import { isEqual } from './util/eq';
export class HasProps extends Signalable() {
    constructor(attrs = {}) {
        super();
        this._subtype = undefined;
        this.document = null;
        this.destroyed = new Signal0(this, "destroyed");
        this.change = new Signal0(this, "change");
        this.transformchange = new Signal0(this, "transformchange");
        this.attributes = {};
        this.properties = {};
        this._set_after_defaults = {};
        this._pending = false;
        this._changing = false;
        for (const name in this.props) {
            const { type, default_value } = this.props[name];
            if (type != null)
                this.properties[name] = new type(this, name, default_value);
            else
                throw new Error(`undefined property type for ${this.type}.${name}`);
        }
        // auto generating ID
        if (attrs.id == null)
            this.setv({ id: uniqueId() }, { silent: true });
        const deferred = attrs.__deferred__ || false;
        if (deferred) {
            attrs = clone(attrs);
            delete attrs.__deferred__;
        }
        this.setv(attrs, { silent: true });
        // allowing us to defer initialization when loading many models
        // when loading a bunch of models, we want to do initialization as a second pass
        // because other objects that this one depends on might not be loaded yet
        if (!deferred)
            this.finalize();
    }
    // XXX: setter is only required for backwards compatibility
    set type(name) {
        console.warn("prototype.type = 'ModelName' is deprecated, use static __name__ instead");
        this.constructor.__name__ = name;
    }
    get type() {
        return this.constructor.__qualified__;
    }
    static get __qualified__() {
        const { __module__, __name__ } = this;
        return __module__ != null ? `${__module__}.${__name__}` : __name__;
    }
    static init_HasProps() {
        this.prototype.props = {};
        this.prototype.mixins = [];
        this.define({
            id: [p.Any],
        });
    }
    // }}}
    static _fix_default(default_value, _attr) {
        if (default_value === undefined)
            return undefined;
        else if (isFunction(default_value))
            return default_value;
        else if (!isObject(default_value))
            return () => default_value;
        else {
            //logger.warn(`${this.prototype.type}.${attr} uses unwrapped non-primitive default value`)
            if (isArray(default_value))
                return () => copy(default_value);
            else
                return () => clone(default_value);
        }
    }
    // TODO: don't use Partial<>, but exclude inherited properties
    static define(obj) {
        for (const name in obj) {
            const prop = obj[name];
            if (this.prototype.props[name] != null)
                throw new Error(`attempted to redefine property '${this.prototype.type}.${name}'`);
            if (this.prototype[name] != null)
                throw new Error(`attempted to redefine attribute '${this.prototype.type}.${name}'`);
            Object.defineProperty(this.prototype, name, {
                // XXX: don't use tail calls in getters/setters due to https://bugs.webkit.org/show_bug.cgi?id=164306
                get() {
                    const value = this.getv(name);
                    return value;
                },
                set(value) {
                    this.setv({ [name]: value });
                    return this;
                },
                configurable: false,
                enumerable: true,
            });
            const [type, default_value, internal] = prop;
            const refined_prop = {
                type,
                default_value: this._fix_default(default_value, name),
                internal: internal || false,
            };
            const props = clone(this.prototype.props);
            props[name] = refined_prop;
            this.prototype.props = props;
        }
    }
    static internal(obj) {
        const _object = {};
        for (const name in obj) {
            const prop = obj[name];
            const [type, default_value] = prop;
            _object[name] = [type, default_value, true];
        }
        this.define(_object);
    }
    static mixin(...names) {
        this.define(property_mixins.create(names));
        const mixins = this.prototype.mixins.concat(names);
        this.prototype.mixins = mixins;
    }
    static mixins(names) {
        this.mixin(...names);
    }
    static override(obj) {
        for (const name in obj) {
            const default_value = this._fix_default(obj[name], name);
            const value = this.prototype.props[name];
            if (value == null)
                throw new Error(`attempted to override nonexistent '${this.prototype.type}.${name}'`);
            const props = clone(this.prototype.props);
            props[name] = Object.assign(Object.assign({}, value), { default_value });
            this.prototype.props = props;
        }
    }
    toString() {
        return `${this.type}(${this.id})`;
    }
    finalize() {
        // This is necessary because the initial creation of properties relies on
        // model.get which is not usable at that point yet in the constructor. This
        // initializer is called when deferred initialization happens for all models
        // and insures that the Bokeh properties are initialized from Backbone
        // attributes in a consistent way.
        //
        // TODO (bev) split property creation up into two parts so that only the
        // portion of init that can be done happens in HasProps constructor and so
        // that subsequent updates do not duplicate that setup work.
        for (const name in this.properties) {
            const prop = this.properties[name];
            prop.update();
            if (prop.spec.transform != null)
                this.connect(prop.spec.transform.change, () => this.transformchange.emit());
        }
        this.initialize();
        this.connect_signals();
    }
    initialize() { }
    connect_signals() { }
    disconnect_signals() {
        Signal.disconnectReceiver(this);
    }
    destroy() {
        this.disconnect_signals();
        this.destroyed.emit();
    }
    // Create a new model with identical attributes to this one.
    clone() {
        return new this.constructor(this.attributes);
    }
    // Set a hash of model attributes on the object, firing `"change"`. This is
    // the core primitive operation of a model, updating the data and notifying
    // anyone who needs to know about the change in state. The heart of the beast.
    _setv(attrs, options) {
        // Extract attributes and options.
        const check_eq = options.check_eq;
        const silent = options.silent;
        const changes = [];
        const changing = this._changing;
        this._changing = true;
        const current = this.attributes;
        // For each `set` attribute, update or delete the current value.
        for (const attr in attrs) {
            const val = attrs[attr];
            if (check_eq !== false) {
                if (!isEqual(current[attr], val))
                    changes.push(attr);
            }
            else
                changes.push(attr);
            current[attr] = val;
        }
        // Trigger all relevant attribute changes.
        if (!silent) {
            if (changes.length > 0)
                this._pending = true;
            for (let i = 0; i < changes.length; i++)
                this.properties[changes[i]].change.emit();
        }
        // You might be wondering why there's a `while` loop here. Changes can
        // be recursively nested within `"change"` events.
        if (changing)
            return;
        if (!silent && !options.no_change) {
            while (this._pending) {
                this._pending = false;
                this.change.emit();
            }
        }
        this._pending = false;
        this._changing = false;
    }
    setv(attrs, options = {}) {
        for (const key in attrs) {
            if (!attrs.hasOwnProperty(key))
                continue;
            const prop_name = key;
            if (this.props[prop_name] == null)
                throw new Error(`property ${this.type}.${prop_name} wasn't declared`);
            if (!(options != null && options.defaults))
                this._set_after_defaults[key] = true;
        }
        if (!isEmpty(attrs)) {
            const old = {};
            for (const key in attrs)
                old[key] = this.getv(key);
            this._setv(attrs, options);
            const silent = options.silent;
            if (silent == null || !silent) {
                for (const key in attrs)
                    this._tell_document_about_change(key, old[key], this.getv(key), options);
            }
        }
    }
    getv(prop_name) {
        if (this.props[prop_name] == null)
            throw new Error(`property ${this.type}.${prop_name} wasn't declared`);
        else
            return this.attributes[prop_name];
    }
    ref() {
        return { id: this.id };
    }
    struct() {
        const struct = {
            type: this.type,
            id: this.id,
            attributes: {},
        };
        if (this._subtype != null) {
            struct.subtype = this._subtype;
        }
        return struct;
    }
    // we only keep the subtype so we match Python;
    // only Python cares about this
    set_subtype(subtype) {
        this._subtype = subtype;
    }
    attribute_is_serializable(attr) {
        const prop = this.props[attr];
        if (prop == null)
            throw new Error(`${this.type}.attribute_is_serializable('${attr}'): ${attr} wasn't declared`);
        else
            return !prop.internal;
    }
    // dict of attributes that should be serialized to the server. We
    // sometimes stick things in attributes that aren't part of the
    // Document's models, subtypes that do that have to remove their
    // extra attributes here.
    serializable_attributes() {
        const attrs = {};
        for (const name in this.attributes) {
            const value = this.attributes[name];
            if (this.attribute_is_serializable(name))
                attrs[name] = value;
        }
        return attrs;
    }
    static _value_to_json(_key, value, _optional_parent_object) {
        if (value instanceof HasProps)
            return value.ref();
        else if (isArray(value)) {
            const ref_array = [];
            for (let i = 0; i < value.length; i++) {
                const v = value[i];
                ref_array.push(HasProps._value_to_json(i.toString(), v, value));
            }
            return ref_array;
        }
        else if (isPlainObject(value)) {
            const ref_obj = {};
            for (const subkey in value) {
                if (value.hasOwnProperty(subkey))
                    ref_obj[subkey] = HasProps._value_to_json(subkey, value[subkey], value);
            }
            return ref_obj;
        }
        else
            return value;
    }
    // Convert attributes to "shallow" JSON (values which are themselves models
    // are included as just references)
    attributes_as_json(include_defaults = true, value_to_json = HasProps._value_to_json) {
        const serializable = this.serializable_attributes();
        const attrs = {};
        for (const key in serializable) {
            if (serializable.hasOwnProperty(key)) {
                const value = serializable[key];
                if (include_defaults)
                    attrs[key] = value;
                else if (key in this._set_after_defaults)
                    attrs[key] = value;
            }
        }
        return value_to_json("attributes", attrs, this);
    }
    // this is like _value_record_references but expects to find refs
    // instead of models, and takes a doc to look up the refs in
    static _json_record_references(doc, v, result, recurse) {
        if (v == null) {
        }
        else if (is_ref(v)) {
            if (!(v.id in result)) {
                const model = doc.get_model_by_id(v.id);
                HasProps._value_record_references(model, result, recurse);
            }
        }
        else if (isArray(v)) {
            for (const elem of v)
                HasProps._json_record_references(doc, elem, result, recurse);
        }
        else if (isPlainObject(v)) {
            for (const k in v) {
                if (v.hasOwnProperty(k)) {
                    const elem = v[k];
                    HasProps._json_record_references(doc, elem, result, recurse);
                }
            }
        }
    }
    // add all references from 'v' to 'result', if recurse
    // is true then descend into refs, if false only
    // descend into non-refs
    static _value_record_references(v, result, recurse) {
        if (v == null) {
        }
        else if (v instanceof HasProps) {
            if (!(v.id in result)) {
                result[v.id] = v;
                if (recurse) {
                    const immediate = v._immediate_references();
                    for (const obj of immediate)
                        HasProps._value_record_references(obj, result, true); // true=recurse
                }
            }
        }
        else if (v.buffer instanceof ArrayBuffer) {
        }
        else if (isArray(v)) {
            for (const elem of v)
                HasProps._value_record_references(elem, result, recurse);
        }
        else if (isPlainObject(v)) {
            for (const k in v) {
                if (v.hasOwnProperty(k)) {
                    const elem = v[k];
                    HasProps._value_record_references(elem, result, recurse);
                }
            }
        }
    }
    // Get models that are immediately referenced by our properties
    // (do not recurse, do not include ourselves)
    _immediate_references() {
        const result = {};
        const attrs = this.serializable_attributes();
        for (const key in attrs) {
            const value = attrs[key];
            HasProps._value_record_references(value, result, false); // false = no recurse
        }
        return values(result);
    }
    references() {
        const references = {};
        HasProps._value_record_references(this, references, true);
        return values(references);
    }
    _doc_attached() { }
    attach_document(doc) {
        // This should only be called by the Document implementation to set the document field
        if (this.document != null && this.document != doc)
            throw new Error("models must be owned by only a single document");
        this.document = doc;
        this._doc_attached();
    }
    detach_document() {
        // This should only be called by the Document implementation to unset the document field
        this.document = null;
    }
    _tell_document_about_change(attr, old, new_, options) {
        if (!this.attribute_is_serializable(attr))
            return;
        if (this.document != null) {
            const new_refs = {};
            HasProps._value_record_references(new_, new_refs, false);
            const old_refs = {};
            HasProps._value_record_references(old, old_refs, false);
            let need_invalidate = false;
            for (const new_id in new_refs) {
                if (!(new_id in old_refs)) {
                    need_invalidate = true;
                    break;
                }
            }
            if (!need_invalidate) {
                for (const old_id in old_refs) {
                    if (!(old_id in new_refs)) {
                        need_invalidate = true;
                        break;
                    }
                }
            }
            if (need_invalidate)
                this.document._invalidate_all_models();
            this.document._notify_change(this, attr, old, new_, options);
        }
    }
    materialize_dataspecs(source) {
        // Note: this should be moved to a function separate from HasProps
        const data = {};
        for (const name in this.properties) {
            const prop = this.properties[name];
            if (!(prop instanceof p.VectorSpec))
                continue;
            // this skips optional properties like radius for circles
            if (prop.optional && prop.spec.value == null && !(name in this._set_after_defaults))
                continue;
            const array = prop.array(source);
            data[`_${name}`] = array;
            // the shapes are indexed by the column name, but when we materialize the dataspec, we should
            // store under the canonical field name, e.g. _image_shape, even if the column name is "foo"
            if (prop.spec.field != null && prop.spec.field in source._shapes)
                data[`_${name}_shape`] = source._shapes[prop.spec.field];
            if (prop instanceof p.DistanceSpec)
                data[`max_${name}`] = max(array);
        }
        return data;
    }
}
HasProps.init_HasProps();
//# sourceMappingURL=has_props.js.map