import { Signal0, Signal, Signalable } from "./signaling";
import { is_ref } from "./util/refs";
import * as p from "./properties";
import * as k from "./kinds";
import { uniqueId } from "./util/string";
import { values, entries, extend } from "./util/object";
import { isPlainObject, isArray, isFunction, isPrimitive } from "./util/types";
import { is_equal } from './util/eq';
import { serialize } from "./serializer";
import { DocumentEventBatch, ModelChangedEvent } from "../document/events";
import { equals } from "./util/eq";
import { pretty } from "./util/pretty";
import { clone, Cloner } from "./util/cloneable";
import * as kinds from "./kinds";
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.exprchange = new Signal0(this, "exprchange");
        this.properties = {};
        this._pending = false;
        this._changing = false;
        const get = attrs instanceof Map ? attrs.get.bind(attrs) : (name) => attrs[name];
        this.id = get("id") ?? uniqueId();
        for (const [name, { type, default_value, options }] of entries(this._props)) {
            let property;
            if (type instanceof p.PropertyAlias) {
                Object.defineProperty(this.properties, name, {
                    get: () => this.properties[type.attr],
                    configurable: false,
                    enumerable: false,
                });
            }
            else {
                if (type instanceof k.Kind)
                    property = new p.PrimitiveProperty(this, name, type, default_value, get(name), options);
                else
                    property = new type(this, name, k.Any, default_value, get(name), options);
                this.properties[name] = property;
            }
        }
        // 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 (!(get("__deferred__") ?? false)) {
            this.finalize();
            this.connect_signals();
        }
    }
    get is_syncable() {
        return true;
    }
    // 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 get [Symbol.toStringTag]() {
        return this.__name__;
    }
    static init_HasProps() {
        this.prototype._props = {};
        this.prototype._mixins = [];
    }
    static _fix_default(default_value, _attr) {
        if (default_value === undefined || isFunction(default_value))
            return default_value;
        else if (isPrimitive(default_value))
            return () => default_value;
        else {
            const cloner = new Cloner();
            return () => cloner.clone(default_value);
        }
    }
    // TODO: don't use Partial<>, but exclude inherited properties
    static define(obj) {
        for (const [name, prop] of entries(isFunction(obj) ? obj(kinds) : obj)) {
            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.properties[name].get_value();
                    return value;
                },
                set(value) {
                    this.setv({ [name]: value });
                    return this;
                },
                configurable: false,
                enumerable: true,
            });
            const [type, default_value, options = {}] = prop;
            const refined_prop = {
                type,
                default_value: this._fix_default(default_value, name),
                options,
            };
            const props = { ...this.prototype._props };
            props[name] = refined_prop;
            this.prototype._props = props;
        }
    }
    static internal(obj) {
        const _object = {};
        for (const [name, prop] of entries(isFunction(obj) ? obj(kinds) : obj)) {
            const [type, default_value, options = {}] = prop;
            _object[name] = [type, default_value, { ...options, internal: true }];
        }
        this.define(_object);
    }
    static mixins(defs) {
        function rename(prefix, mixin) {
            const result = {};
            for (const [name, prop] of entries(mixin)) {
                result[prefix + name] = prop;
            }
            return result;
        }
        const mixin_defs = {};
        const mixins = [];
        for (const def of isArray(defs) ? defs : [defs]) {
            if (isArray(def)) {
                const [prefix, mixin] = def;
                extend(mixin_defs, rename(prefix, mixin));
                mixins.push([prefix, mixin]);
            }
            else {
                const mixin = def;
                extend(mixin_defs, mixin);
                mixins.push(["", mixin]);
            }
        }
        this.define(mixin_defs);
        this.prototype._mixins = [...this.prototype._mixins, ...mixins];
    }
    static override(obj) {
        for (const [name, prop] of entries(obj)) {
            const default_value = this._fix_default(prop, name);
            const value = this.prototype._props[name];
            if (value == null)
                throw new Error(`attempted to override nonexistent '${this.prototype.type}.${name}'`);
            const props = { ...this.prototype._props };
            props[name] = { ...value, default_value };
            this.prototype._props = props;
        }
    }
    toString() {
        return `${this.type}(${this.id})`;
    }
    property(name) {
        const prop = this.properties[name];
        if (prop != null)
            return prop;
        else
            throw new Error(`unknown property ${this.type}.${name}`);
    }
    get attributes() {
        const attrs = {};
        for (const prop of this) {
            attrs[prop.attr] = prop.get_value();
        }
        return attrs;
    }
    [clone](cloner) {
        const attrs = new Map();
        for (const prop of this) {
            if (prop.dirty) {
                attrs.set(prop.attr, cloner.clone(prop.get_value()));
            }
        }
        return new this.constructor(attrs);
    }
    [equals](that, cmp) {
        for (const p0 of this) {
            const p1 = that.property(p0.attr);
            if (cmp.eq(p0.get_value(), p1.get_value()))
                return false;
        }
        return true;
    }
    [pretty](printer) {
        const T = printer.token;
        const items = [];
        for (const prop of this) {
            if (prop.dirty) {
                const value = prop.get_value();
                items.push(`${prop.attr}${T(":")} ${printer.to_string(value)}`);
            }
        }
        const cls = this.constructor.__qualified__;
        return `${cls}${T("(")}${T("{")}${items.join(`${T(",")} `)}${T("}")}${T(")")}`;
    }
    [serialize](serializer) {
        const ref = this.ref();
        serializer.add_ref(this, ref);
        const struct = this.struct();
        for (const prop of this) {
            if (prop.syncable && (serializer.include_defaults || prop.dirty)) {
                struct.attributes[prop.attr] = serializer.to_serializable(prop.get_value());
            }
        }
        serializer.add_def(this, struct);
        return ref;
    }
    finalize() {
        for (const prop of this) {
            if (!(prop instanceof p.VectorSpec || prop instanceof p.ScalarSpec))
                continue;
            const value = prop.get_value(); // XXX: T -> any under instanceof
            if (value != null) {
                const { transform, expr } = value;
                if (transform != null)
                    this.connect(transform.change, () => this.transformchange.emit());
                if (expr != null)
                    this.connect(expr.change, () => this.exprchange.emit());
            }
        }
        this.initialize();
    }
    initialize() { }
    connect_signals() { }
    disconnect_signals() {
        Signal.disconnectReceiver(this);
    }
    destroy() {
        this.disconnect_signals();
        this.destroyed.emit();
    }
    // Create a new model with exact attribute values to this one, but new identity.
    clone() {
        const cloner = new Cloner();
        return cloner.clone(this);
    }
    // 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(changes, options) {
        // Extract attributes and options.
        const check_eq = options.check_eq;
        const changed = [];
        const changing = this._changing;
        this._changing = true;
        for (const [prop, value] of changes) {
            if (check_eq === false || !is_equal(prop.get_value(), value)) {
                prop.set_value(value);
                changed.push(prop);
            }
        }
        // Trigger all relevant attribute changes.
        if (changed.length > 0)
            this._pending = true;
        for (const prop of changed) {
            prop.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 (!options.no_change) {
            while (this._pending) {
                this._pending = false;
                this.change.emit();
            }
        }
        this._pending = false;
        this._changing = false;
    }
    setv(changed_attrs, options = {}) {
        const changes = entries(changed_attrs);
        if (changes.length == 0)
            return;
        if (options.silent === true) {
            for (const [attr, value] of changes) {
                this.properties[attr].set_value(value);
            }
            return;
        }
        const changed = new Map();
        const previous = new Map();
        for (const [attr, value] of changes) {
            const prop = this.properties[attr];
            changed.set(prop, value);
            previous.set(prop, prop.get_value());
        }
        this._setv(changed, options);
        const { document } = this;
        if (document != null) {
            const changed = [];
            for (const [prop, value] of previous) {
                changed.push([prop, value, prop.get_value()]);
            }
            for (const [, old_value, new_value] of changed) {
                if (this._needs_invalidate(old_value, new_value)) {
                    document._invalidate_all_models();
                    break;
                }
            }
            this._push_changes(changed, options);
        }
    }
    /** @deprecated */
    getv(name) {
        return this.property(name).get_value();
    }
    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;
    }
    *[Symbol.iterator]() {
        yield* values(this.properties);
    }
    *syncable_properties() {
        for (const prop of this) {
            if (prop.syncable)
                yield prop;
        }
    }
    /** @deprecated */
    serializable_attributes() {
        const attrs = {};
        for (const prop of this.syncable_properties()) {
            attrs[prop.attr] = prop.get_value();
        }
        return attrs;
    }
    // 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, refs, options) {
        const { recursive } = options;
        if (is_ref(v)) {
            const model = doc.get_model_by_id(v.id);
            if (model != null && !refs.has(model)) {
                HasProps._value_record_references(model, refs, { recursive });
            }
        }
        else if (isArray(v)) {
            for (const elem of v)
                HasProps._json_record_references(doc, elem, refs, { recursive });
        }
        else if (isPlainObject(v)) {
            for (const elem of values(v)) {
                HasProps._json_record_references(doc, elem, refs, { recursive });
            }
        }
    }
    // 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, refs, options) {
        const { recursive } = options;
        if (v instanceof HasProps) {
            if (!refs.has(v)) {
                refs.add(v);
                if (recursive) {
                    for (const prop of v.syncable_properties()) {
                        const value = prop.get_value();
                        HasProps._value_record_references(value, refs, { recursive });
                    }
                }
            }
        }
        else if (isArray(v)) {
            for (const elem of v)
                HasProps._value_record_references(elem, refs, { recursive });
        }
        else if (isPlainObject(v)) {
            for (const elem of values(v)) {
                HasProps._value_record_references(elem, refs, { recursive });
            }
        }
    }
    references() {
        const refs = new Set();
        HasProps._value_record_references(this, refs, { recursive: true });
        return refs;
    }
    _doc_attached() { }
    _doc_detached() { }
    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._doc_detached();
        this.document = null;
    }
    _needs_invalidate(old_value, new_value) {
        const new_refs = new Set();
        HasProps._value_record_references(new_value, new_refs, { recursive: false });
        const old_refs = new Set();
        HasProps._value_record_references(old_value, old_refs, { recursive: false });
        for (const new_id of new_refs) {
            if (!old_refs.has(new_id))
                return true;
        }
        for (const old_id of old_refs) {
            if (!new_refs.has(old_id))
                return true;
        }
        return false;
    }
    _push_changes(changes, options = {}) {
        if (!this.is_syncable)
            return;
        const { document } = this;
        if (document == null)
            return;
        const { setter_id } = options;
        const events = [];
        for (const [prop, old_value, new_value] of changes) {
            if (prop.syncable)
                events.push(new ModelChangedEvent(document, this, prop.attr, old_value, new_value, setter_id));
        }
        if (events.length != 0) {
            let event;
            if (events.length == 1)
                [event] = events;
            else
                event = new DocumentEventBatch(document, events, setter_id);
            document._trigger_on_change(event);
        }
    }
    on_change(properties, fn) {
        for (const property of isArray(properties) ? properties : [properties]) {
            this.connect(property.change, fn);
        }
    }
}
HasProps.init_HasProps();
//# sourceMappingURL=has_props.js.map