import { SessionChangedEvent } from "@pie-framework/pie-player-events";
import * as mr from "@pie-lib/math-rendering";
import { h } from "@stencil/core";
import { PieLoader, BundleType, DEFAULT_ENDPOINTS } from "../../pie-loader";
import { addRubric } from "../../rubric-utils";
import { normalizeContentElements } from "../../utils/utils";
import { VERSION } from "../../version";
const controllerErrorMessage = "Error processing question configuration, verify the question model?";
export class Player {
    constructor() {
        /**
         * Allows disabling of the default behaviour which is to look up and load the JS bundle that define the Custom Elements
         * used by the item config.
         * This if for advanced use cases when using the pie-player in a container that is managing loading of Custom Elements and Controllers.
         */
        this.disableBundler = false;
        this.elementsLoaded = false;
        /**
         * Simulates a correct response for the item.
         * This property will only have this effect if the `hosted` property is
         * false and player is running client-side-only.
         */
        this.addCorrectResponse = false;
        /**
         * In evaluate mode, add a bottom border to visually separate each item in the case of a multi-item
         */
        this.showBottomBorder = false;
        /**
         * The Pie Session
         */
        this.session = { id: "", data: [] };
        /**
         * Describes runtime environment for the player.
         *
         */
        this.env = { mode: "gather", role: "student" };
        /**
         * Indicates if player running in the context of a PIE hosting system.
         * Do not modify the default value for this property if you are not implementing a PIE host.
         * If true, the host is responsible for all model updates.
         */
        this.hosted = false;
        /**
         * If the item contains a stimulus, the player will render it by default.
         * Set this property to false to not render stimulus.
         */
        this.renderStimulus = true;
        this.version = VERSION;
        /**
         * Allow to resize pie-stimulus layout
         * Set this property to false to not render the resizer.
         */
        this.allowedResize = false;
        this.pieLoader = new PieLoader();
    }
    watchAddCorrectResponse(newValue, oldValue) {
        if (newValue !== oldValue) {
            this.updateModels();
        }
    }
    player() {
        return this.stimulusPlayer ? this.stimulusPlayer : this;
    }
    async watchConfig(newConfig) {
        this.elementsLoaded = false;
        try {
            if (!newConfig) {
                return;
            }
            try {
                if (typeof newConfig == "string") {
                    newConfig = JSON.parse(newConfig);
                }
                if (newConfig.pie) {
                    this.stimulusItemModel = newConfig;
                    return; // if stimulus item
                }
                else if (newConfig.elements) {
                    this.pieContentModel = addRubric(newConfig);
                    this.pieContentModel = normalizeContentElements(this.pieContentModel);
                }
                else {
                    this.playerError.emit(`invalid pie data model`);
                    return;
                }
            }
            catch (err) {
                this.playerError.emit(`exception processing content model - ${err.message}`);
                return;
            }
            if (!this.elementsLoaded && !this.disableBundler) {
                let endpoints = DEFAULT_ENDPOINTS.prod;
                if (this.bundleHost &&
                    ["dev", "stage", "prod"].includes(this.bundleHost)) {
                    endpoints = DEFAULT_ENDPOINTS[this.bundleHost];
                }
                if (this.bundleEndpoints) {
                    endpoints = this.bundleEndpoints;
                }
                await this.pieLoader.loadCloudPies({
                    content: this.pieContentModel,
                    doc: this.doc,
                    endpoints: endpoints,
                    bundle: this.hosted ? BundleType.player : BundleType.clientPlayer,
                    useCdn: false
                });
            }
        }
        catch (err) {
            this.playerError.emit(`problem loading item (${err})`);
        }
    }
    /**
     * For previewing changes to an item. Updates the model for one question in the item model.
     * @param update the updated model
     */
    async updateElementModel(update) {
        if (this.pieContentModel && this.pieContentModel.models) {
            const index = this.pieContentModel.models.findIndex(m => m.id === update.id);
            if (index !== -1) {
                this.pieContentModel.models.splice(index, 1, update);
            }
            await this.updateModels();
        }
    }
    async provideScore() {
        // TODO I think we have to check if it's hosted or not to expose this function
        if (!this.pieContentModel || !this.pieContentModel.models) {
            console.error('No pie content model');
            return false;
        }
        return Promise.all((this.pieContentModel.models || []).map(async (model) => {
            let pieEl = this.el.querySelector(`[id='${model.id}']`);
            !pieEl && (pieEl = this.el.querySelector(`[pie-id='${model.id}']`));
            const session = this.findOrAddSession(this.session.data, model.id);
            if (pieEl) {
                const controller = this.pieLoader.getController(pieEl.localName);
                if (controller && controller.outcome) {
                    return Object.assign(Object.assign({}, session), (await controller.outcome(model, session, { mode: 'evaluate' })));
                }
            }
        }));
    }
    updateModels(newEnv = this.env) {
        // wrapping a player in stimulus layout
        if (this.stimulusPlayer) {
            this.stimulusPlayer.env = newEnv;
            return;
        }
        if (this.pieContentModel &&
            this.pieContentModel.models &&
            typeof this.pieContentModel.models.forEach === "function" &&
            this.pieContentModel.markup &&
            this.elementsLoaded) {
            /**
             * Block session changed events while we set model/session on the elements.
             * TODO: The elements should *not* be firing 'session-changed' when the session is set.
             * They should only fire this if a user has made a change. Can we guarantee that?
             */
            this.el.addEventListener(SessionChangedEvent.TYPE, this.stopEventFromPropagating);
            this.pieContentModel.models.forEach(async (model, index) => {
                if (model && model.error) {
                    this.playerError.emit(`error loading question data`);
                    throw new Error(model.error);
                }
                const pieEl = this.el.querySelector(`[id='${model.id}']`);
                let session = this.findOrAddSession(this.session.data, model.id);
                if (pieEl) {
                    if (!this.hosted) {
                        try {
                            // use local controllers
                            const controller = this.pieLoader.getController(pieEl.localName);
                            if (controller) {
                                if (this.addCorrectResponse &&
                                    controller.createCorrectResponseSession &&
                                    typeof controller.createCorrectResponseSession === "function") {
                                    session = await controller.createCorrectResponseSession(model, Object.assign(Object.assign({}, newEnv), { role: "instructor" }));
                                }
                                pieEl.model = await controller.model(model, session, newEnv);
                            }
                            else {
                                // no controller provided
                                pieEl.model = model;
                            }
                        }
                        catch (err) {
                            this.playerError.emit(`${controllerErrorMessage}  -  (${err})`);
                        }
                    }
                    else {
                        if (model.error) {
                            this.playerError.emit(`${controllerErrorMessage}  -  '${model.error}'`);
                        }
                        pieEl.model = model;
                        if (this.addCorrectResponse) {
                            this.playerError.emit(`add-correct-response cannot be used in hosted environement`);
                        }
                    }
                    try {
                        pieEl.session = session;
                    }
                    catch (err) {
                        this.playerError.emit(`error setting item session value - ${err.message}`);
                    }
                }
            });
            setTimeout(() => {
                /** remove the event blocker - see above */
                this.el.removeEventListener(SessionChangedEvent.TYPE, this.stopEventFromPropagating);
                //TODO: is this needed anymore? maybe it should be using 'model-set' instead of 'session-changed' which is what it was using.
                this.loadComplete.emit();
            }, 150);
        }
    }
    stopEventFromPropagating(e) {
        e.stopPropagation();
    }
    async componentWillLoad() {
        if (this.config) {
            this.watchConfig(this.config);
        }
    }
    findOrAddSession(data, id) {
        const s = data.find(d => d.id === id);
        if (s) {
            return s;
        }
        const ss = { id };
        data.push(ss);
        return ss;
    }
    renderMath() {
        setTimeout(() => {
            mr.renderMath(this.el);
        }, 50);
    }
    addBottomBorder(tags) {
        if (!Array.isArray(tags)) {
            return;
        }
        tags.forEach(tag => {
            const elems = document.querySelectorAll(`${tag}`);
            for (const elem of elems) {
                if (elem && elem instanceof HTMLElement) {
                    elem.classList.add('evaluate-bottom-border');
                }
            }
        });
    }
    async afterRender() {
        if (this.pieContentModel && this.pieContentModel.markup) {
            if (this.elementsLoaded) {
                this.updateModels();
                this.renderMath();
                if (this.showBottomBorder && this.env.mode === 'evaluate') {
                    const pieTags = this.pieContentModel.elements && Object.keys(this.pieContentModel.elements);
                    this.addBottomBorder(pieTags);
                }
            }
            else {
                const elements = Object.keys(this.pieContentModel.elements).map(el => ({
                    name: el,
                    tag: el
                }));
                // Note: hard to verify but it appears that we need to resolve
                // the value first rather than setting the promise directly on
                // this state property - otherwise lifecycle re-render is triggered too early
                const loadedInfo = await this.pieLoader.elementsHaveLoaded(elements);
                if (loadedInfo.val &&
                    !!loadedInfo.elements.find(el => this.pieContentModel.elements[el.name])) {
                    this.elementsLoaded = true;
                }
            }
        }
    }
    async componentDidLoad() {
        await this.afterRender();
    }
    async componentDidUpdate() {
        await this.afterRender();
    }
    render() {
        if (this.stimulusItemModel) {
            return this.renderStimulus ? (h("pie-stimulus-layout", { allowedResize: this.allowedResize },
                h("div", { slot: "stimulus", class: 'player-stimulus-container' },
                    h("pie-player", { id: "stimulusPlayer", config: this.stimulusItemModel.passage, env: this.env, hosted: this.hosted, session: this.session, ref: el => (this.stimulusPlayer = el), bundleHost: this.bundleHost })),
                h("div", { slot: "item", class: 'player-item-container' },
                    h("pie-player", { id: "itemPlayer", addCorrectResponse: this.addCorrectResponse, config: this.stimulusItemModel.pie, env: this.env, hosted: this.hosted, session: this.session, bundleHost: this.bundleHost })))) : (h("pie-player", { id: "itemPlayer", addCorrectResponse: this.addCorrectResponse, config: this.stimulusItemModel.pie, env: this.env, hosted: this.hosted, session: this.session }));
        }
        else {
            if (this.elementsLoaded) {
                return (h("div", { class: 'player-container', innerHTML: this.pieContentModel && this.pieContentModel.markup
                        ? this.pieContentModel.markup
                        : "" }));
            }
            return h("pie-spinner", null);
        }
    }
    static get is() { return "pie-player"; }
    static get originalStyleUrls() { return {
        "$": ["../components.css"]
    }; }
    static get styleUrls() { return {
        "$": ["../components.css"]
    }; }
    static get properties() { return {
        "bundleHost": {
            "type": "string",
            "mutable": false,
            "complexType": {
                "original": "string",
                "resolved": "string",
                "references": {}
            },
            "required": false,
            "optional": true,
            "docs": {
                "tags": [],
                "text": "Optionally specifies the back-end that builds and hosts javascript bundles for rendering assessment items.\nThis property lets you choose which environment to use, from 'dev' , 'stage' or 'prod' environments.\nUntil 1.0 will default to 'stage'.\nStarting v2.0.3, it will temporarily redirect to production only"
            },
            "attribute": "bundle-host",
            "reflect": false
        },
        "bundleEndpoints": {
            "type": "unknown",
            "mutable": false,
            "complexType": {
                "original": "BundleEndpoints",
                "resolved": "BundleEndpoints",
                "references": {
                    "BundleEndpoints": {
                        "location": "import",
                        "path": "../../pie-loader"
                    }
                }
            },
            "required": false,
            "optional": true,
            "docs": {
                "tags": [],
                "text": "Provide this property override the default endpoints used by the player to retrieve JS\nbundles. Must be set before setting the config property.\nMost users will not need to use this property."
            }
        },
        "disableBundler": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "Allows disabling of the default behaviour which is to look up and load the JS bundle that define the Custom Elements\nused by the item config.\nThis if for advanced use cases when using the pie-player in a container that is managing loading of Custom Elements and Controllers."
            },
            "attribute": "disable-bundler",
            "reflect": false,
            "defaultValue": "false"
        },
        "config": {
            "type": "unknown",
            "mutable": false,
            "complexType": {
                "original": "ItemConfig",
                "resolved": "AdvancedItemConfig | PieContent",
                "references": {
                    "ItemConfig": {
                        "location": "import",
                        "path": "../../interface"
                    }
                }
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "The Pie config model."
            }
        },
        "addCorrectResponse": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "Simulates a correct response for the item.\nThis property will only have this effect if the `hosted` property is\nfalse and player is running client-side-only."
            },
            "attribute": "add-correct-response",
            "reflect": false,
            "defaultValue": "false"
        },
        "showBottomBorder": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "In evaluate mode, add a bottom border to visually separate each item in the case of a multi-item"
            },
            "attribute": "show-bottom-border",
            "reflect": false,
            "defaultValue": "false"
        },
        "session": {
            "type": "unknown",
            "mutable": false,
            "complexType": {
                "original": "ItemSession",
                "resolved": "{ id: string; data: any[]; }",
                "references": {
                    "ItemSession": {
                        "location": "import",
                        "path": "../../interface"
                    }
                }
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "The Pie Session"
            },
            "defaultValue": "{id: \"\", data: []}"
        },
        "env": {
            "type": "unknown",
            "mutable": false,
            "complexType": {
                "original": "Env",
                "resolved": "{ mode: String | Mode; role: String | Role; partialScoring?: boolean; }",
                "references": {
                    "Env": {
                        "location": "import",
                        "path": "../../interface"
                    }
                }
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "Describes runtime environment for the player."
            },
            "defaultValue": "{mode: \"gather\", role: \"student\"}"
        },
        "hosted": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": true,
            "docs": {
                "tags": [],
                "text": "Indicates if player running in the context of a PIE hosting system.\nDo not modify the default value for this property if you are not implementing a PIE host.\nIf true, the host is responsible for all model updates."
            },
            "attribute": "hosted",
            "reflect": false,
            "defaultValue": "false"
        },
        "renderStimulus": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "If the item contains a stimulus, the player will render it by default.\nSet this property to false to not render stimulus."
            },
            "attribute": "render-stimulus",
            "reflect": false,
            "defaultValue": "true"
        },
        "version": {
            "type": "string",
            "mutable": false,
            "complexType": {
                "original": "string",
                "resolved": "string",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": ""
            },
            "attribute": "version",
            "reflect": false,
            "defaultValue": "VERSION"
        },
        "allowedResize": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": true,
            "docs": {
                "tags": [],
                "text": "Allow to resize pie-stimulus layout\nSet this property to false to not render the resizer."
            },
            "attribute": "allowed-resize",
            "reflect": false,
            "defaultValue": "false"
        }
    }; }
    static get contextProps() { return [{
            "name": "doc",
            "context": "document"
        }]; }
    static get states() { return {
        "elementsLoaded": {},
        "pieContentModel": {},
        "stimulusItemModel": {}
    }; }
    static get events() { return [{
            "method": "sessionChanged",
            "name": "session-changed",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "Emmitted when any interaction in the set of interactions being rendered has\nbeen mutated by user action.\n\nThe `Event.detail` property contains a `complete` property.\nIf true, this indicates that enough data has been gathered by the interaciton to constitute a response.\nFor example, in a plot line questsion where a user had to plot three points to plot the line,\nthe `complete` propery would be false if 1 or 2 points had been added, but true if all three had."
            },
            "complexType": {
                "original": "any",
                "resolved": "any",
                "references": {}
            }
        }, {
            "method": "playerError",
            "name": "player-error",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "Emmitted if there is an error encountered while rendering.\n`event.detail` will be a string containing a message about the error."
            },
            "complexType": {
                "original": "any",
                "resolved": "any",
                "references": {}
            }
        }, {
            "method": "responseCompleted",
            "name": "responseCompleted",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "TODO - Emmitted when any all interactions in a PIE Assessment Item have reported that a user\nhas provided a response to the interaction."
            },
            "complexType": {
                "original": "any",
                "resolved": "any",
                "references": {}
            }
        }, {
            "method": "loadComplete",
            "name": "load-complete",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "Emitted when the content in the config has been loaded."
            },
            "complexType": {
                "original": "any",
                "resolved": "any",
                "references": {}
            }
        }]; }
    static get methods() { return {
        "updateElementModel": {
            "complexType": {
                "signature": "(update: PieModel) => Promise<void>",
                "parameters": [{
                        "tags": [{
                                "text": "update the updated model",
                                "name": "param"
                            }],
                        "text": "the updated model"
                    }],
                "references": {
                    "Promise": {
                        "location": "global"
                    },
                    "PieModel": {
                        "location": "import",
                        "path": "../../interface"
                    }
                },
                "return": "Promise<void>"
            },
            "docs": {
                "text": "For previewing changes to an item. Updates the model for one question in the item model.",
                "tags": [{
                        "name": "param",
                        "text": "update the updated model"
                    }]
            }
        },
        "provideScore": {
            "complexType": {
                "signature": "() => Promise<false | any[]>",
                "parameters": [],
                "references": {
                    "Promise": {
                        "location": "global"
                    },
                    "PieElement": {
                        "location": "import",
                        "path": "../../interface"
                    },
                    "PieController": {
                        "location": "import",
                        "path": "../../interface"
                    }
                },
                "return": "Promise<false | any[]>"
            },
            "docs": {
                "text": "",
                "tags": []
            }
        }
    }; }
    static get elementRef() { return "el"; }
    static get watchers() { return [{
            "propName": "addCorrectResponse",
            "methodName": "watchAddCorrectResponse"
        }, {
            "propName": "config",
            "methodName": "watchConfig"
        }, {
            "propName": "env",
            "methodName": "updateModels"
        }]; }
}
