// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.m.js';


/**
 * The different pages that can be shown at a time.
 * Note: This must remain in sync with the page ids in manager.html!
 * @enum {string}
 */
export const Page = {
  LIST: 'items-list',
  DETAILS: 'details-view',
  ACTIVITY_LOG: 'activity-log',
  SHORTCUTS: 'keyboard-shortcuts',
  ERRORS: 'error-page',
};

/** @enum {string} */
export const Dialog = {
  OPTIONS: 'options',
};

/**
 * @typedef {{page: Page,
 *            extensionId: (string|undefined),
 *            subpage: (!Dialog|undefined)}}
 */
export let PageState;

/**
 * @param {!PageState} a
 * @param {!PageState} b
 * @return {boolean} Whether a and b are equal.
 */
function isPageStateEqual(a, b) {
  return a.page === b.page && a.subpage === b.subpage &&
      a.extensionId === b.extensionId;
}

/**
 * Regular expression that captures the leading slash, the content and the
 * trailing slash in three different groups.
 * @const {!RegExp}
 */
const CANONICAL_PATH_REGEX = /(^\/)([\/-\w]+)(\/$)/;

/**
 * A helper object to manage in-page navigations. Since the extensions page
 * needs to support different urls for different subpages (like the details
 * page), we use this object to manage the history and url conversions.
 */
export class NavigationHelper {
  constructor() {
    this.processRoute_();

    /** @private {number} */
    this.nextListenerId_ = 1;

    /** @private {!Map<number, function(!PageState)>} */
    this.listeners_ = new Map();

    /** @private {!PageState} */
    this.previousPage_;

    window.addEventListener('popstate', () => {
      this.notifyRouteChanged_(this.getCurrentPage());
    });
  }

  /** @private */
  get currentPath_() {
    return location.pathname.replace(CANONICAL_PATH_REGEX, '$1$2');
  }

  /**
   * Going to /configureCommands and /shortcuts should land you on /shortcuts.
   * These are the only two supported routes, so all other cases will redirect
   * you to root path if not already on it.
   * @private
   */
  processRoute_() {
    if (this.currentPath_ === '/configureCommands' ||
        this.currentPath_ === '/shortcuts') {
      window.history.replaceState(
          undefined /* stateObject */, '', '/shortcuts');
    } else if (this.currentPath_ !== '/') {
      window.history.replaceState(undefined /* stateObject */, '', '/');
    }
  }

  /**
   * @return {!PageState} The page that should be displayed for the
   *     current URL.
   */
  getCurrentPage() {
    const search = new URLSearchParams(location.search);
    let id = search.get('id');
    if (id) {
      return {page: Page.DETAILS, extensionId: id};
    }
    id = search.get('activity');
    if (id) {
      return {page: Page.ACTIVITY_LOG, extensionId: id};
    }
    id = search.get('options');
    if (id) {
      return {page: Page.DETAILS, extensionId: id, subpage: Dialog.OPTIONS};
    }
    id = search.get('errors');
    if (id) {
      return {page: Page.ERRORS, extensionId: id};
    }

    if (this.currentPath_ === '/shortcuts') {
      return {page: Page.SHORTCUTS};
    }

    return {page: Page.LIST};
  }

  /**
   * Function to add subscribers.
   * @param {!function(!PageState)} listener
   * @return {number} A numerical ID to be used for removing the listener.
   */
  addListener(listener) {
    const nextListenerId = this.nextListenerId_++;
    this.listeners_.set(nextListenerId, listener);
    return nextListenerId;
  }

  /**
   * Remove a previously registered listener.
   * @param {number} id
   * @return {boolean} Whether a listener with the given ID was actually found
   *     and removed.
   */
  removeListener(id) {
    return this.listeners_.delete(id);
  }

  /**
   * Function to notify subscribers.
   * @private
   */
  notifyRouteChanged_(newPage) {
    this.listeners_.forEach((listener, id) => {
      listener(newPage);
    });
  }

  /**
   * @param {!PageState} newPage the page to navigate to.
   */
  navigateTo(newPage) {
    const currentPage = this.getCurrentPage();
    if (currentPage && isPageStateEqual(currentPage, newPage)) {
      return;
    }

    this.updateHistory(newPage, false /* replaceState */);
    this.notifyRouteChanged_(newPage);
  }

  /**
   * @param {!PageState} newPage the page to replace the current
   *     page with.
   */
  replaceWith(newPage) {
    this.updateHistory(newPage, true /* replaceState */);
    if (this.previousPage_ && isPageStateEqual(this.previousPage_, newPage)) {
      // Skip the duplicate history entry.
      history.back();
      return;
    }
    this.notifyRouteChanged_(newPage);
  }

  /**
   * Called when a page changes, and pushes state to history to reflect it.
   * @param {!PageState} entry
   * @param {boolean} replaceState
   */
  updateHistory(entry, replaceState) {
    let path;
    switch (entry.page) {
      case Page.LIST:
        path = '/';
        break;
      case Page.ACTIVITY_LOG:
        path = '/?activity=' + entry.extensionId;
        break;
      case Page.DETAILS:
        if (entry.subpage) {
          assert(entry.subpage === Dialog.OPTIONS);
          path = '/?options=' + entry.extensionId;
        } else {
          path = '/?id=' + entry.extensionId;
        }
        break;
      case Page.SHORTCUTS:
        path = '/shortcuts';
        break;
      case Page.ERRORS:
        path = '/?errors=' + entry.extensionId;
        break;
    }
    assert(path);
    const state = {url: path};
    const currentPage = this.getCurrentPage();
    const isDialogNavigation = currentPage.page === entry.page &&
        currentPage.extensionId === entry.extensionId;
    // Navigating to a dialog doesn't visually change pages; it just opens
    // a dialog. As such, we replace state rather than pushing a new state
    // on the stack so that hitting the back button doesn't just toggle the
    // dialog.
    if (replaceState || isDialogNavigation) {
      history.replaceState(state, '', path);
    } else {
      this.previousPage_ = currentPage;
      history.pushState(state, '', path);
    }
  }
}

export const navigation = new NavigationHelper();
