// Copyright (C) 2024 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.

import {assertExists, assertTrue} from '../base/logging';
import {App} from '../public/app';
import {TraceContext, TraceImpl} from './trace_impl';
import {CommandManagerImpl} from './command_manager';
import {OmniboxManagerImpl} from './omnibox_manager';
import {raf} from './raf_scheduler';
import {SidebarManagerImpl} from './sidebar_manager';
import {PluginManagerImpl} from './plugin_manager';
import {NewEngineMode} from '../trace_processor/engine';
import {RouteArgs} from '../public/route_schema';
import {SqlPackage} from '../public/extra_sql_packages';
import {SerializedAppState} from './state_serialization_schema';
import {PostedTrace, TraceSource} from './trace_source';
import {loadTrace} from './load_trace';
import {CORE_PLUGIN_ID} from './plugin_manager';
import {Router} from './router';
import {AnalyticsInternal, initAnalytics} from './analytics_impl';
import {createProxy, getOrCreate} from '../base/utils';
import {PageManagerImpl} from './page_manager';
import {PageHandler} from '../public/page';
import {PerfManager} from './perf_manager';
import {ServiceWorkerController} from '../frontend/service_worker_controller';
import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
import {featureFlags} from './feature_flags';

// The args that frontend/index.ts passes when calling AppImpl.initialize().
// This is to deal with injections that would otherwise cause circular deps.
export interface AppInitArgs {
  initialRouteArgs: RouteArgs;
}

/**
 * Handles the global state of the ui, for anything that is not related to a
 * specific trace. This is always available even before a trace is loaded (in
 * contrast to TraceContext, which is bound to the lifetime of a trace).
 * There is only one instance in total of this class (see instance()).
 * This class is only exposed to TraceImpl, nobody else should refer to this
 * and should use AppImpl instead.
 */
export class AppContext {
  // The per-plugin instances of AppImpl (including the CORE_PLUGIN one).
  private readonly pluginInstances = new Map<string, AppImpl>();
  readonly commandMgr = new CommandManagerImpl();
  readonly omniboxMgr = new OmniboxManagerImpl();
  readonly pageMgr = new PageManagerImpl();
  readonly sidebarMgr: SidebarManagerImpl;
  readonly pluginMgr: PluginManagerImpl;
  readonly perfMgr = new PerfManager();
  readonly analytics: AnalyticsInternal;
  readonly serviceWorkerController: ServiceWorkerController;
  httpRpc = {
    newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE' as NewEngineMode,
    httpRpcAvailable: false,
  };
  initialRouteArgs: RouteArgs;
  isLoadingTrace = false; // Set when calling openTrace().
  readonly initArgs: AppInitArgs;
  readonly embeddedMode: boolean;
  readonly testingMode: boolean;

  // This is normally empty and is injected with extra google-internal packages
  // via is_internal_user.js
  extraSqlPackages: SqlPackage[] = [];

  // The currently open trace.
  currentTrace?: TraceContext;

  private static _instance: AppContext;

  static initialize(initArgs: AppInitArgs): AppContext {
    assertTrue(AppContext._instance === undefined);
    return (AppContext._instance = new AppContext(initArgs));
  }

  static get instance(): AppContext {
    return assertExists(AppContext._instance);
  }

  // This constructor is invoked only once, when frontend/index.ts invokes
  // AppMainImpl.initialize().
  private constructor(initArgs: AppInitArgs) {
    this.initArgs = initArgs;
    this.initialRouteArgs = initArgs.initialRouteArgs;
    this.serviceWorkerController = new ServiceWorkerController();
    this.embeddedMode = this.initialRouteArgs.mode === 'embedded';
    this.testingMode =
      self.location !== undefined &&
      self.location.search.indexOf('testing=1') >= 0;
    this.sidebarMgr = new SidebarManagerImpl({
      disabled: this.embeddedMode,
      hidden: this.initialRouteArgs.hideSidebar,
    });
    this.analytics = initAnalytics(this.testingMode, this.embeddedMode);
    this.pluginMgr = new PluginManagerImpl({
      forkForPlugin: (pluginId) => this.forPlugin(pluginId),
      get trace() {
        return AppImpl.instance.trace;
      },
    });
  }

  // Gets or creates an instance of AppImpl backed by the current AppContext
  // for the given plugin.
  forPlugin(pluginId: string) {
    return getOrCreate(this.pluginInstances, pluginId, () => {
      return new AppImpl(this, pluginId);
    });
  }

  closeCurrentTrace() {
    this.omniboxMgr.reset(/* focus= */ false);

    if (this.currentTrace !== undefined) {
      // This will trigger the unregistration of trace-scoped commands and
      // sidebar menuitems (and few similar things).
      this.currentTrace[Symbol.dispose]();
      this.currentTrace = undefined;
    }
  }

  // Called by trace_loader.ts soon after it has created a new TraceImpl.
  setActiveTrace(traceCtx: TraceContext) {
    // In 99% this closeCurrentTrace() call is not needed because the real one
    // is performed by openTrace() in this file. However in some rare cases we
    // might end up loading a trace while another one is still loading, and this
    // covers races in that case.
    this.closeCurrentTrace();
    this.currentTrace = traceCtx;
  }
}

