/* eslint-disable @typescript-eslint/ban-ts-comment */
import { html, TemplateResult } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { AmfDefinitions } from '@api-client/core/build/browser.js';
import { editor } from 'monaco-editor';
import { ApiEditorPreviousRequest } from '../../dev/types.js';
import { AmfEventTypes, AmfTypes, ApiNavigationEvent, ApplicationScreen, DomEventsAmfStore, MonacoLoader, MonacoTheme } from '../../src/index.js';
import { reactive } from '../../src/lib/decorators.js';
import MonacoStyles from '../../src/monaco/Monaco.styles.js';
import * as ApiExamples from '../lib/amf/ApiExamples.js';
import '../../src/define/amf/api-navigation.js';
import '../../src/define/amf/api-documentation.js';
import '../../src/define/ui/ui-icon.js';

const StorePrefix = 'ApiEditor.Value.';
const ApiFormatKey = 'ApiEditor.Format';
const ApiMediaKey = 'ApiEditor.Media';

/* global monaco */


try {
  document.adoptedStyleSheets = document.adoptedStyleSheets.concat(MonacoStyles.styleSheet as CSSStyleSheet);
} catch (_) {
  /* istanbul ignore next */
  const s = document.createElement('style');
  s.innerHTML = MonacoStyles.cssText;
  document.getElementsByTagName('head')[0].appendChild(s);
}

export class ApiTextEditor extends ApplicationScreen {
  storeChanged(): Promise<void> {
    throw new Error('Method not implemented.');
  }

  store: DomEventsAmfStore;

  /** 
   * Whether the application navigation is visible, when toggling.
   */
  @reactive() navigationOpened: boolean;

  /**
   * When set the application is loading an API model.
   */
  @reactive() loading: boolean;

  /**
   * The loaded AMF model form the demo APIs.
   */
  @reactive() model?: AmfDefinitions.IAmfDocument;
  
  @reactive() apiId?: string;

  /**
   * The domain id of the currently selected graph object.
   */
  @reactive() domainId: string;

  /**
   * The navigation type of the currently selected graph object.
   */
  @reactive() domainType: AmfTypes.SelectionType;

  /**
   * When the current domain type is an operation this is the selected operation domain id.
   */
  @reactive() operationId?: string;

  @reactive() apiFormat: string;

  @reactive() apiMedia: string;

  requests: ApiEditorPreviousRequest[];

  /** 
   * Whether the HTTP editor is opened.
   */
  @reactive() editorOpened: boolean;

  /** 
   * The domain id of the operation opened in the HTTP editor.
   */
  @reactive() editorOperation?: string;

  httpEditorPositionTarget?: HTMLElement | null;

  value?: string;

  monacoInstance?: editor.IStandaloneCodeEditor;

  changeTimeout?: number;

  constructor() {
    super();
    this.store = new DomEventsAmfStore(window);
    this.store.listen();
    
    this.loading = false;
    this.navigationOpened = false;
    this.domainId = 'summary';
    this.domainType = 'summary';
    this.apiFormat = 'RAML 1.0';
    this.apiMedia='application/yaml';
    this.requests = [];
    this.editorOpened = false;
    
    window.addEventListener(AmfEventTypes.Navigation.apiNavigate, this.apiNavigationHandler.bind(this));
    this.initialize();
  }

  /**
   * Loads the Monaco editor and initializes routing.
   */
  async initialize(): Promise<void> {
    await this.loadMonaco();
    await this.restoreSession();
    this.initializeRouting();
    this.initialized = true;
    this.render();
    await this.updateComplete;
    this.httpEditorPositionTarget = document.body.querySelector('aside');
    this.initializeEditor();
  }

  /**
   * Loads the Monaco editor using ARC's helpers.
   * This can be implemented in any way.
   */
  async loadMonaco(): Promise<void> {
    const base = new URL('../../node_modules/monaco-editor/', import.meta.url).toString();
    MonacoLoader.createEnvironment(base);
    await MonacoLoader.loadMonaco(base);
    await MonacoLoader.monacoReady();
  }

