import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
import { Config, QueueController } from '../../interface';
import { flattenRouterTree, readRedirects, readRoutes } from './utils/parser';
import { readNavState, writeNavState } from './utils/dom';
import { chainToPath, generatePath, parsePath, readPath, writePath } from './utils/path';
import { RouteChain, RouteRedirect, RouterDirection, RouterEventDetail } from './utils/interface';
import { routeRedirect, routerIDsToChain, routerPathToChain } from './utils/matching';

@Component({
  tag: 'ion-router'
})
export class Router {

  private routes: RouteChain[] = [];
  private previousPath: string|null = null;
  private redirects: RouteRedirect[] = [];
  private busy = false;
  private init = false;
  private state = 0;
  private lastState = 0;
  private timer: any;

  @Element() el!: HTMLElement;

  @Prop({ context: 'config' }) config!: Config;
  @Prop({ context: 'queue' }) queue!: QueueController;
  @Prop({ context: 'window' }) win!: Window;
  @Prop({ context: 'isServer' }) isServer!: boolean;

  /**
   * By default `ion-router` will match the routes at the root path ("/").
   * That can be changed when
   *
   * T
   */
  @Prop() root = '/';

  /**
   * The router can work in two "modes":
   * - With hash: `/index.html#/path/to/page`
   * - Without hash: `/path/to/page`
   *
   * Using one or another might depend in the requirements of your app and/or where it's deployed.
   *
   * Usually "hash-less" navigation works better for SEO and it's more user friendly too, but it might
   * requires aditional server-side configuration in order to properly work.
   *
   * On the otherside hash-navigation is much easier to deploy, it even works over the file protocol.
   *
   * By default, this property is `true`, change to `false` to allow hash-less URLs.
   */
  @Prop() useHash = true;

  @Event() ionRouteChanged!: EventEmitter<RouterEventDetail>;

  componentWillLoad() {
    console.debug('[ion-router] router will load');

    const tree = readRoutes(this.el);
    this.routes = flattenRouterTree(tree);
    this.redirects = readRedirects(this.el);

    return this.writeNavStateRoot(this.getPath(), RouterDirection.None);
  }

  componentDidLoad() {
    this.init = true;

    console.debug('[ion-router] router did load');

    // const tree = readRoutes(this.el);
    // this.routes = flattenRouterTree(tree);
    // this.redirects = readRedirects(this.el);

    // // TODO: use something else
    // requestAnimationFrame(() => {
    //   this.historyDirection();
    //   this.writeNavStateRoot(this.getPath(), RouterDirection.None);
    // });
  }

  @Listen('ionRouteRedirectChanged')
  protected onRedirectChanged(ev: CustomEvent) {
    if (!this.init) {
      return;
    }
    console.debug('[ion-router] redirect data changed', ev.target);
    this.redirects = readRedirects(this.el);
  }

  @Listen('ionRouteDataChanged')
  protected onRoutesChanged(ev: CustomEvent) {
    if (!this.init) {
      return;
    }
    console.debug('[ion-router] route data changed', ev.target, ev.detail);

    // schedule write
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = undefined;
    }
    this.timer = setTimeout(() => {
      console.debug('[ion-router] data changed -> update nav');
      const tree = readRoutes(this.el);
      this.routes = flattenRouterTree(tree);
      this.writeNavStateRoot(this.getPath(), RouterDirection.None);
      this.timer = undefined;
    }, 100);
  }

  @Listen('window:popstate')
  protected onPopState() {
    const direction = this.historyDirection();
    const path = this.getPath();
    console.debug('[ion-router] URL changed -> update nav', path, direction);
    return this.writeNavStateRoot(path, direction);
  }

  private historyDirection() {
    if (this.win.history.state === null) {
      this.state++;
      this.win.history.replaceState(this.state, this.win.document.title, this.win.document.location.href);
    }

    const state = this.win.history.state;
    const lastState = this.lastState;
    this.lastState = state;

    if (state > lastState) {
      return RouterDirection.Forward;
    } else if (state < lastState) {
      return RouterDirection.Back;
    } else {
      return RouterDirection.None;
    }
  }

  @Method()
  async navChanged(direction: RouterDirection): Promise<boolean> {
    if (this.busy) {
      return false;
    }
    const { ids, outlet } = readNavState(this.win.document.body);
    const chain = routerIDsToChain(ids, this.routes);
    if (!chain) {
      console.warn('[ion-router] no matching URL for ', ids.map(i => i.id));
      return false;
    }

    const path = chainToPath(chain);
    if (!path) {
      console.warn('[ion-router] router could not match path because some required param is missing');
      return false;
    }

    console.debug('[ion-router] nav changed -> update URL', ids, path);
    this.setPath(path, direction);
    if (outlet) {
      console.debug('[ion-router] updating nested outlet', outlet);
      await this.writeNavState(outlet, chain, RouterDirection.None, ids.length);
    }
    this.emitRouteChange(path, null);
    return true;
  }

  @Method()
  push(url: string, direction = RouterDirection.Forward) {
    const path = parsePath(url);
    this.setPath(path, direction);

    console.debug('[ion-router] URL pushed -> updating nav', url, direction);
    return this.writeNavStateRoot(path, direction);
  }

  private async writeNavStateRoot(path: string[]|null, direction: RouterDirection): Promise<boolean> {
    if (this.busy) {
      return false;
    }
    if (!path) {
      console.error('[ion-router] URL is not part of the routing set');
      return false;
    }
    const redirect = routeRedirect(path, this.redirects);
    let redirectFrom: string[]|null = null;
    if (redirect) {
      this.setPath(redirect.to!, direction);
      redirectFrom = redirect.from;
      path = redirect.to!;
    }
    const chain = routerPathToChain(path, this.routes);
    const changed = await this.writeNavState(this.win.document.body, chain, direction);
    if (changed) {
      this.emitRouteChange(path, redirectFrom);
    }
    return changed;
  }

  private async writeNavState(node: any, chain: RouteChain | null, direction: RouterDirection, index = 0): Promise<boolean> {
    if (this.busy) {
      return false;
    }
    this.busy = true;
    const changed = await writeNavState(node, chain, direction, index);
    this.busy = false;
    return changed;
  }

  private setPath(path: string[], direction: RouterDirection) {
    this.state++;
    writePath(this.win.history, this.root, this.useHash, path, direction, this.state);
  }

  private getPath(): string[] | null {
    return readPath(this.win.location, this.root, this.useHash);
  }

  private emitRouteChange(path: string[], redirectPath: string[]|null) {
    console.debug('[ion-router] route changed', path);
    const from = this.previousPath;
    const redirectedFrom = redirectPath ? generatePath(redirectPath) : null;
    const to = generatePath(path);
    this.previousPath = to;
    this.ionRouteChanged.emit({
      from,
      redirectedFrom,
      to: to
    });
  }
}
