import { createBus } from '@eolme/vma-engine';
import type { Emitter } from '@eolme/vma-engine';

import Route, { PAGE_MAIN } from './Route';
import Scheduler from './Scheduler';

import { log, error } from '../utils/report';

import type { RouteList, RouteStack, HistoryEvent, RouteLike } from '../types';

class History {
  private static _name = '[History]';

  private _idle: boolean = false;

  private _offset!: number;
  private _stack!: RouteStack;
  private _index!: number;

  private _bus!: Emitter;
  private _scheduler!: Scheduler;

  routes!: RouteList;

  constructor(routes) {
    this.routes = routes;

    this._initEmitter();
    this._initScheduler();
    this._initHistory();
    this._initListener();
  }

  get index() {
    return this._index;
  }

  get location() {
    return window.location.hash.slice(1);
  }

  get route(): Readonly<Route> {
    return this._stack[this._index];
  }

  get length() {
    return this._stack.length;
  }

  push(route: Route) {
    log(History._name, 'Queue push', route);
    this._scheduler.nextTick(() => {
      log(History._name, 'Enqueue push', route);

      const event: HistoryEvent = {
        prev: this.route,
        next: route
      };

      route.index = ++this._index;
      this._stack.push(route);
      window.history.pushState(route, route.uri, '#' + route.uri);

      log(History._name, 'Update after push.');
      this._bus.emit('update', event);
    });
  }

  replace(route: Route) {
    log(History._name, 'Queue replace', route);
    this._scheduler.nextTick(() => {
      log(History._name, 'Enqueue replace', route);

      const event: HistoryEvent = {
        prev: this.route,
        next: route
      };

      route.index = this._index;
      this._stack.pop();
      this._stack.push(route);
      window.history.replaceState(route, route.uri, '#' + route.uri);

      log(History._name, 'Update after replace.');
      this._bus.emit('update', event);
    });
  }

  moveBy(by: number) {
    if (by === 0) {
      log(History._name, 'Moving from current to current is the same as reloading window.');
      this._bus.emit('reload');
      return;
    }

    const tick = this._scheduler.nextTick();
    this._scheduler.setTick(this._createTickWithPopstate());

    log(History._name, 'Queue move by', by);
    tick.then(() => {
      log(History._name, 'Enqueue move by', by);

      window.history.go(by);
    });
  }

  moveTo(to: number) {
    const delta = to - this._index;
    if (delta === 0) {
      log(History._name, 'Moving from current to current is the same as reloading window.');
      this._bus.emit('reload');
      return;
    }

    const tick = this._scheduler.nextTick();
    this._scheduler.setTick(this._createTickWithPopstate());

    log(History._name, 'Queue move to', to);
    tick.then(() => {
      log(History._name, 'Enqueue move to', to);

      window.history.go(delta);
    });
  }

  back() {
    if (this._index === 0) {
      log(History._name, 'Going back without history is the same as reloading window.');
      this._bus.emit('reload');
      return;
    }

    const tick = this._scheduler.nextTick();
    this._scheduler.setTick(this._createTickWithPopstate());

    log(History._name, 'Queue back');
    tick.then(() => {
      log(History._name, 'Enqueue back');

      window.history.back();
    });
  }

  reset() {
    if (this._index === 0) {
      log(History._name, 'Resetting without history is the same as reloading window.');
      this._bus.emit('reload');
      return;
    }

    const tick = this._scheduler.nextTick();
    this._scheduler.setTick(this._createTickWithPopstate());

    log(History._name, 'Queue reset.');
    tick.then(() => {
      log(History._name, 'Enqueue reset.');

      window.history.go(-1 * this._index);
    });
  }

