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

import type * as Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as UI from '../../ui/legacy/legacy.js';

export class ExecutionContextSelector implements SDK.TargetManager.SDKModelObserver<SDK.RuntimeModel.RuntimeModel> {
  #targetManager: SDK.TargetManager.TargetManager;
  #context: UI.Context.Context;
  #lastSelectedContextId?: string;
  #ignoreContextChanged?: boolean;

  constructor(targetManager: SDK.TargetManager.TargetManager, context: UI.Context.Context) {
    context.addFlavorChangeListener(SDK.RuntimeModel.ExecutionContext, this.#executionContextChanged, this);
    context.addFlavorChangeListener(SDK.Target.Target, this.#targetChanged, this);

    targetManager.addModelListener(
        SDK.RuntimeModel.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextCreated, this.#onExecutionContextCreated,
        this);
    targetManager.addModelListener(
        SDK.RuntimeModel.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextDestroyed,
        this.#onExecutionContextDestroyed, this);
    targetManager.addModelListener(
        SDK.RuntimeModel.RuntimeModel, SDK.RuntimeModel.Events.ExecutionContextOrderChanged,
        this.#onExecutionContextOrderChanged, this);
    this.#targetManager = targetManager;
    this.#context = context;
    targetManager.observeModels(SDK.RuntimeModel.RuntimeModel, this);
  }

  modelAdded(runtimeModel: SDK.RuntimeModel.RuntimeModel): void {
    // Defer selecting default target since we need all clients to get their
    // targetAdded notifications first.
    queueMicrotask(deferred.bind(this));

    function deferred(this: ExecutionContextSelector): void {
      // We always want the second context for the service worker targets.
      if (!this.#context.flavor(SDK.Target.Target)) {
        this.#context.setFlavor(SDK.Target.Target, runtimeModel.target());
      }
    }
  }

  modelRemoved(runtimeModel: SDK.RuntimeModel.RuntimeModel): void {
    const currentExecutionContext = this.#context.flavor(SDK.RuntimeModel.ExecutionContext);
    if (currentExecutionContext?.runtimeModel === runtimeModel) {
      this.#currentExecutionContextGone();
    }

    const models = this.#targetManager.models(SDK.RuntimeModel.RuntimeModel);
    if (this.#context.flavor(SDK.Target.Target) === runtimeModel.target() && models.length) {
      this.#context.setFlavor(SDK.Target.Target, models[0].target());
    }
  }

  #executionContextChanged({
    data: newContext,
  }: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.ExecutionContext|null>): void {
    if (newContext) {
      this.#context.setFlavor(SDK.Target.Target, newContext.target());
      if (!this.#ignoreContextChanged) {
        this.#lastSelectedContextId = this.#contextPersistentId(newContext);
      }
    }
  }

  #contextPersistentId(executionContext: SDK.RuntimeModel.ExecutionContext): string {
    return executionContext.isDefault ? executionContext.target().name() + ':' + executionContext.frameId : '';
  }

  #targetChanged({data: newTarget}: Common.EventTarget.EventTargetEvent<SDK.Target.Target|null>): void {
    const currentContext = this.#context.flavor(SDK.RuntimeModel.ExecutionContext);

    if (!newTarget || (currentContext && currentContext.target() === newTarget)) {
      return;
    }

    const runtimeModel = newTarget.model(SDK.RuntimeModel.RuntimeModel);
    const executionContexts = runtimeModel ? runtimeModel.executionContexts() : [];
    if (!executionContexts.length) {
      return;
    }

    let newContext: SDK.RuntimeModel.ExecutionContext|null = null;
    for (let i = 0; i < executionContexts.length && !newContext; ++i) {
      if (this.#shouldSwitchToContext(executionContexts[i])) {
        newContext = executionContexts[i];
      }
    }
    for (let i = 0; i < executionContexts.length && !newContext; ++i) {
      if (this.#isDefaultContext(executionContexts[i])) {
        newContext = executionContexts[i];
      }
    }
    this.#ignoreContextChanged = true;
    this.#context.setFlavor(SDK.RuntimeModel.ExecutionContext, newContext || executionContexts[0]);
    this.#ignoreContextChanged = false;
  }