/*
 * Every plugin gets its own instance. This is how we keep track
 * what each plugin is doing and how we can blame issues on particular
 * plugins.
 * The instance exists for the whole duration a plugin is active.
 */

export class AppImpl implements App {
  readonly pluginId: string;
  private readonly appCtx: AppContext;
  private readonly pageMgrProxy: PageManagerImpl;

  // Invoked by frontend/index.ts.
  static initialize(args: AppInitArgs) {
    AppContext.initialize(args).forPlugin(CORE_PLUGIN_ID);
  }

  // Gets access to the one instance that the core can use. Note that this is
  // NOT the only instance, as other AppImpl instance will be created for each
  // plugin.
  static get instance(): AppImpl {
    return AppContext.instance.forPlugin(CORE_PLUGIN_ID);
  }

  // Only called by AppContext.forPlugin().
  constructor(appCtx: AppContext, pluginId: string) {
    this.appCtx = appCtx;
    this.pluginId = pluginId;

    this.pageMgrProxy = createProxy(this.appCtx.pageMgr, {
      registerPage(pageHandler: PageHandler): Disposable {
        return appCtx.pageMgr.registerPage({
          ...pageHandler,
          pluginId,
        });
      },
    });
  }

  forPlugin(pluginId: string): AppImpl {
    return this.appCtx.forPlugin(pluginId);
  }

  get commands(): CommandManagerImpl {
    return this.appCtx.commandMgr;
  }

  get sidebar(): SidebarManagerImpl {
    return this.appCtx.sidebarMgr;
  }

  get omnibox(): OmniboxManagerImpl {
    return this.appCtx.omniboxMgr;
  }

  get plugins(): PluginManagerImpl {
    return this.appCtx.pluginMgr;
  }

  get analytics(): AnalyticsInternal {
    return this.appCtx.analytics;
  }

  get pages(): PageManagerImpl {
    return this.pageMgrProxy;
  }

  get trace(): TraceImpl | undefined {
    return this.appCtx.currentTrace?.forPlugin(this.pluginId);
  }

  scheduleFullRedraw(force?: 'force'): void {
    raf.scheduleFullRedraw(force);
  }

  get httpRpc() {
    return this.appCtx.httpRpc;
  }

  get initialRouteArgs(): RouteArgs {
    return this.appCtx.initialRouteArgs;
  }

  get featureFlags(): FeatureFlagManager {
    return {
      register: (settings: FlagSettings) => featureFlags.register(settings),
    };
  }

  openTraceFromFile(file: File): void {
    this.openTrace({type: 'FILE', file});
  }

  openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
    this.openTrace({type: 'URL', url, serializedAppState});
  }

  openTraceFromBuffer(postMessageArgs: PostedTrace): void {
    this.openTrace({type: 'ARRAY_BUFFER', ...postMessageArgs});
  }

  openTraceFromHttpRpc(): void {
    this.openTrace({type: 'HTTP_RPC'});
  }

  private async openTrace(src: TraceSource) {
    this.appCtx.closeCurrentTrace();
    this.appCtx.isLoadingTrace = true;
    try {
      // loadTrace() in trace_loader.ts will do the following:
      // - Create a new engine.
      // - Pump the data from the TraceSource into the engine.
      // - Do the initial queries to build the TraceImpl object
      // - Call AppImpl.setActiveTrace(TraceImpl)
      // - Continue with the trace loading logic (track decider, plugins, etc)
      // - Resolve the promise when everything is done.
      await loadTrace(this, src);
      this.omnibox.reset(/* focus= */ false);
      // loadTrace() internally will call setActiveTrace() and change our
      // _currentTrace in the middle of its ececution. We cannot wait for
      // loadTrace to be finished before setting it because some internal
      // implementation details of loadTrace() rely on that trace to be current
      // to work properly (mainly the router hash uuid).
    } catch (err) {
      this.omnibox.showStatusMessage(`${err}`);
      throw err;
    } finally {
      this.appCtx.isLoadingTrace = false;
      raf.scheduleFullRedraw();
    }
  }

  // Called by trace_loader.ts soon after it has created a new TraceImpl.
  setActiveTrace(traceImpl: TraceImpl) {
    this.appCtx.setActiveTrace(traceImpl.__traceCtxForApp);
  }

  get embeddedMode(): boolean {
    return this.appCtx.embeddedMode;
  }

  get testingMode(): boolean {
    return this.appCtx.testingMode;
  }

  get isLoadingTrace() {
    return this.appCtx.isLoadingTrace;
  }

  get extraSqlPackages(): SqlPackage[] {
    return this.appCtx.extraSqlPackages;
  }

  get perfDebugging(): PerfManager {
    return this.appCtx.perfMgr;
  }

  get serviceWorkerController(): ServiceWorkerController {
    return this.appCtx.serviceWorkerController;
  }

  // Nothing other than TraceImpl's constructor should ever refer to this.
  // This is necessary to avoid circular dependencies between trace_impl.ts
  // and app_impl.ts.
  get __appCtxForTrace() {
    return this.appCtx;
  }

  navigate(newHash: string): void {
    Router.navigate(newHash);
  }
}
