import * as plugins from './domtools.plugins.js';
import { type TViewport } from './domtools.css.breakpoints.js';
import { Scroller } from './domtools.classes.scroller.js';
import { WebSetup } from '@push.rocks/websetup';
import { ThemeManager } from './domtools.classes.thememanager.js';
import { Keyboard } from './domtools.classes.keyboard.js';

export interface IDomToolsState {
  virtualViewport: TViewport;
  jwt: string;
}

export interface IDomToolsContructorOptions {
  ignoreGlobal?: boolean;
}

export class DomTools {
  // ======
  // STATIC
  // ======
  /**
   * setups domtools
   */
  public static async setupDomTools(optionsArg: IDomToolsContructorOptions = {}) {
    let domToolsInstance: DomTools;
    if (!globalThis.deesDomTools && !optionsArg.ignoreGlobal) {
      globalThis.deesDomTools = new DomTools(optionsArg);
      domToolsInstance = globalThis.deesDomTools;

      // lets make sure the dom is ready
      const readyStateChangedFunc = () => {
        if (document.readyState === 'interactive' || document.readyState === 'complete') {
          domToolsInstance.elements.headElement = document.querySelector('head');
          domToolsInstance.elements.bodyElement = document.querySelector('body');
          domToolsInstance.domReady.resolve();
        }
      };
      document.addEventListener('readystatechange', readyStateChangedFunc);
      domToolsInstance.domToolsReady.resolve();
    } else if (optionsArg.ignoreGlobal) {
      domToolsInstance = new DomTools(optionsArg);
    } else {
      domToolsInstance = globalThis.deesDomTools;
    }
    await domToolsInstance.domToolsReady.promise;
    return domToolsInstance;
  }

  /**
   * if you can, use the static asysnc .setupDomTools() function instead since it is safer to use.
   */
  public static getGlobalDomToolsSync(): DomTools {
    const globalDomTools: DomTools = globalThis.deesDomTools;
    if (!globalDomTools) {
      throw new Error('You tried to access domtools synchronously too early');
    }
    return globalThis.deesDomTools;
  }

  // ========
  // INSTANCE
  // ========
  // elements
  public elements: {
    headElement: HTMLElement;
    bodyElement: HTMLElement;
  } = {
    headElement: null,
    bodyElement: null,
  };

  public websetup: WebSetup = new WebSetup({
    metaObject: {
      title: 'loading...',
    },
  });

  public smartstate = new plugins.smartstate.Smartstate();
  public domToolsStatePart = this.smartstate.getStatePart<IDomToolsState>('domtools', {
    virtualViewport: 'native',
    jwt: null,
  });

  public router = new plugins.smartrouter.SmartRouter({
    debug: false,
  });

  public convenience = {
    typedrequest: plugins.typedrequest,
    smartdelay: plugins.smartdelay,
    smartjson: plugins.smartjson,
    smarturl: plugins.smarturl,
  };

  public deesComms = new plugins.deesComms.DeesComms();
  public scroller = new Scroller(this);
  public themeManager = new ThemeManager(this);
  public keyboard = new Keyboard(document.body);

  public domToolsReady = plugins.smartpromise.defer();
  public domReady = plugins.smartpromise.defer();
  public globalStylesReady = plugins.smartpromise.defer();

  constructor(optionsArg: IDomToolsContructorOptions) {}

  private runOnceTrackerStringMap = new plugins.lik.Stringmap();
  private runOnceResultMap = new plugins.lik.FastMap();

  /**
   * run a function once and always get the Promise of the first execution
   * @param identifierArg the indentifier arg identifies functions. functions with the same identifier are considered equal
   * @param funcArg the actual func arg to run
   */
  public async runOnce<T>(identifierArg: string, funcArg: () => Promise<T>) {
    const runningId = `${identifierArg}+runningCheck`;
    if (!this.runOnceTrackerStringMap.checkString(identifierArg)) {
      this.runOnceTrackerStringMap.addString(identifierArg);
      this.runOnceTrackerStringMap.addString(runningId);
      const result = await funcArg();
      this.runOnceResultMap.addToMap(identifierArg, result);
      this.runOnceTrackerStringMap.removeString(runningId);
    }
    return await this.runOnceTrackerStringMap.registerUntilTrue(
      (stringMap) => {
        return !stringMap.includes(runningId);
      },
      () => {
        return this.runOnceResultMap.getByKey(identifierArg);
      }
    );
  }

  // setStuff
  /**
   * allows to set global styles
   * @param stylesText the css text you want to set
   */
  public async setGlobalStyles(stylesText: string) {
    await this.domReady.promise;
    const styleElement = document.createElement('style');
    styleElement.type = 'text/css';
    styleElement.appendChild(document.createTextNode(stylesText));
    this.elements.headElement.appendChild(styleElement);
  }

  /**
   * allows to set global styles
   * @param stylesText the css text you want to set
   */
  public async setExternalScript(scriptLinkArg: string) {
    await this.domReady.promise;
    const done = plugins.smartpromise.defer();
    const script = document.createElement('script');
    script.src = scriptLinkArg;
    script.addEventListener('load', function () {
      done.resolve();
    });
    const parentNode = document.head || document.body;
    parentNode.append(script);
    await done.promise;
  }

  /**
   * allows setting external css files
   * @param cssLinkArg a url to an external stylesheet
   */
  public async setExternalCss(cssLinkArg: string) {
    const cssTag = document.createElement('link');
    cssTag.rel = 'stylesheet';
    cssTag.crossOrigin = 'anonymous';
    cssTag.href = cssLinkArg;
    document.head.append(cssTag);
  }

  /**
   * allows setting of website infos
   * @param optionsArg the website info
   */
  public async setWebsiteInfo(optionsArg: plugins.websetup.IWebSetupConstructorOptions) {
    await this.websetup.setup(optionsArg);
    await this.websetup.readyPromise;
  }
}