  #shouldSwitchToContext(executionContext: SDK.RuntimeModel.ExecutionContext): boolean {
    if (executionContext.target().targetInfo()?.subtype) {
      return false;
    }
    if (this.#lastSelectedContextId && this.#lastSelectedContextId === this.#contextPersistentId(executionContext)) {
      return true;
    }
    return !this.#lastSelectedContextId && this.#isDefaultContext(executionContext);
  }

  #isDefaultContext(executionContext: SDK.RuntimeModel.ExecutionContext): boolean {
    if (!executionContext.isDefault || !executionContext.frameId) {
      return false;
    }
    if (executionContext.target().parentTarget()?.type() === SDK.Target.Type.FRAME) {
      return false;
    }
    const resourceTreeModel = executionContext.target().model(SDK.ResourceTreeModel.ResourceTreeModel);
    const frame = resourceTreeModel?.frameForId(executionContext.frameId);
    return Boolean(frame?.isOutermostFrame());
  }

  #onExecutionContextCreated(event: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.ExecutionContext>): void {
    if (this.#lastSelectedContextId === undefined) {
      // We switch to the first context created (if applicable) but ignore sub-sequent
      // worker context creations.
      this.#switchContextIfNecessary(event.data);
      return;
    }

    switch (event.data.target().type()) {
      case SDK.Target.Type.AUCTION_WORKLET:
      case SDK.Target.Type.SHARED_STORAGE_WORKLET:
      case SDK.Target.Type.SHARED_WORKER:
      case SDK.Target.Type.ServiceWorker:
      case SDK.Target.Type.WORKLET:
      case SDK.Target.Type.Worker:
        return;

      case SDK.Target.Type.BROWSER:
      case SDK.Target.Type.FRAME:
      case SDK.Target.Type.NODE:
      case SDK.Target.Type.TAB:
        this.#switchContextIfNecessary(event.data);
        break;
    }
  }

  #onExecutionContextDestroyed(event: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.ExecutionContext>): void {
    const executionContext = event.data;
    if (this.#context.flavor(SDK.RuntimeModel.ExecutionContext) === executionContext) {
      this.#currentExecutionContextGone();
    }
  }

  #onExecutionContextOrderChanged(event: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.RuntimeModel>): void {
    const runtimeModel = event.data;
    const executionContexts = runtimeModel.executionContexts();
    for (let i = 0; i < executionContexts.length; i++) {
      if (this.#switchContextIfNecessary(executionContexts[i])) {
        break;
      }
    }
  }

  #switchContextIfNecessary(executionContext: SDK.RuntimeModel.ExecutionContext): boolean {
    if (!this.#context.flavor(SDK.RuntimeModel.ExecutionContext) || this.#shouldSwitchToContext(executionContext)) {
      this.#ignoreContextChanged = true;
      this.#context.setFlavor(SDK.RuntimeModel.ExecutionContext, executionContext);
      this.#ignoreContextChanged = false;
      return true;
    }
    return false;
  }

  #currentExecutionContextGone(): void {
    const runtimeModels = this.#targetManager.models(SDK.RuntimeModel.RuntimeModel);
    let newContext: SDK.RuntimeModel.ExecutionContext|null = null;
    for (let i = 0; i < runtimeModels.length && !newContext; ++i) {
      const executionContexts = runtimeModels[i].executionContexts();
      for (const executionContext of executionContexts) {
        if (this.#isDefaultContext(executionContext)) {
          newContext = executionContext;
          break;
        }
      }
    }
    if (!newContext) {
      for (let i = 0; i < runtimeModels.length && !newContext; ++i) {
        const executionContexts = runtimeModels[i].executionContexts();
        if (executionContexts.length) {
          newContext = executionContexts[0];
          break;
        }
      }
    }
    this.#ignoreContextChanged = true;
    this.#context.setFlavor(SDK.RuntimeModel.ExecutionContext, newContext);
    this.#ignoreContextChanged = false;
  }
}