  /**
   * Restores previously stored session.
   */
  async restoreSession(): Promise<void> {
    let format = localStorage.getItem(ApiFormatKey);
    if (!format) {
      format = 'OAS 3.0';
    }
    let mime = localStorage.getItem(ApiMediaKey);
    if (!mime) {
      mime = 'application/yaml';
    }
    this.apiFormat = format;
    this.apiMedia = mime;
    this.value = this.getEditorValue(format, mime);
    this.parse();
  }

  /**
   * Stores the current session in the store.
   */
  async storeSession(): Promise<void> {
    const { value='', apiFormat='', apiMedia='' } = this;
    localStorage.setItem(ApiFormatKey, apiFormat);
    localStorage.setItem(ApiMediaKey, apiMedia);
    const key = this.editorValueKey(apiFormat, apiMedia);
    localStorage.setItem(key, value);
  }

  /**
   * Cleans up the API and the selection state,
   */
  cleanUp(): void {
    this.apiId = undefined;
    this.domainId = 'summary';
    this.domainType = 'summary';
    this.operationId = undefined;
    this.model = undefined;
  }

  /**
   * @param e Dispatched navigation event
   */
  apiNavigationHandler(e: ApiNavigationEvent): void {
    const { domainId, domainType, parentId, passive } = e.detail;
    if (passive === true) {
      return;
    }
    this.domainType = domainType;
    if (domainType === 'operation') {
      this.domainId = parentId as string;
      this.operationId = domainId;
    } else {
      this.domainId = domainId;
      this.operationId = undefined;
    }
    if (this.navigationOpened) {
      this.navigationOpened = false;
    }
  }

  menuToggleHandler(): void {
    this.navigationOpened = !this.navigationOpened;
  }