  pushAfterMove(prevRoute: Route, nextRoute: Route) {
    let prevIndex = prevRoute.index;
    if (prevRoute.index === -1) {
      prevIndex = this.indexOf(prevRoute);
    }

    if (this.canMoveTo(prevIndex)) {
      const delta = prevIndex - this._index;
      if (delta === 0) {
        this.replace(nextRoute);
      } else {
        this._idle = true;

        const tick = this._scheduler.nextTick();
        this._scheduler.setTick(this._createTickWithPopstate());

        this._scheduler.nextTick(() => {
          this.push(nextRoute);
        }).then(() => {
          this._idle = false;
        });

        tick.then(() => {
          window.history.go(delta);
        });
      }
      return;
    }

    let nextIndex = nextRoute.index;
    if (nextRoute.index === -1) {
      nextIndex = this.lastIndexOf(nextRoute);
    }

    if (this.canMoveTo(nextIndex)) {
      const delta = nextIndex - this._index;
      if (delta === 0) {
        this.push(nextRoute);
      } else {
        this._idle = true;

        const tick = this._scheduler.nextTick();
        this._scheduler.setTick(this._createTickWithPopstate());

        this._scheduler.nextTick(() => {
          this.push(nextRoute);
        }).then(() => {
          this._idle = false;
        });

        tick.then(() => {
          window.history.go(delta);
        });
      }
      return;
    }

    if (prevRoute.isSameWith(nextRoute)) {
      this.replace(nextRoute);
    } else {
      error('Cant find pair in history for', prevRoute, nextRoute);
    }
  }

  canMoveBy(by: number) {
    const next = this._index + by;
    return next >= 0 && next < this._stack.length;
  }

  canMoveTo(to: number) {
    return to >= 0 && to < this._stack.length;
  }

  indexOf(route: RouteLike) {
    for (let i = 0, find: Route; i < this._stack.length; ++i) {
      find = this._stack[i];
      if (find.isSameWith(route)) {
        return i;
      }
    }
    return -1;
  }

  lastIndexOf(route: RouteLike) {
    for (let i = this._stack.length - 1, find: Route; i >= 0; --i) {
      find = this._stack[i];
      if (find.isSameWith(route)) {
        return i;
      }
    }
    return -1;
  }

  check() {
    const historyLength = window.history.length - this._offset;
    const stackLength = this._stack.length;

    const historyRoute = window.history.state as Route;
    const stackRoute = this.route;

    const isNormal = (
      historyLength === stackLength &&
      historyRoute && stackRoute &&
      historyRoute.index === stackRoute.index &&
      historyRoute.uri === stackRoute.uri
    );

    if (isNormal) {
      log(History._name, 'History in the correct state.');
    } else {
      log(History._name, 'History in an incorrect state. Need to fix.');
      this._fixHistory();
    }
  }

  /**
   * History is broken after:
   * - VKPay
   * - Post from notification
   * - Outside manipulations
   */
  private _fixHistory() {
    const historyIndex = (
      window.history.state && typeof window.history.state.index === 'number' ?
        window.history.state.index : -1
    );

    let isHistoryClean = (
      window.history.length === 1 ||
      window.history.length === (this._offset + 1) ||
      historyIndex === 0
    );

    const isCanPush = (
      !isHistoryClean &&
      historyIndex !== -1 &&
      historyIndex < this._index
    );

    if (isCanPush) {
      log(History._name, 'Fixing by push missing.');

      log(History._name, 'Queue push missing.');
      this._scheduler.nextTick(() => {
        log(History._name, 'Enqueue push missing.');

        const append = this._stack.slice(historyIndex);
        append.forEach((route) => {
          window.history.pushState(route, route.uri, route.uri);
        });

        const event: HistoryEvent = {
          prev: this.route,
          next: this.route
        };

        log(History._name, 'Update after push missing.');
        this._bus.emit('update', event);
      });
      return;
    }

    const isCleanable = (
      !isHistoryClean &&
      window.history.length === (this._stack.length + this._offset)
    );

    if (isCleanable) {
      log(History._name, 'Fixing by clean history.');

      log(History._name, 'Queue history clearing.');
      this._scheduler.nextTick(() => {
        log(History._name, 'Enqueue history clearing.');

        this._idle = true;
        this._scheduler.setTick(this._createTickWithPopstate());
        this._scheduler.nextTick(() => {
          this._idle = false;
        });

        const by = this._offset - window.history.length + 1;
        window.history.go(by);
      });

      isHistoryClean = true;
    }

    if (isHistoryClean) {
      log(History._name, 'Fixing by re-push.');

      log(History._name, 'Queue re-push.');
      this._scheduler.nextTick(() => {
        log(History._name, 'Enqueue re-push.');

        const first = this._stack[0];
        window.history.replaceState(first, first.uri, first.uri);

        const other = this._stack.slice(1);
        other.forEach((route) => {
          window.history.pushState(route, route.uri, route.uri);
        });

        const event: HistoryEvent = {
          prev: this.route,
          next: this.route
        };

        log(History._name, 'Update after re-push.');
        this._bus.emit('update', event);
      });
      return;
    }

    error('History in unknown state. Impossible to fix.');
  }

