/**
 * @license
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * This defines the Gerrit instance. All methods directly attached to Gerrit
 * should be defined or linked here.
 */
import {
  getPluginLoader,
  PluginOptionMap,
  PluginLoader,
} from './gr-plugin-loader';
import {getRestAPI, send} from './gr-api-utils';
import {appContext} from '../../../services/app-context';
import {PluginApi} from '../../plugins/gr-plugin-types';
import {HttpMethod} from '../../../constants/constants';
import {RequestPayload} from '../../../types/common';
import {
  EventCallback,
  EventEmitterService,
} from '../../../services/gr-event-interface/gr-event-interface';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {getRootElement} from '../../../scripts/rootElement';
import {GrPluginEndpoints} from './gr-plugin-endpoints';
import {rangesEqual} from '../../diff/gr-diff/gr-diff-utils';
import {SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import {CoverageType} from '../../../types/types';
import {RevisionInfo} from '../revision-info/revision-info';

export interface GerritGlobal extends EventEmitterService {
  flushPreinstalls?(): void;
  css(rule: string): string;
  install(
    callback: (plugin: PluginApi) => void,
    opt_version?: string,
    src?: string
  ): void;
  getLoggedIn(): Promise<boolean>;
  get(url: string, callback?: (response: unknown) => void): void;
  post(
    url: string,
    payload?: RequestPayload,
    callback?: (response: unknown) => void
  ): void;
  put(
    url: string,
    payload?: RequestPayload,
    callback?: (response: unknown) => void
  ): void;
  delete(url: string, callback?: (response: unknown) => void): void;
  isPluginLoaded(pathOrUrl: string): boolean;
  awaitPluginsLoaded(): Promise<unknown>;
  _loadPlugins(plugins: string[], opts: PluginOptionMap): void;
  _arePluginsLoaded(): boolean;
  _isPluginPreloaded(pathOrUrl: string): boolean;
  _isPluginEnabled(pathOrUrl: string): boolean;
  _isPluginLoaded(pathOrUrl: string): boolean;
  _eventEmitter: EventEmitterService;
  _customStyleSheet: CSSStyleSheet;

  // exposed methods
  Nav: typeof GerritNav;
  Auth: typeof appContext.authService;
  getRootElement: typeof getRootElement;
  _pluginLoader: PluginLoader;
  _endpoints: GrPluginEndpoints;
  slotToContent(slot: unknown): unknown;
  rangesEqual: typeof rangesEqual;
  SUGGESTIONS_PROVIDERS_USERS_TYPES: typeof SUGGESTIONS_PROVIDERS_USERS_TYPES;
  CoverageType: typeof CoverageType;
  RevisionInfo: typeof RevisionInfo;
}

/**
 * Trigger the preinstalls for bundled plugins.
 * This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
 */
function flushPreinstalls() {
  const Gerrit = window.Gerrit;
  if (Gerrit?.flushPreinstalls) {
    Gerrit.flushPreinstalls();
  }
}
export const _testOnly_flushPreinstalls = flushPreinstalls;

export function initGerritPluginApi() {
  window.Gerrit = window.Gerrit || {};
  flushPreinstalls();
  initGerritPluginsMethods(window.Gerrit as GerritGlobal);
  // Preloaded plugins should be installed after Gerrit.install() is set,
  // since plugin preloader substitutes Gerrit.install() temporarily.
  // (Gerrit.install() is set in initGerritPluginsMethods)
  getPluginLoader().installPreloadedPlugins();
}

export function _testOnly_initGerritPluginApi(): GerritGlobal {
  window.Gerrit = window.Gerrit || {};
  initGerritPluginApi();
  return window.Gerrit as GerritGlobal;
}

export function deprecatedDelete(
  url: string,
  callback?: (response: Response) => void
) {
  console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
  return getRestAPI()
    .send(HttpMethod.DELETE, url)
    .then(response => {
      if (response.status !== 204) {
        return response.text().then(text => {
          if (text) {
            return Promise.reject(new Error(text));
          } else {
            return Promise.reject(new Error(`${response.status}`));
          }
        });
      }
      if (callback) callback(response);
      return response;
    });
}

function initGerritPluginsMethods(globalGerritObj: GerritGlobal) {
  /**
   * @deprecated Use plugin.styles().css(rulesStr) instead. Please, consult
   * the documentation how to replace it accordingly.
   */
  globalGerritObj.css = (rulesStr: string) => {
    console.warn(
      'Gerrit.css(rulesStr) is deprecated!',
      'Use plugin.styles().css(rulesStr)'
    );
    if (!globalGerritObj._customStyleSheet) {
      const styleEl = document.createElement('style');
      document.head.appendChild(styleEl);
      globalGerritObj._customStyleSheet = styleEl.sheet!;
    }

    const name = `__pg_js_api_class_${globalGerritObj._customStyleSheet.cssRules.length}`;
    globalGerritObj._customStyleSheet.insertRule(
      '.' + name + '{' + rulesStr + '}',
      0
    );
    return name;
  };

  globalGerritObj.install = (callback, opt_version, opt_src) => {
    getPluginLoader().install(callback, opt_version, opt_src);
  };

  globalGerritObj.getLoggedIn = () => {
    console.warn(
      'Gerrit.getLoggedIn() is deprecated! ' +
        'Use plugin.restApi().getLoggedIn()'
    );
    return document.createElement('gr-rest-api-interface').getLoggedIn();
  };

  globalGerritObj.get = (
    url: string,
    callback?: (response: unknown) => void
  ) => {
    console.warn('.get() is deprecated! Use plugin.restApi().get()');
    send(HttpMethod.GET, url, callback);
  };

  globalGerritObj.post = (
    url: string,
    payload?: RequestPayload,
    callback?: (response: unknown) => void
  ) => {
    console.warn('.post() is deprecated! Use plugin.restApi().post()');
    send(HttpMethod.POST, url, callback, payload);
  };

  globalGerritObj.put = (
    url: string,
    payload?: RequestPayload,
    callback?: (response: unknown) => void
  ) => {
    console.warn('.put() is deprecated! Use plugin.restApi().put()');
    send(HttpMethod.PUT, url, callback, payload);
  };

  globalGerritObj.delete = (
    url: string,
    callback?: (response: Response) => void
  ) => {
    deprecatedDelete(url, callback);
  };

  globalGerritObj.awaitPluginsLoaded = () => {
    return getPluginLoader().awaitPluginsLoaded();
  };

  // TODO(taoalpha): consider removing these proxy methods
  // and using getPluginLoader() directly
  globalGerritObj._loadPlugins = (plugins, opt_option) => {
    getPluginLoader().loadPlugins(plugins, opt_option);
  };

  globalGerritObj._arePluginsLoaded = () => {
    return getPluginLoader().arePluginsLoaded();
  };

  globalGerritObj._isPluginPreloaded = url => {
    return getPluginLoader().isPluginPreloaded(url);
  };

  globalGerritObj._isPluginEnabled = pathOrUrl => {
    return getPluginLoader().isPluginEnabled(pathOrUrl);
  };

  globalGerritObj._isPluginLoaded = pathOrUrl => {
    return getPluginLoader().isPluginLoaded(pathOrUrl);
  };

  const eventEmitter = appContext.eventEmitter;

  // TODO(taoalpha): List all internal supported event names.
  // Also convert this to inherited class once we move Gerrit to class.
  globalGerritObj._eventEmitter = eventEmitter;
  /**
   * Enabling EventEmitter interface on Gerrit.
   *
   * This will enable to signal across different parts of js code without relying on DOM,
   * including core to core, plugin to plugin and also core to plugin.
   *
   * @example
   *
   * // Emit this event from pluginA
   * Gerrit.install(pluginA => {
   *   fetch("some-api").then(() => {
   *     Gerrit.on("your-special-event", {plugin: pluginA});
   *   });
   * });
   *
   * // Listen on your-special-event from pluignB
   * Gerrit.install(pluginB => {
   *   Gerrit.on("your-special-event", ({plugin}) => {
   *     // do something, plugin is pluginA
   *   });
   * });
   */
  globalGerritObj.addListener = (eventName: string, cb: EventCallback) =>
    eventEmitter.addListener(eventName, cb);
  globalGerritObj.dispatch = (eventName: string, detail: any) =>
    eventEmitter.dispatch(eventName, detail);
  globalGerritObj.emit = (eventName: string, detail: any) =>
    eventEmitter.emit(eventName, detail);
  globalGerritObj.off = (eventName: string, cb: EventCallback) =>
    eventEmitter.off(eventName, cb);
  globalGerritObj.on = (eventName: string, cb: EventCallback) =>
    eventEmitter.on(eventName, cb);
  globalGerritObj.once = (eventName: string, cb: EventCallback) =>
    eventEmitter.once(eventName, cb);
  globalGerritObj.removeAllListeners = (eventName: string) =>
    eventEmitter.removeAllListeners(eventName);
  globalGerritObj.removeListener = (eventName: string, cb: EventCallback) =>
    eventEmitter.removeListener(eventName, cb);
}