  /**
   * Generates Monaco configuration
   */
  generateEditorConfig(): editor.IStandaloneEditorConstructionOptions {
    const { value='', apiMedia: language } = this;
    // @ts-ignore
    const modelUri = monaco.Uri.parse(`http://api-editor/model.json`);
    // @ts-ignore
    const model = (monaco.editor as typeof editor).createModel(value, language || 'json', modelUri) as editor.ITextModel;

    const schema = {
      uri: "http://api-editor/default-schema.json",
      fileMatch: [modelUri.toString()],
      schema: {},
    };
    
    // @ts-ignore
    monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
      validate: true,
      schemas: [schema],
    });
    
    let config: editor.IStandaloneEditorConstructionOptions = ({
      minimap: {
        enabled: false,
      },
      formatOnType: true,
      folding: true,
      tabSize: 2,
      detectIndentation: true,
      // value,
      automaticLayout: true,
      model,
    });
    // @ts-ignore
    config = MonacoTheme.assignTheme(monaco, config);
    if (language) {
      config.language = language;
    }
    return config;
  }

  initializeEditor(): void {
    const config = this.generateEditorConfig();
    // @ts-ignore
    const instance = monaco.editor.create(document.body.querySelector('#container'), config) as editor.IStandaloneCodeEditor;
    // @ts-ignore
    instance.onDidChangeModelContent(this.valueChanged.bind(this));
    this.monacoInstance = instance;
  }

  /**
   * The callback for the Monaco editor value change.
   */
  valueChanged(): void {
    const { monacoInstance } = this;
    if (!monacoInstance) {
      return;
    }
    this.value = monacoInstance.getValue();
    if (this.changeTimeout) {
      window.clearTimeout(this.changeTimeout);
    }
    this.changeTimeout = window.setTimeout(() => {
      this.changeTimeout = undefined;
      this.storeSession();
      this.parse();
    }, 400);
  }

  formatSelectorHandler(e: Event): void {
    const node = (e.target as HTMLElement);
    const { vendor } = node.dataset;
    if (!vendor) {
      return;
    }
    this.selectVendor(vendor);
  }

  mediaSelectorHandler(e: Event): void {
    const node = (e.target as HTMLElement);
    const { media } = node.dataset;
    if (!media) {
      return;
    }
    this.selectMedia(media);
  }

  selectVendor(vendor: string): void {
    this.apiFormat = vendor;
    this.apiMedia = 'application/yaml';
    const value = this.getEditorValue(vendor, this.apiMedia);
    this.value = value;
    if (this.monacoInstance) {
      this.monacoInstance.setValue(value);
      const model = this.monacoInstance.getModel();
      // @ts-ignore
      monaco.editor.setModelLanguage(model, this.apiMedia);
    }
    this.domainId = 'summary';
    this.domainType = 'summary';
    this.model = undefined;
  }

  selectMedia(media: string): void {
    this.apiMedia = media;
    const value = this.getEditorValue(this.apiFormat, media);
    this.value = value;
    if (this.monacoInstance) {
      this.monacoInstance.setValue(value);
      const model = this.monacoInstance.getModel();
      // @ts-ignore
      monaco.editor.setModelLanguage(model, media);
    }
    this.domainId = 'summary';
    this.domainType = 'summary';
    this.model = undefined;
  }

  /**
   * @param vendor The API format.
   * @param media The API media type.
   * @returns The store key for the value.
   */
  editorValueKey(vendor: string, media?: string): string {
    let suffix = vendor;
    if (media) {
      suffix += `.${media}`;
    }
    return `${StorePrefix}${suffix}`;
  }

  /**
   * @param vendor The API format.
   * @param media The API media type.
   * @returns The value for the editor.
   */
  getEditorValue(vendor: string, media?: string): string {
    const key = this.editorValueKey(vendor, media);
    const value = localStorage.getItem(key);
    if (!value) {
      return this.getExampleValue(vendor, media)
    }
    return value;
  }

  /**
   * @param vendor The API format.
   * @param media The API media type.
   * @returns The value for the editor.
   */
  getExampleValue(vendor: string, media='application/yaml'): string {
    if (vendor === 'RAML 1.0') {
      return ApiExamples.Raml10.trim();
    }
    if (vendor === 'OAS 2.0') {
      if (media === 'application/yaml') {
        return ApiExamples.Oas20Yaml.trim();
      }
      if (media === 'application/json') {
        return ApiExamples.Oas20Json.trim();
      }
    }
    if (vendor === 'OAS 3.0') {
      if (media === 'application/yaml') {
        return ApiExamples.Oas30Yaml.trim();
      }
      if (media === 'application/json') {
        return ApiExamples.Oas30Json.trim();
      }
    }
    if (vendor === 'ASYNC 2.0') {
      return ApiExamples.Async20.trim();
    }
    return '';
  }

  formatSelectorKeyHandler(e: KeyboardEvent): void {
    if (!['Enter', 'Space'].includes(e.code)) {
      return;
    }
    const node = e.target as HTMLElement;
    const { media } = node.dataset;
    if (!media) {
      return;
    }
    this.selectMedia(media);
  }

  mediaSelectorKeyHandler(e: KeyboardEvent): void {
    if (!['Enter', 'Space'].includes(e.code)) {
      return;
    }
    const node = (e.target as HTMLElement);
    const { vendor } = node.dataset;
    if (!vendor) {
      return;
    }
    this.selectVendor(vendor);
  }

  getServerKey(location: string): string {
    const index = location.lastIndexOf('/');
    return location.substr(index + 1);
  }


  /**
   * Parses the current state of the editor.
   */
  async parse(): Promise<void> {
    this.loading = true;
    /** @type string */
    let previousKey;
    const lastRequest = this.requests[this.requests.length - 1];
    if (lastRequest) {
      previousKey = lastRequest.key;
      console.log('Cancelling', previousKey);
      lastRequest.ctrl.abort();
    }
    const url = new URL('/amf-server/api/parse-text', window.location.href).toString();
    const { value, apiFormat, apiMedia } = this;
    const headers = {
      'content-type': apiMedia,
      'x-api-vendor': apiFormat,
    }
    if (previousKey) {
      headers['x-previous-request'] = previousKey;
    }
    const response = await fetch(url, {
      method: 'POST',
      body: value,
      headers,
    });
    if (response.status !== 201) {
      this.loading = false;
      return;
    }
    const location = response.headers.get('location');
    if (!location) {
      this.loading = false;
      return;
    }
    const key = this.getServerKey(location);
    const ctrl = new AbortController();
    this.requests.push({
      key,
      ctrl,
    });
    this.checkStatus(location);
  }

  /**
   * Checks the parsing status from the server.
   */
  async checkStatus(location: string): Promise<void> {
    const key = this.getServerKey(location);
    const index = this.requests.findIndex(item => item.key === key);
    if (index === -1) {
      return;
    }
    const info = this.requests[index];
    if (info.ctrl.signal.aborted) {
      this.requests.splice(index, 1);
      return;
    }
    const url = new URL(location, window.location.href).toString();
    const response = await fetch(url);
    if (response.status === 204) {
      const newLocation = response.headers.get('location');
      if (!newLocation) {
        this.loading = false;
        return;
      }
      setTimeout(() => {
        this.checkStatus(newLocation);
      }, 500);
      return;
    }
    if (response.status === 200) {
      const newLocation = response.headers.get('location');
      if (!newLocation) {
        this.loading = false;
        console.error('The server did not return result location.');
        return;
      }
      this.readResult(newLocation);
      return;
    }

    this.loading = false;
    console.error('Invalid state.');
  }

  /**
   * Reads the parsed model result.
   */
  async readResult(location: string): Promise<void> {
    const key = this.getServerKey(location);
    const index = this.requests.findIndex(item => item.key === key);
    if (index === -1) {
      return;
    }
    const info = this.requests[index];
    this.requests.splice(index, 1);
    if (info.ctrl.signal.aborted) {
      return;
    }
    const url = new URL(location, window.location.href).toString();
    const response = await fetch(url);
    if (response.status !== 200) {
      this.loading = false;
      return;
    }
    let model = await response.json();
    if (Array.isArray(model)) {
      [model] = model;
    }
    this.model = model;
    this.store.amf = model;
    this.loading = false;
    console.log(model);
  }

  tryitHandler(e: CustomEvent): void {
    const { id } = e.detail;
    this.editorOperation = id;
    this.editorOpened = true;
  }

  editorCloseHandler(): void {
    this.editorOperation = undefined;
    this.editorOpened = false;
  }

  pageTemplate(): TemplateResult {
    const { initialized } = this;
    if (!initialized) {
      return super.pageTemplate();
    }
    return html`
      ${this.headerTemplate()}
      ${this.navigationTemplate()}
      ${this.mainTemplate()}
      ${this.footerTemplate()}
      ${this.requestEditorDialogTemplate()}
    `;
  }

  /**
   * @returns The application header template
   */
  headerTemplate(): TemplateResult {
    const { isMobile } = this;
    return html`
    <header class="start-page-header">
      ${isMobile ? this.navigationTriggerTemplate() : this.apiNameHeaderTemplate()}
    </header>`;
  }

  /**
   * @returns The navigation trigger button template
   */
  navigationTriggerTemplate(): TemplateResult {
    return html`
    <anypoint-icon-button 
      title="Open the menu" 
      aria-label="Activate to open the API navigation"
      @click="${this.menuToggleHandler}"
    >
      <ui-icon icon="menu"></ui-icon>
    </anypoint-icon-button>
    `;
  }

  /**
   * @returns The template for the API name.
   */
  apiNameHeaderTemplate(): TemplateResult {
    return html`
    <h1 class="start-page-header-title">API editor</h1>
    `;
  }

  protected mainTemplate(): TemplateResult {
    return html`<main>
    ${this.editorInputTemplate()}
    <aside>
      ${this.documentationTemplate()}
    </aside>
  </main>`;
  }

  /**
   * @returns The template for the API input editor
   */
  editorInputTemplate(): TemplateResult {
    return html`
    <section class="editing-area">
      ${this.apiSpecFormatSelector()}
      ${this.editorContainerTemplate()}
    </section>
    `;
  }

  /**
   * @returns {TemplateResult} The template for the API format selector
   */
  apiSpecFormatSelector(): TemplateResult {
    const { apiFormat } = this;
    return html`
    <div class="format-bar">
      <div class="button-group" @click="${this.formatSelectorHandler}" @keydown="${this.formatSelectorKeyHandler}">
        <anypoint-button ?active="${apiFormat === 'RAML 1.0'}" data-vendor="RAML 1.0">RAML 1.0</anypoint-button>
        <anypoint-button ?active="${apiFormat === 'OAS 2.0'}" data-vendor="OAS 2.0">OAS 2.0</anypoint-button>
        <anypoint-button ?active="${apiFormat === 'OAS 3.0'}" data-vendor="OAS 3.0">OAS 3.0</anypoint-button>
        <anypoint-button ?active="${apiFormat === 'ASYNC 2.0'}" data-vendor="ASYNC 2.0">Async API 2.0</anypoint-button>
      </div>
      ${this.apiMediaTypeSelector()}
    </div>
    `;
  }

  /**
   * @returns {TemplateResult|string} The template for the API media selector
   */
  apiMediaTypeSelector(): TemplateResult | string {
    const { apiFormat, apiMedia } = this;
    const hasMediaType = ['OAS 2.0', 'OAS 3.0'].includes(apiFormat);
    if (!hasMediaType) {
      return '';
    }
    return html`
    <div class="button-group" @click="${this.mediaSelectorHandler}" @keydown="${this.mediaSelectorKeyHandler}">
      <anypoint-button ?active="${apiMedia === 'application/yaml'}" data-media="application/yaml">application/yaml</anypoint-button>
      <anypoint-button ?active="${apiMedia === 'application/json'}" data-media="application/json">application/json</anypoint-button>
    </div>
    `;
  }

  /**
   * @returns {TemplateResult} The template for the Monaco editor.
   */
  editorContainerTemplate(): TemplateResult {
    return html`<div id="container"></div>`;
  }

  /**
   * @returns {TemplateResult|string} The template for API navigation.
   */
  navigationDrawerTemplate(): TemplateResult | string {
    const { model, navigationOpened } = this;
    if (!model) {
      return '';
    }
    const classes = {
      navigation: true,
      opened: navigationOpened,
    };
    return html`
    <nav class="${classMap(classes)}">
      <div class="api-doc-toolbar">
        <anypoint-icon-button 
          title="Close the menu" 
          aria-label="Activate to close the API navigation"
          @click="${this.menuToggleHandler}"
        >
          <ui-icon icon="close"></ui-icon>
        </anypoint-icon-button>
      </div>
      <api-navigation
        summary
        endpointsOpened
      ></api-navigation>
    </nav>`;
  }

  /**
   * @returns {TemplateResult} The template for the documentation section.
   */
  documentationTemplate(): TemplateResult {
    return html`
    ${this.navigationDrawerTemplate()}
    <section class="api-console">
      ${this.apiLoaderTemplate()}
      ${this.apiTemplate()}
    </section>
    `;
  }

  /**
   * @returns {TemplateResult|string} The template for the API loading indicator.
   */
  apiLoaderTemplate(): TemplateResult | string {
    if (!this.loading) {
      return '';
    }
    return html`
    <progress class="loading-progress"></progress>
    `;
  }

  /**
   * @returns {TemplateResult|string} The template for the API console's main documentation.
   */
  apiTemplate(): TemplateResult | string {
    const { model } = this;
    if (!model) {
      return '';
    }
    // .redirectUri="${this.redirectUri}"
    return html`
    <div class="api-doc-toolbar">
      ${this.navigationTriggerTemplate()}
    </div>
    <api-documentation
      slot="content"
      .domainId="${this.domainId}"
      .operationId="${this.operationId}"
      .domainType="${this.domainType}"
      tryItButton
      @tryit="${this.tryitHandler}"
    >
    </api-documentation>
    `;
  }

  requestEditorDialogTemplate(): TemplateResult | string {
    if (!this.httpEditorPositionTarget) {
      return '';
    }
    // .redirectUri="${this.redirectUri}"
    return html`
    <anypoint-dialog 
      @closed="${this.editorCloseHandler}" 
      .opened="${this.editorOpened}" 
      class="request-dialog"
      horizontalAlign="center"
      verticalAlign="middle"
      .positionTarget="${this.httpEditorPositionTarget}"
    >
      <h2>API request</h2>
      <anypoint-dialog-scrollable>
        <api-request
          .domainId="${this.editorOperation}"
          urlLabel
          applyAuthorization
          globalCache
          allowHideOptional
        >
        </api-request>
      </anypoint-dialog-scrollable>
      <div class="buttons">
        <anypoint-button data-dialog-dismiss>Close</anypoint-button>
      </div>
    </anypoint-dialog>
    `;
  }
}

const page = new ApiTextEditor();
page.render();
