import { CreatorUI } from './ui/creator-ui.js';
import { Highlighter } from './ui/highlighter.js';
import { ToolbarEntryPosition } from '../../elements/kc-workspace-toolbar/entry.js';
import { downloadFile } from '../util/file.js';
import { createChallenge } from '../challenge/index.js';
import { CreatorDevTools } from './dev.js';
import { dataURI } from '@kano/icons-rendering/index.js';
import { download } from './ui/icons.js';
import { dispose } from '@kano/common/index.js';
import { ContributionManager } from '../contribution.js';
export const CopyGenerators = new ContributionManager();
export class CreatorWidget {
    constructor() {
        this.domNode = new CreatorUI();
    }
    getDomNode() {
        return this.domNode;
    }
    getPosition() {
        return null;
    }
    layout() {
        this.domNode.style.bottom = '0px';
        this.domNode.style.left = '0px';
        this.domNode.style.right = '0px';
    }
}
const VERSION = '1.0.0';
export class Creator {
    constructor(editor, opts = { copyGenerator: 'default' }) {
        this.ui = new CreatorWidget();
        this.highlighter = new Highlighter();
        this.stepsMap = new Map();
        this.whitelist = {};
        this.partsWhitelist = {};
        this.partsList = [];
        this.challenge = null;
        this.previewStepper = null;
        this.devTools = new CreatorDevTools();
        this.aliasCounter = -1;
        this.editor = editor;
        this.subscriptions = [];
        this.stepper = this.createStepper();
        this.stepper.developmentMode = true;
        if (this.editor.injected) {
            this.onInject();
        }
        else {
            this.editor.onDidInject(() => this.onInject(), this, this.subscriptions);
        }
        this.ui.domNode.onDidFocusStep((step) => this.focusTarget(step.source), this, this.subscriptions);
        this.ui.domNode.onDidPlayStep((step) => this.playStep(step), this, this.subscriptions);
        this.ui.domNode.onDidSelectStep((step) => this.selectStep(step), this, this.subscriptions);
        this.ui.domNode.onDidBlurStep(() => this.blurTarget(), this, this.subscriptions);
        this.ui.domNode.onDidClickChallengeToggle((step) => this.toggleMode(step), this, this.subscriptions);
        this.ui.domNode.onDidSelectFile((path) => this.selectFile(path), this, this.subscriptions);
        this.ui.domNode.onDidClickPrevious(() => this.previousStep(), this, this.subscriptions);
        this.ui.domNode.onDidClickNext(() => this.nextStep(), this, this.subscriptions);
        const copyGenerators = CopyGenerators.get(this.editor.sourceType);
        if (!copyGenerators) {
            throw new Error(`Could not instantiate creator: Copy generator for source type '${this.editor.sourceType}' was not imported`);
        }
        const generatorId = opts.copyGenerator || 'default';
        const copyGenerator = copyGenerators.get(generatorId);
        if (!copyGenerator) {
            throw new Error(`Could not instantiate creator: Copy generator with id '${generatorId}' was not imported`);
        }
        this.copyGenerator = copyGenerator;
        this.watchCodeChanges();
    }
    createChallenge(data) {
        return createChallenge(this.editor, data);
    }
    createAlias(prefix = 'block') {
        this.aliasCounter += 1;
        return `${prefix}_${this.aliasCounter}`;
    }
    watchCodeChanges() {
        this.codeChangesSub = this.editor.sourceEditor.onDidCodeChange(() => {
            this.onCodeChanged();
        });
    }
    unwatchCodeChanges() {
        if (this.codeChangesSub) {
            this.codeChangesSub.dispose();
        }
    }
    onCodeChanged() {
        if (this.ui.domNode.mode === 'play') {
            return;
        }
        this.app = this.editor.save();
        const challenge = this.generate();
        this.generatedSteps = challenge.steps;
        this.ui.domNode.setStepData(this.generatedSteps);
        this.ui.domNode.title = challenge.id || 'untitled';
    }
    generate() {
        const parts = this.editor.output.parts.getParts();
        const steps = [];
        parts.forEach((part) => {
            const type = part.constructor.type;
            const step = {
                source: `part#${part.id}`,
                data: {
                    type: 'create-part',
                    alias: this.createAlias('part'),
                    part: type,
                    openPartsCopy: this.getCopy('openParts', `part.${type}`),
                },
            };
            steps.push(step);
            this.stepsMap.set(step.source, step);
            this.editor.registerAlias(step.data.alias, step.source);
        });
        return { id: '', name: '', steps, defaultApp: '{}' };
    }
    generateChallenge() {
        this.whitelist = {};
        this.partsWhitelist = {};
        this.partsList = [];
        const challenge = this.generate();
        const steps = challenge.steps.map((generatedStep) => generatedStep.data);
        return {
            version: VERSION,
            id: challenge.id,
            name: challenge.name,
            defaultApp: challenge.defaultApp,
            steps,
            whitelist: this.whitelist,
            partsWhitelist: this.partsWhitelist,
            parts: this.partsList,
        };
    }
    addToWhitelist(category, id) {
        if (!id) {
            return;
        }
        const blocksOfType = this.whitelist[category];
        let alreadyInWhitelist = false;
        if (!blocksOfType) {
            this.whitelist[category] = [];
        }
        else {
            alreadyInWhitelist = this.whitelist[category].indexOf(id) >= 0;
        }
        if (!alreadyInWhitelist) {
            this.whitelist[category].push(id);
        }
    }
    addToPartsList(partType, blockId) {
        if (!partType || !blockId) {
            return;
        }
        const alreadyInWhitelist = this.partsList.indexOf(partType) >= 0;
        if (!alreadyInWhitelist) {
            this.partsList.push(partType);
            this.partsWhitelist[partType] = [];
        }
        const alreadyInPartWhitelist = this.partsWhitelist[partType].indexOf(blockId) >= 0;
        if (!alreadyInPartWhitelist) {
            this.partsWhitelist[partType].push(blockId);
        }
    }
    loadChallenge(d) {
        // Copy the current state, will be used to re-apply the step after everything is re-loaded
        const previousMode = this.ui.domNode.mode;
        const previousIndex = this.ui.domNode.selectedStepIndex;
        // Leave the play mode, resets everything
        this.editStep();
        // Update the local copy of the challenge
        this.loadedChallenge = d;
        // Simulate the whole challenge
        this.stepper.stepTo(Infinity, this.loadedChallenge);
        if (previousMode === 'play') {
            // The user was previewing a step, trigger a challenge generation and jump to that step
            this.onCodeChanged();
            this.ui.domNode.selectedStepIndex = previousIndex;
            this.playStep(this.ui.domNode.selectedStep);
        }
    }
    focusTarget(source) {
        const el = this.editor.queryElement(source);
        if (!el) {
            return;
        }
        this.highlighter.highlight(el);
    }
    blurTarget() {
        this.highlighter.clear();
    }
    selectFile(path) {
        this.devTools.openFile(path);
    }
    onInject() {
        this.editor.addContentWidget(this.ui);
        this.setupDownloadButton();
        this.registerKeybindings();
        this.setupDevTools();
    }
    /**
     * Add a download button to the editor's workpscae toolbar.
     * This button simply generate the challenge and triggers a download
     */
    setupDownloadButton() {
        const downloadButton = this.editor.workspaceToolbar.addEntry({
            id: 'download-challenge',
            position: ToolbarEntryPosition.RIGHT,
            icon: dataURI(download),
        });
        downloadButton.onDidActivate(() => {
            const challengeSource = this.generateChallenge();
            downloadFile(`${challengeSource.id || 'untitled-challenge'}.kch`, JSON.stringify(challengeSource, null, '    '));
        });
        this.subscriptions.push(downloadButton);
    }
    setupDevTools() {
        // The selected file changed, reload the challenge
        this.devTools.onDidChangeFile((data) => this.loadChallenge(data), this, this.subscriptions);
        // The list of available files changed, update the list in the UI
        this.devTools.onDidUpdateFiles((files) => {
            this.ui.domNode.files = files;
        }, this, this.subscriptions);
        // THe connection status changed, let the UI know
        this.devTools.onDidConnectionStatusChange((connected) => {
            this.ui.domNode.offline = !connected;
        }, this, this.subscriptions);
        // Try to connect immediately
        this.devTools.connect();
    }
    playStep(step) {
        this.ui.domNode.mode = 'play';
        // Hide any leftover highlight from hovering
        this.highlighter.clear();
        // Clear up previous challenge and stepper
        if (this.challenge) {
            this.challenge.stop();
            this.challenge.dispose();
        }
        if (this.previewStepper) {
            this.previewStepper.dispose();
        }
        // Create a new stepper
        this.previewStepper = this.createStepper();
        // Load the app being edited
        this.editor.load(this.app);
        // Generate the challenge from the app
        const data = this.generateChallenge();
        // Create a challenge instance to go thorugh the steps
        this.challenge = this.createChallenge(data);
        this.challenge.reset();
        // Get the index of the step we want to preview
        const stepIndex = this.generatedSteps.indexOf(step);
        const engine = this.challenge.engine;
        // Find the index of the real step that will be previewed
        const realIndex = engine.getExpandedStepIndex(stepIndex);
        // Simulate the user goinf through all steps until the target one
        this.previewStepper.stepTo(realIndex, data);
        // Start the challenge
        this.challenge.start();
        // Jump to the specific step
        engine.stepIndex = realIndex;
        // Watch step changes to update the UI accordingly
        this.challenge.engine.onDidUpdateStepIndex((index) => {
            // Retrieve the source step using the stepper's mappings
            if (this.previewStepper && this.previewStepper.mappings) {
                const originalIndex = this.previewStepper.mappings.get(index);
                if (typeof originalIndex !== 'undefined') {
                    this.ui.domNode.selectedStepIndex = originalIndex;
                }
            }
        });
    }
    registerKeybindings() {
        // The left and right chevrons jump through steps
        this.subscriptions.push(this.editor.keybindings.register('>', () => this.nextStep()), this.editor.keybindings.register('<', () => this.previousStep()));
    }
    nextStep() {
        this.ui.domNode.selectedStepIndex = Math.min(this.ui.domNode.selectedStepIndex + 1, this.ui.domNode.generatedSteps.length - 1);
        if (this.ui.domNode.mode === 'edit') {
            return;
        }
        this.playStep(this.ui.domNode.selectedStep);
    }
    previousStep() {
        this.ui.domNode.selectedStepIndex = Math.max(this.ui.domNode.selectedStepIndex - 1, 0);
        if (this.ui.domNode.mode === 'edit') {
            return;
        }
        this.playStep(this.ui.domNode.selectedStep);
    }
    selectStep(step) {
        if (this.ui.domNode.mode === 'play') {
            this.playStep(step);
        }
    }
    toggleMode(step) {
        if (this.ui.domNode.mode === 'edit') {
            this.playStep(step);
        }
        else {
            this.editStep();
        }
    }
    editStep() {
        if (this.challenge) {
            this.challenge.stop();
            this.challenge.dispose();
            this.challenge = null;
        }
        if (this.previewStepper) {
            this.previewStepper.dispose();
            this.previewStepper = null;
        }
        this.editor.load(this.app);
        this.ui.domNode.mode = 'edit';
    }
    getCopy(type, ...args) {
        if (typeof this.copyGenerator[type] !== 'function') {
            throw new Error(`Could not generate copy: Generator for '${type}' does not exist`);
        }
        return this.copyGenerator[type](...args);
    }
    dispose() {
        this.editor.removeContentWidget(this.ui);
        dispose(this.subscriptions);
        this.subscriptions.length = 0;
        this.unwatchCodeChanges();
    }
}
//# sourceMappingURL=creator.js.map