import { ModelResolver } from "../base";
import { version as js_version } from "../version";
import { logger } from "../core/logging";
import { DocumentReady, LODStart, LODEnd } from "../core/bokeh_events";
import { HasProps } from "../core/has_props";
import { Serializer } from "../core/serializer";
import { Signal0 } from "../core/signaling";
import { is_ref } from "../core/util/refs";
import { is_NDArray_ref, decode_NDArray } from "../core/util/serialization";
import { ndarray } from "../core/util/ndarray";
import { difference, intersection, copy, includes } from "../core/util/array";
import { values, entries } from "../core/util/object";
import * as sets from "../core/util/set";
import { is_equal } from "../core/util/eq";
import { isArray, isPlainObject } from "../core/util/types";
import { LayoutDOM } from "../models/layouts/layout_dom";
import { ColumnDataSource } from "../models/sources/column_data_source";
import { Model } from "../model";
import { resolve_defs } from "./defs";
import { DocumentEventBatch, ModelChangedEvent, RootRemovedEvent, TitleChangedEvent, MessageSentEvent, RootAddedEvent, } from "./events";
// Dispatches events to the subscribed models
export class EventManager {
    constructor(document) {
        this.document = document;
        this.session = null;
        this.subscribed_models = new Set();
    }
    send_event(bokeh_event) {
        const event = new MessageSentEvent(this.document, "bokeh_event", bokeh_event.to_json());
        this.document._trigger_on_change(event);
    }
    trigger(event) {
        for (const model of this.subscribed_models) {
            if (event.origin != null && event.origin != model)
                continue;
            model._process_event(event);
        }
    }
}
EventManager.__name__ = "EventManager";
export const documents = [];
export const DEFAULT_TITLE = "Bokeh Application";
// This class should match the API of the Python Document class
// as much as possible.
export class Document {
    constructor(options) {
        documents.push(this);
        this._init_timestamp = Date.now();
        this._resolver = options?.resolver ?? new ModelResolver();
        this._title = DEFAULT_TITLE;
        this._roots = [];
        this._all_models = new Map();
        this._all_models_freeze_count = 0;
        this._callbacks = new Map();
        this._message_callbacks = new Map();
        this.event_manager = new EventManager(this);
        this.idle = new Signal0(this, "idle");
        this._idle_roots = new WeakMap(); // TODO: WeakSet would be better
        this._interactive_timestamp = null;
        this._interactive_plot = null;
    }
    get layoutables() {
        return this._roots.filter((root) => root instanceof LayoutDOM);
    }
    get is_idle() {
        for (const root of this.layoutables) {
            if (!this._idle_roots.has(root))
                return false;
        }
        return true;
    }
    notify_idle(model) {
        this._idle_roots.set(model, true);
        if (this.is_idle) {
            logger.info(`document idle at ${Date.now() - this._init_timestamp} ms`);
            this.event_manager.send_event(new DocumentReady());
            this.idle.emit();
        }
    }
    clear() {
        this._push_all_models_freeze();
        try {
            while (this._roots.length > 0) {
                this.remove_root(this._roots[0]);
            }
        }
        finally {
            this._pop_all_models_freeze();
        }
    }
    interactive_start(plot, finalize = null) {
        if (this._interactive_plot == null) {
            this._interactive_plot = plot;
            this._interactive_plot.trigger_event(new LODStart());
        }
        this._interactive_finalize = finalize;
        this._interactive_timestamp = Date.now();
    }
    interactive_stop() {
        if (this._interactive_plot != null) {
            this._interactive_plot.trigger_event(new LODEnd());
            if (this._interactive_finalize != null) {
                this._interactive_finalize();
            }
        }
        this._interactive_plot = null;
        this._interactive_timestamp = null;
        this._interactive_finalize = null;
    }
    interactive_duration() {
        if (this._interactive_timestamp == null)
            return -1;
        else
            return Date.now() - this._interactive_timestamp;
    }
    destructively_move(dest_doc) {
        if (dest_doc === this) {
            throw new Error("Attempted to overwrite a document with itself");
        }
        dest_doc.clear();
        // we have to remove ALL roots before adding any
        // to the new doc or else models referenced from multiple
        // roots could be in both docs at once, which isn't allowed.
        const roots = copy(this._roots);
        this.clear();
        for (const root of roots) {
            if (root.document != null)
                throw new Error(`Somehow we didn't detach ${root}`);
        }
        if (this._all_models.size != 0) {
            throw new Error(`this._all_models still had stuff in it: ${this._all_models}`);
        }
        for (const root of roots) {
            dest_doc.add_root(root);
        }
        dest_doc.set_title(this._title);
    }
    // TODO other fields of doc
    _push_all_models_freeze() {
        this._all_models_freeze_count += 1;
    }
    _pop_all_models_freeze() {
        this._all_models_freeze_count -= 1;
        if (this._all_models_freeze_count === 0) {
            this._recompute_all_models();
        }
    }
    /*protected*/ _invalidate_all_models() {
        logger.debug("invalidating document models");
        // if freeze count is > 0, we'll recompute on unfreeze
        if (this._all_models_freeze_count === 0) {
            this._recompute_all_models();
        }
    }
    _recompute_all_models() {
        let new_all_models_set = new Set();
        for (const r of this._roots) {
            new_all_models_set = sets.union(new_all_models_set, r.references());
        }
        const old_all_models_set = new Set(this._all_models.values());
        const to_detach = sets.difference(old_all_models_set, new_all_models_set);
        const to_attach = sets.difference(new_all_models_set, old_all_models_set);
        const recomputed = new Map();
        for (const model of new_all_models_set) {
            recomputed.set(model.id, model);
        }
        for (const d of to_detach) {
            d.detach_document();
        }
        for (const a of to_attach) {
            a.attach_document(this);
        }
        this._all_models = recomputed;
    }
    roots() {
        return this._roots;
    }
    add_root(model, setter_id) {
        logger.debug(`Adding root: ${model}`);
        if (includes(this._roots, model))
            return;
        this._push_all_models_freeze();
        try {
            this._roots.push(model);
        }
        finally {
            this._pop_all_models_freeze();
        }
        this._trigger_on_change(new RootAddedEvent(this, model, setter_id));
    }
    remove_root(model, setter_id) {
        const i = this._roots.indexOf(model);
        if (i < 0)
            return;
        this._push_all_models_freeze();
        try {
            this._roots.splice(i, 1);
        }
        finally {
            this._pop_all_models_freeze();
        }
        this._trigger_on_change(new RootRemovedEvent(this, model, setter_id));
    }
    title() {
        return this._title;
    }
    set_title(title, setter_id) {
        if (title !== this._title) {
            this._title = title;
            this._trigger_on_change(new TitleChangedEvent(this, title, setter_id));
        }
    }
    get_model_by_id(model_id) {
        return this._all_models.get(model_id) ?? null;
    }
    get_model_by_name(name) {
        const found = [];
        for (const model of this._all_models.values()) {
            if (model instanceof Model && model.name == name)
                found.push(model);
        }
        switch (found.length) {
            case 0:
                return null;
            case 1:
                return found[0];
            default:
                throw new Error(`Multiple models are named '${name}'`);
        }
    }
    on_message(msg_type, callback) {
        const message_callbacks = this._message_callbacks.get(msg_type);
        if (message_callbacks == null)
            this._message_callbacks.set(msg_type, new Set([callback]));
        else
            message_callbacks.add(callback);
    }
    remove_on_message(msg_type, callback) {
        this._message_callbacks.get(msg_type)?.delete(callback);
    }
    _trigger_on_message(msg_type, msg_data) {
        const message_callbacks = this._message_callbacks.get(msg_type);
        if (message_callbacks != null) {
            for (const cb of message_callbacks) {
                cb(msg_data);
            }
        }
    }
    on_change(callback, allow_batches = false) {
        if (!this._callbacks.has(callback)) {
            this._callbacks.set(callback, allow_batches);
        }
    }
    remove_on_change(callback) {
        this._callbacks.delete(callback);
    }
    _trigger_on_change(event) {
        for (const [callback, allow_batches] of this._callbacks) {
            if (!allow_batches && event instanceof DocumentEventBatch) {
                for (const ev of event.events) {
                    callback(ev);
                }
            }
            else {
                callback(event); // TODO
            }
        }
    }
    _notify_change(model, attr, old_value, new_value, options) {
        this._trigger_on_change(new ModelChangedEvent(this, model, attr, old_value, new_value, options?.setter_id, options?.hint));
    }
    static _instantiate_object(obj_id, obj_type, obj_attrs, resolver) {
        const full_attrs = { ...obj_attrs, id: obj_id, __deferred__: true };
        const model = resolver.get(obj_type);
        return new model(full_attrs);
    }
    // given a JSON representation of all models in a graph, return a
    // dict of new model objects
    static _instantiate_references_json(references_json, existing_models, resolver) {
        // Create all instances, but without setting their props
        const references = new Map();
        for (const obj of references_json) {
            const obj_id = obj.id;
            const obj_type = obj.type;
            const obj_attrs = obj.attributes ?? {};
            let instance = existing_models.get(obj_id);
            if (instance == null) {
                instance = Document._instantiate_object(obj_id, obj_type, obj_attrs, resolver);
                if (obj.subtype != null)
                    instance.set_subtype(obj.subtype);
            }
            references.set(instance.id, instance);
        }
        return references;
    }
    // if v looks like a ref, or a collection, resolve it, otherwise return it unchanged
    // recurse into collections but not into HasProps
    static _resolve_refs(value, old_references, new_references, buffers) {
        function resolve_ref(v) {
            if (is_ref(v)) {
                const obj = old_references.get(v.id) ?? new_references.get(v.id);
                if (obj != null)
                    return obj;
                else
                    throw new Error(`reference ${JSON.stringify(v)} isn't known (not in Document?)`);
            }
            else if (is_NDArray_ref(v)) {
                const { buffer, dtype, shape } = decode_NDArray(v, buffers);
                return ndarray(buffer, { dtype, shape });
            }
            else if (isArray(v))
                return resolve_array(v);
            else if (isPlainObject(v))
                return resolve_dict(v);
            else
                return v;
        }
        function resolve_array(array) {
            const results = [];
            for (const v of array) {
                results.push(resolve_ref(v));
            }
            return results;
        }
        function resolve_dict(dict) {
            const resolved = {};
            for (const [k, v] of entries(dict)) {
                resolved[k] = resolve_ref(v);
            }
            return resolved;
        }
        return resolve_ref(value);
    }
    // given a JSON representation of all models in a graph and new
    // model instances, set the properties on the models from the
    // JSON
    static _initialize_references_json(references_json, old_references, new_references, buffers) {
        const to_update = new Map();
        for (const { id, attributes } of references_json) {
            const is_new = !old_references.has(id);
            const instance = is_new ? new_references.get(id) : old_references.get(id);
            // replace references with actual instances in obj_attrs
            const resolved_attrs = Document._resolve_refs(attributes, old_references, new_references, buffers);
            instance.setv(resolved_attrs, { silent: true });
            to_update.set(id, { instance, is_new });
        }
        const ordered_instances = [];
        const handled = new Set();
        function finalize_all_by_dfs(v) {
            if (v instanceof HasProps) {
                // note that we ignore instances that aren't updated (not in to_update)
                if (to_update.has(v.id) && !handled.has(v.id)) {
                    handled.add(v.id);
                    const { instance, is_new } = to_update.get(v.id);
                    const { attributes } = instance;
                    for (const value of values(attributes)) {
                        finalize_all_by_dfs(value);
                    }
                    if (is_new) {
                        // Finalizing here just to avoid iterating
                        // over `ordered_instances` twice.
                        instance.finalize();
                        // Preserving an ordered collection of instances
                        // to avoid having to go through DFS again.
                        ordered_instances.push(instance);
                    }
                }
            }
            else if (isArray(v)) {
                for (const e of v)
                    finalize_all_by_dfs(e);
            }
            else if (isPlainObject(v)) {
                for (const value of values(v))
                    finalize_all_by_dfs(value);
            }
        }
        for (const item of to_update.values()) {
            finalize_all_by_dfs(item.instance);
        }
        // `connect_signals` has to be executed last because it
        // may rely on properties of dependencies that are initialized
        // only in `finalize`. It's a problem that appears when
        // there are circular references, e.g. as in
        // CDS -> CustomJS (on data change) -> GlyphRenderer (in args) -> CDS.
        for (const instance of ordered_instances) {
            instance.connect_signals();
        }
    }
    //////
    ///{{{
    static _event_for_attribute_change(changed_obj, key, new_value, doc, value_refs) {
        const changed_model = doc.get_model_by_id(changed_obj.id); // XXX!
        if (!changed_model.property(key).syncable)
            return null;
        else {
            const event = {
                kind: "ModelChanged",
                model: { id: changed_obj.id },
                attr: key,
                new: new_value,
            };
            HasProps._json_record_references(doc, new_value, value_refs, { recursive: true });
            return event;
        }
    }
    static _events_to_sync_objects(from_obj, to_obj, to_doc, value_refs) {
        const from_keys = Object.keys(from_obj.attributes); //XXX!
        const to_keys = Object.keys(to_obj.attributes); //XXX!
        const removed = difference(from_keys, to_keys);
        const added = difference(to_keys, from_keys);
        const shared = intersection(from_keys, to_keys);
        const events = [];
        for (const key of removed) {
            // we don't really have a "remove" event - not sure this ever
            // happens even. One way this could happen is if the server
            // does include_defaults=True and we do
            // include_defaults=false ... in that case it'd be best to
            // just ignore this probably. Warn about it, could mean
            // there's a bug if we don't have a key that the server sent.
            logger.warn(`Server sent key ${key} but we don't seem to have it in our JSON`);
        }
        for (const key of added) {
            const new_value = to_obj.attributes[key]; // XXX!
            events.push(Document._event_for_attribute_change(from_obj, key, new_value, to_doc, value_refs));
        }
        for (const key of shared) {
            const old_value = from_obj.attributes[key]; // XXX!
            const new_value = to_obj.attributes[key]; // XXX!
            if (old_value == null && new_value == null) {
            }
            else if (old_value == null || new_value == null) {
                events.push(Document._event_for_attribute_change(from_obj, key, new_value, to_doc, value_refs));
            }
            else {
                // XXX: issue #11803, ndarrays' JSON-like repr may not be comparable due to lazy serialization
                if (key != "data" && !is_equal(old_value, new_value))
                    events.push(Document._event_for_attribute_change(from_obj, key, new_value, to_doc, value_refs));
            }
        }
        return events.filter((e) => e != null);
    }
    // we use this to detect changes during document deserialization
    // (in model constructors and initializers)
    static _compute_patch_since_json(from_json, to_doc) {
        const to_json = to_doc.to_json(false); // include_defaults=false
        function refs(json) {
            const result = new Map();
            for (const obj of json.roots.references)
                result.set(obj.id, obj);
            return result;
        }
        const from_references = refs(from_json);
        const from_roots = new Map();
        const from_root_ids = [];
        for (const r of from_json.roots.root_ids) {
            from_roots.set(r, from_references.get(r));
            from_root_ids.push(r);
        }
        const to_references = refs(to_json);
        const to_roots = new Map();
        const to_root_ids = [];
        for (const r of to_json.roots.root_ids) {
            to_roots.set(r, to_references.get(r));
            to_root_ids.push(r);
        }
        from_root_ids.sort();
        to_root_ids.sort();
        if (difference(from_root_ids, to_root_ids).length > 0 ||
            difference(to_root_ids, from_root_ids).length > 0) {
            // this would arise if someone does add_root/remove_root during
            // document deserialization, hopefully they won't ever do so.
            throw new Error("Not implemented: computing add/remove of document roots");
        }
        const value_refs = new Set();
        let events = [];
        for (const id of to_doc._all_models.keys()) {
            if (from_references.has(id)) {
                const update_model_events = Document._events_to_sync_objects(from_references.get(id), to_references.get(id), to_doc, value_refs);
                events = events.concat(update_model_events);
            }
        }
        const serializer = new Serializer({ include_defaults: false });
        serializer.to_serializable([...value_refs]);
        return {
            references: [...serializer.definitions],
            events,
        };
    }
    ///}}}
    //////
    to_json_string(include_defaults = true) {
        return JSON.stringify(this.to_json(include_defaults));
    }
    to_json(include_defaults = true) {
        const serializer = new Serializer({ include_defaults });
        const roots = serializer.to_serializable(this._roots);
        return {
            version: js_version,
            title: this._title,
            roots: {
                root_ids: roots.map((r) => r.id),
                references: [...serializer.definitions],
            },
        };
    }
    static from_json_string(s) {
        const json = JSON.parse(s);
        return Document.from_json(json);
    }
    static from_json(json) {
        logger.debug("Creating Document from JSON");
        function pyify(version) {
            return version.replace(/-(dev|rc)\./, "$1");
        }
        const py_version = json.version; // XXX!
        const is_dev = py_version.indexOf("+") !== -1 || py_version.indexOf("-") !== -1;
        const versions_string = `Library versions: JS (${js_version}) / Python (${py_version})`;
        if (!is_dev && pyify(js_version) != py_version) {
            logger.warn("JS/Python version mismatch");
            logger.warn(versions_string);
        }
        else
            logger.debug(versions_string);
        const resolver = new ModelResolver();
        if (json.defs != null) {
            resolve_defs(json.defs, resolver);
        }
        const roots_json = json.roots;
        const root_ids = roots_json.root_ids;
        const references_json = roots_json.references;
        const references = Document._instantiate_references_json(references_json, new Map(), resolver);
        Document._initialize_references_json(references_json, new Map(), references, new Map());
        const doc = new Document({ resolver });
        doc._push_all_models_freeze();
        for (const id of root_ids) {
            const root = references.get(id);
            if (root != null) {
                doc.add_root(root); // XXX: HasProps
            }
        }
        doc._pop_all_models_freeze();
        doc.set_title(json.title); // XXX!
        return doc;
    }
    replace_with_json(json) {
        const replacement = Document.from_json(json);
        replacement.destructively_move(this);
    }
    /** @deprecated */
    create_json_patch_string(events) {
        return JSON.stringify(this.create_json_patch(events));
    }
    create_json_patch(events) {
        for (const event of events) {
            if (event.document != this)
                throw new Error("Cannot create a patch using events from a different document");
        }
        const serializer = new Serializer();
        const events_repr = serializer.to_serializable(events);
        // TODO: We need a proper differential serializer. For now just remove known
        // definitions. We are doing this after a complete serialization, so that all
        // new objects are recorded.
        for (const model of this._all_models.values()) {
            serializer.remove_def(model);
        }
        return {
            events: events_repr,
            references: [...serializer.definitions],
        };
    }
    apply_json_patch(patch, buffers = new Map(), setter_id) {
        const references_json = patch.references;
        const events_json = patch.events;
        const references = Document._instantiate_references_json(references_json, this._all_models, this._resolver);
        if (!(buffers instanceof Map)) {
            buffers = new Map(buffers);
        }
        // The model being changed isn't always in references so add it in
        for (const event_json of events_json) {
            switch (event_json.kind) {
                case "RootAdded":
                case "RootRemoved":
                case "ModelChanged": {
                    const model_id = event_json.model.id;
                    const model = this._all_models.get(model_id);
                    if (model != null) {
                        references.set(model_id, model);
                    }
                    else if (!references.has(model_id)) {
                        logger.warn(`Got an event for unknown model ${event_json.model}"`);
                        throw new Error("event model wasn't known");
                    }
                    break;
                }
            }
        }
        // split references into old and new so we know whether to initialize or update
        const old_references = new Map(this._all_models);
        const new_references = new Map();
        for (const [id, value] of references) {
            if (!old_references.has(id))
                new_references.set(id, value);
        }
        Document._initialize_references_json(references_json, old_references, new_references, buffers);
        for (const event_json of events_json) {
            switch (event_json.kind) {
                case "MessageSent": {
                    const { msg_type, msg_data } = event_json;
                    let data;
                    if (msg_data === undefined) {
                        if (buffers.size == 1) {
                            const [[, buffer]] = buffers;
                            data = buffer;
                        }
                        else {
                            throw new Error("expected exactly one buffer");
                        }
                    }
                    else {
                        data = Document._resolve_refs(msg_data, old_references, new_references, buffers);
                    }
                    this._trigger_on_message(msg_type, data);
                    break;
                }
                case "ModelChanged": {
                    const patched_id = event_json.model.id;
                    const patched_obj = this._all_models.get(patched_id);
                    if (patched_obj == null) {
                        throw new Error(`Cannot apply patch to ${patched_id} which is not in the document`);
                    }
                    const attr = event_json.attr;
                    const value = Document._resolve_refs(event_json.new, old_references, new_references, buffers);
                    patched_obj.setv({ [attr]: value }, { setter_id });
                    break;
                }
                case "ColumnDataChanged": {
                    const column_source_id = event_json.column_source.id;
                    const column_source = this._all_models.get(column_source_id);
                    if (column_source == null) {
                        throw new Error(`Cannot stream to ${column_source_id} which is not in the document`);
                    }
                    const data = Document._resolve_refs(event_json.new, new Map(), new Map(), buffers);
                    if (event_json.cols != null) {
                        for (const k in column_source.data) {
                            if (!(k in data)) {
                                data[k] = column_source.data[k];
                            }
                        }
                    }
                    column_source.setv({ data }, { setter_id, check_eq: false });
                    break;
                }
                case "ColumnsStreamed": {
                    const column_source_id = event_json.column_source.id;
                    const column_source = this._all_models.get(column_source_id);
                    if (column_source == null) {
                        throw new Error(`Cannot stream to ${column_source_id} which is not in the document`);
                    }
                    if (!(column_source instanceof ColumnDataSource)) {
                        throw new Error("Cannot stream to non-ColumnDataSource");
                    }
                    const data = event_json.data;
                    const rollover = event_json.rollover;
                    column_source.stream(data, rollover, setter_id);
                    break;
                }
                case "ColumnsPatched": {
                    const column_source_id = event_json.column_source.id;
                    const column_source = this._all_models.get(column_source_id);
                    if (column_source == null) {
                        throw new Error(`Cannot patch ${column_source_id} which is not in the document`);
                    }
                    if (!(column_source instanceof ColumnDataSource)) {
                        throw new Error("Cannot patch non-ColumnDataSource");
                    }
                    const patches = event_json.patches;
                    column_source.patch(patches, setter_id);
                    break;
                }
                case "RootAdded": {
                    const root_id = event_json.model.id;
                    const root_obj = references.get(root_id);
                    this.add_root(root_obj, setter_id); // XXX: HasProps
                    break;
                }
                case "RootRemoved": {
                    const root_id = event_json.model.id;
                    const root_obj = references.get(root_id);
                    this.remove_root(root_obj, setter_id); // XXX: HasProps
                    break;
                }
                case "TitleChanged": {
                    this.set_title(event_json.title, setter_id);
                    break;
                }
                default:
                    throw new Error(`Unknown patch event ${JSON.stringify(event_json)}`);
            }
        }
    }
}
Document.__name__ = "Document";
//# sourceMappingURL=document.js.map