import { __decorate } from "tslib";
import { html } from "lit";
import { HttpProject, Events as CoreEvents, HttpProjectKind, ProjectFolderKind, ProjectRequestKind, EnvironmentKind } from "@api-client/core/build/browser.js";
import { Patch } from "@api-client/json";
import { ApplicationScreen } from "../ApplicationScreen.js";
import pageStyles from './HttpProject.styles.js';
import globalStyles from '../../styles/global-styles.js';
import typography from '../../styles/m3/typography.module.js';
import dialogStyles from '../../styles/m3/dialog.module.js';
import surfaceStyles from '../../styles/m3/surface.module.js';
import { StoreBroadcast } from "../../http-client/store/StoreBroadcast.js";
import { Events } from "../../events/Events.js";
import { navigate } from "../../lib/route.js";
import { randomString } from "../../lib/Random.js";
import { query } from "../../lib/decorators.js";
import { HttpClientContextMenu } from "./HttpClientContextMenu.js";
import AppInfo from './AppInfo.js';
import { EventTypes } from "../../events/EventTypes.js";
import { SplitLayout } from "../../elements/layout/SplitLayout.js";
import { LayoutType } from "../../elements/layout/type.js";
import { RunnerKind } from "./types.js";
import '../../define/ui/ui-button.js';
import '../../define/ui/ui-icon.js';
import '../../define/http-project/project-navigation.js';
import '../../define/http-project/http-project-request.js';
import '../../define/http-project/http-project-request-history.js';
import '../../define/environment/environment-editor.js';
import '../../define/http-project/project-runner.js';
/**
 * Application screen that renders an HTTP project in the HTTP Client supporting local IDB store.
 */