  private _initEmitter() {
    this._bus = createBus();
    this.on = this._bus.on.bind(this);
    this.once = this._bus.once.bind(this);
    this.off = this._bus.off.bind(this);
  }

  private _initScheduler() {
    this._scheduler = new Scheduler();
  }

  private _initHistory() {
    const initRoute = new Route();
    initRoute.index = 0;
    initRoute.page = PAGE_MAIN;
    initRoute.uri = PAGE_MAIN;
    window.history.replaceState(initRoute, initRoute.uri, '#' + initRoute.uri);

    this._stack = [initRoute];
    this._offset = window.history.length - 1;
    this._index = 0;
  }

  private _initListener() {
    window.addEventListener('popstate', (e = window.event as PopStateEvent) => {
      log(History._name, 'Queue popstate.');
      this._scheduler.nextTick(() => {
        log(History._name, 'Enqueue popstate.');

        if (this._idle) {
          // Router is idle
          log(History._name, 'Popstate while Router is idle. This is normal behavior while waiting for an action.');
          return;
        }

        let prevRoute: Route;
        let nextRoute: Route;

        const fromIndex = e.state?.index ?? -1;
        const toIndex = window.history.state?.index ?? -1;

        if (fromIndex !== -1) {
          if (fromIndex < this.length) {
            prevRoute = this._stack[fromIndex];
          } else {
            const state = e.state as RouteLike;
            prevRoute = Route.buildFromState(this.routes, state);
          }
        } else {
          prevRoute = null;
        }

        if (toIndex !== -1) {
          if (toIndex < this._stack.length) {
            nextRoute = this._stack[toIndex];
          } else {
            const state = window.history.state as RouteLike;
            nextRoute = Route.buildFromState(this.routes, state);
          }
        } else {
          nextRoute = Route.buildFromLocation(this.routes, this.location);
        }

        if (nextRoute.index !== -1) {
          this._index = nextRoute.index;
        } else {
          const index = this.lastIndexOf(nextRoute);
          if (index === -1) {
            this._index++;
          } else {
            this._index = index;
          }
          nextRoute.index = this._index;
        }

        const delta = () => nextRoute.index - (this._stack.length - 1);

        const offset = delta();
        if (offset > 1) {
          error('Back to the Future.');
        }

        while (delta() <= 0) {
          this._stack.pop();
        }
        this._stack.push(nextRoute);

        const event: HistoryEvent = {
          prev: prevRoute,
          next: nextRoute
        };

        log(History._name, 'Update after popstate.');
        this._bus.emit('update', event);
      });
    });
  }

  _createTickWithPopstate(): Promise<void> {
    return new Promise((resolve) => {
      const flush = () => {
        window.removeEventListener('popstate', flush);
        window.setTimeout(resolve, 26);
      };
      window.addEventListener('popstate', flush);
    });
  }

  on: Emitter['on'];
  once: Emitter['once'];
  off: Emitter['off'];
}

export { History };
export default History;