export default class HttpProjectScreen extends ApplicationScreen {
    static get styles() {
        return [globalStyles, pageStyles, typography, dialogStyles, surfaceStyles];
    }
    static get routes() {
        return [
            { pattern: '/default', method: 'defaultRoute', fallback: true, name: 'HTTP Client home', title: 'HTTP Client home' },
            { pattern: '/404', method: 'e404Route', name: 'Not found', title: 'Not found' }
        ];
    }
    /**
     * The key of the workspace that has the project.
     */
    get workspace() {
        return this._workspace;
    }
    /**
     * The schema file key.
     */
    get key() {
        return this._key;
    }
    constructor(bindings) {
        super(bindings);
        this.mediaChannel = new BroadcastChannel(StoreBroadcast.media);
        this._workspace = '';
        this._key = '';
        /**
         * Own patches sent to the server.
         * These are removed after the sever event is received.
         */
        this.pendingPatches = new Map();
        /**
         * The list of patches made by the current user and received from the server since the beginning of this session
         * or page reload.
         * This is to used to recognize own patches so it won't process a patch that is already applied to the project.
         */
        this.livePatches = new Map();
        this.layout = new SplitLayout({
            dragTypes: ['text/kind', 'text/key'],
            constrain: true,
            stateCallback: this.handlerLayoutStore.bind(this),
        });
        /**
         * The key where the layout state is stored.
         */
        this.layoutStoreKey = '';
        this.uiMap = new Map();
        /**
         * A list of shared execution variables.
         * These are set by the core execution logic and distributed to the UI elements
         * that make HTTP request. These elements can freely change the sate and send it
         * to the execution.
         */
        this.sharedVariables = {};
        this.environmentSchemas = new Map();
        this.bindings.idb = bindings.idb;
        this.bindings.proxy = bindings.proxy;
        this.bindings.navigation = bindings.navigation;
        this.menu = new HttpClientContextMenu();
        this.menu.connect();
        this.contextMenuMutationCallback = this.contextMenuMutationCallback.bind(this);
        this.menu.store.set('callback', this.contextMenuMutationCallback);
        this.menu.store.set('layout', this.layout);
        this.menu.store.set('page', this);
    }
    async initialize() {
        if (!await this.isPlatformSupported()) {
            return;
        }
        try {
            this.setupWorkspaceParameters();
        }
        catch (e) {
            this.reportCriticalError(e.message);
            return;
        }
        await this.bindings.navigation.initialize();
        await this.bindings.idb.initialize();
        await this.bindings.store.initialize();
        await this.bindings.proxy.initialize();
        await this.bindings.config.initialize();
        await this.initializeConfig();
        await this.initializeStore();
        this.bindings.store.space = this.workspace;
        this.bindings.proxy.space = this.workspace;
        await this.setupProject(this.key);
        await this.bindings.store.observeFile(this.key, 'media');
        await this.bindings.store.observeHistory();
        await this.bindings.store.observeProjectExecutions();
        this.layoutStoreKey = `http-client.http-project.state.layout.${this.key}`;
        const state = await this.restoreLayout();
        this.layout.initialize(state);
        await this.loadRequestUiMeta();
        this.mediaChannel.addEventListener('message', this.mediaMessageHandler.bind(this));
        this.initializeRouting();
        this.initialized = true;
        this.layout.addEventListener('change', this.renderHandler.bind(this));
        this.layout.addEventListener('nameitem', this.nameLayoutItemHandler.bind(this));
        window.addEventListener(EventTypes.HttpProject.changed, () => this.contextMenuMutationCallback());
        window.addEventListener(EventTypes.HttpProject.State.nameChanged, this.handleProjectNameChange.bind(this));
        window.addEventListener(EventTypes.Http.Request.rename, this.handleRenameRequest.bind(this));
    }
    async storeChanged() {
        this.reportCriticalError('You are no longer connected to the data store. Editing is disabled.');
    }
    setupWorkspaceParameters() {
        const url = new URL(window.location.href);
        const key = url.searchParams.get('key');
        const space = url.searchParams.get('space');
        if (key) {
            this._key = key;
        }
        else {
            throw new Error(`The project key is not set. Go back to the start page.`);
        }
        if (space) {
            this._workspace = space;
        }
        else {
            throw new Error(`The space key is not set. Go back to the start page.`);
        }
    }
    async setupProject(key) {
        try {
            const media = await this.readProjectFile(key);
            this.schema = JSON.stringify(media);
            this.project = new HttpProject(media);
            this.menu.store.set('project', this.project);
        }
        catch (e) {
            const cause = e;
            if (cause.code === 404) {
                navigate('404');
            }
            CoreEvents.Telemetry.exception(cause.message, true);
            this.reportCriticalError(cause.message);
        }
    }
    async readProjectFile(key) {
        const media = await this.bindings.store.media.read(key);
        return media;
    }
    async storePatch(key, id, value, app) {
        const info = {
            app: app.code,
            appVersion: app.version,
            patch: value,
            id,
        };
        await this.bindings.store.media.patch(key, info);
    }
    /**
     * A handler for the broadcast channel message which informs about changes to files media.
     */
    mediaMessageHandler(e) {
        const event = e.data;
        if (event.space !== this.workspace) {
            return;
        }
        this.processChangeFileEvent(event);
    }
    /**
     * Apples changes to the Project file.
     * @param event The backend event received from the store.
     */
    processChangeFileEvent(event) {
        const { schema, project } = this;
        if (!schema || !project || this.key !== event.key || event.kind !== HttpProjectKind) {
            return;
        }
        if (event.operation === 'patch') {
            this.processFilePatch(event.data);
        }
    }
    /**
     * Logic run when a patch operation is reported from the backend.
     * @param rev The revision info from the backend event
     */
    processFilePatch(rev) {
        const { schema, project } = this;
        if (!schema || !project) {
            return;
        }
        const iData = JSON.parse(schema);
        const { id } = rev;
        const ownPatch = this.pendingPatches.get(id);
        if (ownPatch) {
            this.livePatches.set(id, rev);
            this.pendingPatches.delete(id);
            // we still need to propagate changes to the requests.
            this.applyRequestPatches(rev.patch);
            return;
        }
        const result = Patch.apply(iData, rev.patch);
        this.schema = JSON.stringify(result.doc);
        project.new(result.doc);
        this.render();
        const { nav } = this;
        if (nav) {
            nav.requestUpdate();
        }
        this.applyRequestPatches(rev.patch);
    }
    applyRequestPatches(patch) {
        const { project } = this;
        if (!project) {
            return;
        }
        patch.forEach((info) => {
            if (info.path.startsWith('/definitions/environments')) {
                // environments changed.
                const panels = document.querySelectorAll(`http-project-request`);
                Array.from(panels).forEach(p => p.applyPatch(info));
                return;
            }
            if (!info.path.startsWith('/definitions/requests/')) {
                return;
            }
            // "/definitions/requests/\d+/..."
            const parts = info.path.replace('/definitions/requests/', '').split('/');
            const index = Number(parts[0]);
            if (Number.isNaN(index)) {
                return;
            }
            const r = project.definitions.requests[index];
            if (!r) {
                return;
            }
            const panel = document.querySelector(`http-project-request[data-key="${r.key}"]`);
            if (panel) {
                panel.applyPatch(info);
            }
        });
    }
    /**
     * A reference to this function is passed to the context menu
     * and it is called when a project mutates.
     */
    contextMenuMutationCallback() {
        this.render();
        this.nav?.requestUpdate();
        this.updateProject();
    }
    handleProjectNameChange(e) {
        const { key } = e.detail;
        this.layout.requestNameUpdate(key);
        const { nav } = this;
        if (nav) {
            nav.requestUpdate();
        }
    }
    handleRenameRequest(e) {
        const { key, name } = e.detail;
        if (!key || !name) {
            return;
        }
        const { project } = this;
        if (!project) {
            return;
        }
        const request = project.findRequest(key);
        if (!request) {
            return;
        }
        request.info.name = name;
        this.layout.requestNameUpdate(key);
        const { nav } = this;
        if (nav) {
            nav.requestUpdate();
        }
        this.updateProject();
    }
    /**
     * Triggers a save action on one of the workspace items.
     *
     * What happens next depends on the type of the item.
     *
     * Note, this is used by the context menu to handle the "save" action.
     *
     * @param key The key of the item to trigger the save action on.
     */
    triggerSave(key) {
        // eslint-disable-next-line no-console
        console.log('trigger save', key);
        // const item = this.workspace.items.find(i => i.key === key);
        // if (!item) {
        //   return;
        // }
        // switch (item.kind) {
        //   case EnvironmentKind: this._triggerEnvironmentSave(item); break;
        //   default:
        // }
    }
    openStart() {
        Events.Navigation.App.runHttpClient({ space: this.workspace });
    }
    resetRoute() {
        this.page = undefined;
    }
    defaultRoute() {
        this.resetRoute();
        this.page = 'default';
    }
    e404Route() {
        this.resetRoute();
        this.page = '404';
    }
    /**
     * The main function to call when updating the project in the store.
     * It takes care of the concurrent updates, patch building, and IO.
     */
    async updateProject() {
        if (this.updatingProject) {
            this.isDirty = true;
            return;
        }
        const { schema, project } = this;
        if (!project || !schema) {
            // eslint-disable-next-line no-console
            console.warn(`Trying to update the project but none is set.`);
            return;
        }
        const newSchema = project.toJSON();
        const diff = Patch.diff(JSON.parse(schema), newSchema);
        if (!diff.length) {
            // eslint-disable-next-line no-console
            console.warn(`Expected to detect changes to the project schema.`);
            return;
        }
        this.updatingProject = true;
        // gives 2.092279e+13 permutations.
        const id = randomString(16);
        this.pendingPatches.set(id, diff);
        try {
            await this.storePatch(project.key, id, diff, AppInfo);
            this.schema = JSON.stringify(newSchema);
        }
        catch (e) {
            const cause = e;
            CoreEvents.Telemetry.exception(cause.message, true);
            this.reportCriticalError(cause.message);
        }
        finally {
            this.updatingProject = false;
            if (this.isDirty) {
                this.updateProject();
            }
        }
    }
    /**
     * An event handler that renders the view on request.
     */
    renderHandler() {
        this.render();
    }
    nameLayoutItemHandler(event) {
        const item = event.detail;
        if (item.kind === ProjectFolderKind) {
            const folder = this.project?.findFolder(item.key);
            if (folder && folder.info.name) {
                item.label = folder.info.name;
            }
            else {
                item.label = 'Folder';
            }
            item.icon = 'folder';
        }
        else if (item.kind === ProjectRequestKind) {
            const request = this.project?.findRequest(item.key);
            if (request && request.info.name && request.info.name !== 'http://') {
                item.label = request.info.name;
            }
            else if (request && request.expects.url) {
                item.label = request.expects.url;
            }
            else {
                item.label = 'HTTP request';
            }
            item.icon = 'request';
        }
        else if (item.kind === EnvironmentKind) {
            const env = this.project?.findEnvironment(item.key);
            if (env && env.info.name) {
                item.label = env.info.name;
            }
            else {
                item.label = 'Environment';
            }
            item.icon = 'environment';
        }
    }
    requestChangeHandler(e) {
        const node = e.target;
        const key = node.dataset.key;
        if (!key) {
            return;
        }
        this.layout.requestNameUpdate(key);
        this.updateProject();
    }
    handleRequestUiChange(e) {
        const editor = e.target;
        const { ui, key } = editor;
        if (!ui || !key) {
            return;
        }
        this.uiMap.set(key, ui);
        Events.HttpProject.Ui.HttpRequest.set(this.key, key, ui);
    }
    async loadRequestUiMeta() {
        const ids = [];
        for (const [key, value] of this.layout.definitions) {
            if (value.type === LayoutType.item) {
                const item = value.value;
                if (item.kind === ProjectRequestKind) {
                    ids.push(key);
                }
            }
        }
        if (!ids.length) {
            return;
        }
        const result = await Events.HttpProject.Ui.HttpRequest.getBulk(this.key, ids);
        result?.items.forEach((item, index) => {
            if (!item) {
                return;
            }
            this.uiMap.set(ids[index], item);
        });
    }
    environmentChangeHandler(e) {
        const { project } = this;
        if (!project) {
            return;
        }
        const node = e.target;
        const key = node.dataset.key;
        if (!key) {
            return;
        }
        const schema = this.environmentSchemas.get(key);
        if (!schema) {
            return;
        }
        const environment = project.definitions.environments.find(i => i.key === key);
        if (environment) {
            environment.new(schema);
        }
        else {
            project.addEnvironment(schema);
        }
        this.layout.requestNameUpdate(key);
        this.updateProject();
    }
    handleNavigationOpen(event) {
        const e = event;
        const key = e.detail.key;
        const kind = e.detail.kind;
        if (!key || !kind) {
            return;
        }
        const { layout } = this;
        const panel = layout.findPanel(key);
        if (panel) {
            panel.selected = key;
            layout.updateView(panel.key);
            this.render();
            return;
        }
        switch (kind) {
            // case ProjectFolderKind: this._openProjectFolder(key, pid as string); break;
            case ProjectRequestKind:
                this.openProjectRequest(key);
                break;
            case EnvironmentKind:
                this.openProjectEnvironment(key);
                break;
            default:
        }
    }
    async openProjectRequest(key) {
        const { project } = this;
        if (!project) {
            return;
        }
        const request = project.findRequest(key);
        if (!request) {
            this.reportCriticalError(`The request was not found in the project.`);
            return;
        }
        const ui = await Events.HttpProject.Ui.HttpRequest.get(project.key, key);
        if (ui) {
            this.uiMap.set(key, ui);
        }
        else {
            this.uiMap.set(key, {});
        }
        const parent = request.getParent();
        const parentKey = parent === project ? undefined : parent?.key;
        this.layout.addItem({
            kind: ProjectRequestKind,
            key,
            label: request.info.renderLabel,
            icon: 'request',
            parent: parentKey,
        });
    }
    openProjectEnvironment(key) {
        const { project } = this;
        if (!project) {
            return;
        }
        const environment = project.findEnvironment(key);
        if (!environment) {
            this.reportCriticalError(`The environment was not found in the project.`);
            return;
        }
        const parent = project.findParent(environment.key);
        const parentKey = parent === project ? undefined : parent?.key;
        this.layout.addItem({
            kind: EnvironmentKind,
            key,
            label: environment.info.renderLabel,
            icon: 'environment',
            parent: parentKey,
        });
    }
    /**
     * Opens a new tab (or focuses on an existing tab) with a Project runner.
     * @param parentFolder The key of the parent folder to open the runner for.
     */
    openRunner(parentFolder) {
        const { project } = this;
        if (!project) {
            return;
        }
        const keyInfo = ['runner'];
        let label;
        let icon;
        if (parentFolder) {
            keyInfo.push('folder');
            keyInfo.push(parentFolder);
            const folder = project.findFolder(parentFolder);
            if (!folder) {
                this.reportCriticalError(`The folder was not found in the project.`);
                return;
            }
            label = `Runner: ${folder.info.renderLabel}`;
            icon = 'folder';
        }
        else {
            keyInfo.push('project');
            keyInfo.push(project.key);
            label = `Runner: project`;
            icon = 'project';
        }
        const key = keyInfo.join('|');
        this.layout.addItem({
            kind: RunnerKind,
            key,
            label,
            icon,
            parent: parentFolder,
        });
    }
    async handlerLayoutStore(state) {
        const { layoutStoreKey } = this;
        try {
            await Events.Config.Local.set(layoutStoreKey, state);
        }
        catch (_) {
            // ...
        }
    }
    async restoreLayout() {
        try {
            const data = await Events.Config.Local.get(this.layoutStoreKey);
            if (data) {
                return data;
            }
        }
        catch (e) {
            // 
        }
        return undefined;
    }
    handleRunnerChange(e) {
        const target = e.target;
        const { config } = target;
        const { key } = target.dataset;
        if (!config || !key) {
            return;
        }
        const item = this.layout.findItem(key);
        if (!item) {
            return;
        }
        item.value = config;
        this.layout.scheduleStore();
    }
    pageTemplate() {
        const { initialized } = this;
        if (!initialized) {
            return super.pageTemplate();
        }
        return html `
      ${this.navigationTemplate()}
      ${this.mainTemplate()}
    `;
    }
    // protected headerTemplate(): TemplateResult {
    //   const { project } = this;
    //   const projectTitle = project && project.info && project.info.name;
    //   const title = projectTitle || 'HTTP Project';
    //   return html`
    //   <header class="start-page-header">
    //     <h1 class="title-large">${title}</h1>
    //     <div class="project-global-actions">
    //       <ui-button type="outlined" class="toolbar-action">
    //         <ui-icon icon="playArrow"></ui-icon> Run
    //       </ui-button>
    //     </div>
    //   </header>
    //   `;
    // }
    e404Template() {
        return html `
    <main>
      <div class="full-error">
        <h2 class="headline-large">Not found</h2>
        <p class="body-large">
          The project you are trying to open does not exist or was removed.
        </p>
        <ui-button type="filled" @click="${this.openStart}">Back to start</ui-button>
      </div>
    </main>`;
    }
    navigationTemplate() {
        return html `
    <project-navigation .project="${this.project}" @select="${this.handleNavigationOpen}" class="nav"></project-navigation>
    `;
    }
    mainTemplate() {
        const { page } = this;
        if (page === '404') {
            return this.e404Template();
        }
        return html `<main>
      <div class="content">
        ${this.renderLayout()}
      </div>
    </main>`;
    }
    renderLayout() {
        const { layout } = this;
        if (layout.isEmpty()) {
            return [this.renderEmptyLayout()];
        }
        return layout.render(this.renderItem.bind(this));
    }
    renderEmptyLayout() {
        return html `
    <div class="empty-layout surface1">
      <p class="headline-small">Open an item from the navigation.</p>
    </div>
    `;
    }
    renderItem(item, visible) {
        switch (item.kind) {
            case EnvironmentKind: return this.renderEnvironment(item, visible);
            case ProjectRequestKind: return this.renderProjectRequest(item, visible);
            case RunnerKind: return this.renderProjectRunner(item, visible);
            default: return html `<p ?hidden="${!visible}" data-key="${item.key}" >Unsupported object: ${item.kind}</p>`;
        }
    }
    renderProjectRequest(item, visible) {
        const { key } = item;
        const { project, uiMap } = this;
        const request = project && project.findRequest(key);
        if (!request) {
            return html `
      <p ?hidden="${!visible}" data-key="${key}" class="missing-data">The request is no longer in the project.</p>
      `;
        }
        const ui = (uiMap.get(item.key) || {});
        return html `
    <div class="request-container surface1" ?hidden="${!visible}" data-key="${key}">
      <div class="scrollable">
        <http-project-request  
          .sharedVariables="${this.sharedVariables}"
          data-key="${key}" 
          key="${key}" 
          .project="${project}" 
          .appInfo="${AppInfo}"
          .ui="${ui}"
          useNetStore
          @change="${this.requestChangeHandler}"
          @uichange="${this.handleRequestUiChange}"
        ></http-project-request>
  
        <div class="response-wrapper surface2">
          <http-project-request-history 
            .project="${this.project && this.project.key}" 
            .request="${key}"
          ></http-project-request-history>
        </div>
      </div>
    </div>
    `;
    }
    renderEnvironment(item, visible) {
        const { key } = item;
        let schema = this.environmentSchemas.get(key);
        if (!schema) {
            const { project } = this;
            const env = project && project.findEnvironment(key);
            if (env) {
                schema = env.toJSON();
                this.environmentSchemas.set(key, schema);
            }
        }
        if (!schema) {
            return html `
      <p ?hidden="${!visible}" data-key="${key}" class="missing-data">The environment is no longer in the project.</p>
      `;
        }
        return html `
    <div class="environment-editor-content surface1" ?hidden="${!visible}" data-key="${key}">
      <div class="scrollable">
        <environment-editor .environment="${schema}"  data-key="${key}" @change="${this.environmentChangeHandler}"></environment-editor>
      </div>
    </div>
    `;
    }
    renderProjectRunner(item, visible) {
        const { key, parent } = item;
        const config = item.value;
        return html `
    <div class="project-runner-content surface1" ?hidden="${!visible}" data-key="${key}">
      <div class="scrollable">
        <project-runner
          .sharedVariables="${this.sharedVariables}"
          .project="${this.project}" 
          .appInfo="${AppInfo}"
          .folder="${parent}"
          .config="${config}"
          data-key="${key}"
          @change="${this.handleRunnerChange}"
        ></project-runner>
      </div>
    </div>
    `;
    }
}
__decorate([
    query('project-navigation')
], HttpProjectScreen.prototype, "nav", void 0);
//# sourceMappingURL=HttpProject.screen.js.map