// Copyright 2012 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 type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TextUtils from '../../core/text_utils/text_utils.js';
import * as Protocol from '../../generated/protocol.js';
import * as Workspace from '../workspace/workspace.js';

import type {CSSWorkspaceBinding} from './CSSWorkspaceBinding.js';
import type {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js';
import {type LiveLocation, LiveLocationPool, LiveLocationWithPool} from './LiveLocation.js';

export interface MessageSource {
  url?: Platform.DevToolsPath.UrlString;
  line: number;
  column: number;
  scriptId?: Protocol.Runtime.ScriptId;
  stackTrace?: Protocol.Runtime.StackTrace;
}

export class PresentationSourceFrameMessageManager implements
    SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel>,
    SDK.TargetManager.SDKModelObserver<SDK.CSSModel.CSSModel> {
  #targetToMessageHelperMap = new WeakMap<SDK.Target.Target, PresentationSourceFrameMessageHelper>();
  #targetManager: SDK.TargetManager.TargetManager;
  readonly #workspace: Workspace.Workspace.WorkspaceImpl;
  readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
  readonly #cssWorkspaceBinding: CSSWorkspaceBinding;
  constructor(targetManager: SDK.TargetManager.TargetManager, workspace: Workspace.Workspace.WorkspaceImpl,
              debuggerWorkspaceBinding: DebuggerWorkspaceBinding, cssWorkspaceBinding: CSSWorkspaceBinding) {
    this.#workspace = workspace;
    this.#targetManager = targetManager;
    this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
    this.#cssWorkspaceBinding = cssWorkspaceBinding;
  }

  enable(): void {
    this.#targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this);
    this.#targetManager.observeModels(SDK.CSSModel.CSSModel, this);
  }

  modelAdded(model: SDK.DebuggerModel.DebuggerModel|SDK.CSSModel.CSSModel): void {
    const target = model.target();
    const helper = this.#targetToMessageHelperMap.get(target) ??
        new PresentationSourceFrameMessageHelper(this.#workspace, this.#debuggerWorkspaceBinding,
                                                 this.#cssWorkspaceBinding);
    if (model instanceof SDK.DebuggerModel.DebuggerModel) {
      helper.setDebuggerModel(model);
    } else {
      helper.setCSSModel(model);
    }
    this.#targetToMessageHelperMap.set(target, helper);
  }

  modelRemoved(model: SDK.DebuggerModel.DebuggerModel|SDK.CSSModel.CSSModel): void {
    const target = model.target();
    const helper = this.#targetToMessageHelperMap.get(target);
    helper?.clear();
  }

  addMessage(message: Workspace.UISourceCode.Message, source: MessageSource, target: SDK.Target.Target): void {
    const helper = this.#targetToMessageHelperMap.get(target);
    void helper?.addMessage(message, source);
  }

  clear(): void {
    for (const target of this.#targetManager.targets()) {
      const helper = this.#targetToMessageHelperMap.get(target);
      helper?.clear();
    }
  }
}

export class PresentationConsoleMessageManager {
  #sourceFrameMessageManager: PresentationSourceFrameMessageManager;

  constructor(
      targetManager: SDK.TargetManager.TargetManager,
      workspace: Workspace.Workspace.WorkspaceImpl,
      debuggerWorkspaceBinding: DebuggerWorkspaceBinding,
      cssWorkspaceBinding: CSSWorkspaceBinding,
  ) {
    this.#sourceFrameMessageManager = new PresentationSourceFrameMessageManager(
        targetManager,
        workspace,
        debuggerWorkspaceBinding,
        cssWorkspaceBinding,
    );
    targetManager.addModelListener(SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.MessageAdded,
                                   event => this.consoleMessageAdded(event.data));
    SDK.ConsoleModel.ConsoleModel.allMessagesUnordered(targetManager).forEach(this.consoleMessageAdded, this);
    targetManager.addModelListener(SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.ConsoleCleared,
                                   () => this.#sourceFrameMessageManager.clear());
  }

  enable(): void {
    this.#sourceFrameMessageManager.enable();
  }

  private consoleMessageAdded(consoleMessage: SDK.ConsoleModel.ConsoleMessage): void {
    const runtimeModel = consoleMessage.runtimeModel();
    if (!consoleMessage.isErrorOrWarning() || !consoleMessage.runtimeModel() ||
        consoleMessage.source === Protocol.Log.LogEntrySource.Violation || !runtimeModel) {
      return;
    }
    const level = consoleMessage.level === Protocol.Log.LogEntryLevel.Error ?
        Workspace.UISourceCode.Message.Level.ERROR :
        Workspace.UISourceCode.Message.Level.WARNING;
    this.#sourceFrameMessageManager.addMessage(new Workspace.UISourceCode.Message(level, consoleMessage.messageText),
                                               consoleMessage, runtimeModel.target());
  }
}

export class PresentationSourceFrameMessageHelper {
  #debuggerModel?: SDK.DebuggerModel.DebuggerModel;
  #cssModel?: SDK.CSSModel.CSSModel;
  #presentationMessages = new Map<Platform.DevToolsPath.UrlString, Array<{
                                    source: MessageSource,
                                    presentation: PresentationSourceFrameMessage,
                                  }>>();
  readonly #locationPool: LiveLocationPool;
  readonly #workspace: Workspace.Workspace.WorkspaceImpl;
  readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
  readonly #cssWorkspaceBinding: CSSWorkspaceBinding;

  constructor(workspace: Workspace.Workspace.WorkspaceImpl, debuggerWorkspaceBinding: DebuggerWorkspaceBinding,
              cssWorkspaceBinding: CSSWorkspaceBinding) {
    this.#workspace = workspace;
    this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
    this.#cssWorkspaceBinding = cssWorkspaceBinding;
    this.#locationPool = new LiveLocationPool();

    this.#workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this.#uiSourceCodeAdded.bind(this));
  }

  setDebuggerModel(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
    if (this.#debuggerModel) {
      throw new Error('Cannot set DebuggerModel twice');
    }
    this.#debuggerModel = debuggerModel;
    // TODO(dgozman): queueMicrotask because we race with DebuggerWorkspaceBinding on ParsedScriptSource event delivery.
    debuggerModel.addEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, event => {
      queueMicrotask(() => {
        this.#parsedScriptSource(event);
      });
    });
    debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.#debuggerReset, this);
  }

  setCSSModel(cssModel: SDK.CSSModel.CSSModel): void {
    if (this.#cssModel) {
      throw new Error('Cannot set CSSModel twice');
    }
    this.#cssModel = cssModel;
    cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetAdded,
                              event => queueMicrotask(() => this.#styleSheetAdded(event)));
  }

  async addMessage(message: Workspace.UISourceCode.Message, source: MessageSource): Promise<void> {
    const presentation = new PresentationSourceFrameMessage(message, this.#locationPool, this.#debuggerWorkspaceBinding,
                                                            this.#cssWorkspaceBinding);
    const location = this.#rawLocation(source) ?? this.#cssLocation(source) ?? this.#uiLocation(source);
    if (location) {
      await presentation.updateLocationSource(location);
    }
    if (source.url) {
      let messages = this.#presentationMessages.get(source.url);
      if (!messages) {
        messages = [];
        this.#presentationMessages.set(source.url, messages);
      }
      messages.push({source, presentation});
    }
  }

  #uiLocation(source: MessageSource): Workspace.UISourceCode.UILocation|null {
    if (!source.url) {
      return null;
    }

    const uiSourceCode = this.#workspace.uiSourceCodeForURL(source.url);
    if (!uiSourceCode) {
      return null;
    }
    return new Workspace.UISourceCode.UILocation(uiSourceCode, source.line, source.column);
  }

  #cssLocation(source: MessageSource): SDK.CSSModel.CSSLocation|null {
    if (!this.#cssModel || !source.url) {
      return null;
    }
    const locations = this.#cssModel.createRawLocationsByURL(source.url, source.line, source.column);
    return locations[0] ?? null;
  }

  #rawLocation(source: MessageSource): SDK.DebuggerModel.Location|null {
    if (!this.#debuggerModel) {
      return null;
    }
    if (source.scriptId) {
      return this.#debuggerModel.createRawLocationByScriptId(source.scriptId, source.line, source.column);
    }
    const callFrame = source.stackTrace?.callFrames ? source.stackTrace.callFrames[0] : null;
    if (callFrame) {
      return this.#debuggerModel.createRawLocationByScriptId(callFrame.scriptId, callFrame.lineNumber,
                                                             callFrame.columnNumber);
    }
    if (source.url) {
      return this.#debuggerModel.createRawLocationByURL(source.url, source.line, source.column);
    }
    return null;
  }

  #parsedScriptSource(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void {
    const script = event.data;
    const messages = this.#presentationMessages.get(script.sourceURL);

    const promises: Array<Promise<void>> = [];
    for (const {presentation, source} of messages ?? []) {
      const rawLocation = this.#rawLocation(source);
      if (rawLocation && script.scriptId === rawLocation.scriptId) {
        promises.push(presentation.updateLocationSource(rawLocation));
      }
    }

    void Promise.all(promises).then(this.parsedScriptSourceForTest.bind(this));
  }

  parsedScriptSourceForTest(): void {
  }

  #uiSourceCodeAdded(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
    const uiSourceCode = event.data;
    const messages = this.#presentationMessages.get(uiSourceCode.url());

    const promises: Array<Promise<void>> = [];
    for (const {presentation, source} of messages ?? []) {
      promises.push(presentation.updateLocationSource(
          new Workspace.UISourceCode.UILocation(uiSourceCode, source.line, source.column)));
    }
    void Promise.all(promises).then(this.uiSourceCodeAddedForTest.bind(this));
  }

  uiSourceCodeAddedForTest(): void {
  }

  #styleSheetAdded(event: Common.EventTarget
                       .EventTargetEvent<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, SDK.CSSModel.EventTypes>): void {
    const header = event.data;
    const messages = this.#presentationMessages.get(header.sourceURL);

    const promises: Array<Promise<void>> = [];
    for (const {source, presentation} of messages ?? []) {
      if (header.containsLocation(source.line, source.column)) {
        promises.push(
            presentation.updateLocationSource(new SDK.CSSModel.CSSLocation(header, source.line, source.column)));
      }
    }
    void Promise.all(promises).then(this.styleSheetAddedForTest.bind(this));
  }

  styleSheetAddedForTest(): void {
  }

  clear(): void {
    this.#debuggerReset();
  }

  #debuggerReset(): void {
    const presentations = Array.from(this.#presentationMessages.values()).flat();
    for (const {presentation} of presentations) {
      presentation.dispose();
    }
    this.#presentationMessages.clear();
    this.#locationPool.disposeAll();
  }
}

class FrozenLiveLocation extends LiveLocationWithPool {
  #uiLocation: Workspace.UISourceCode.UILocation;
  constructor(uiLocation: Workspace.UISourceCode.UILocation, updateDelegate: (arg0: LiveLocation) => Promise<void>,
              locationPool: LiveLocationPool) {
    super(updateDelegate, locationPool);
    this.#uiLocation = uiLocation;
  }

  override async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> {
    return this.#uiLocation;
  }
}

export class PresentationSourceFrameMessage {
  #uiSourceCode?: Workspace.UISourceCode.UISourceCode;
  #liveLocation?: LiveLocation;
  readonly #locationPool: LiveLocationPool;
  readonly #message: Workspace.UISourceCode.Message;
  readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
  readonly #cssWorkspaceBinding: CSSWorkspaceBinding;

  constructor(
      message: Workspace.UISourceCode.Message,
      locationPool: LiveLocationPool,
      debuggerWorkspaceBinding: DebuggerWorkspaceBinding,
      cssWorkspaceBinding: CSSWorkspaceBinding,
  ) {
    this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
    this.#cssWorkspaceBinding = cssWorkspaceBinding;
    this.#message = message;
    this.#locationPool = locationPool;
  }

  async updateLocationSource(source: SDK.DebuggerModel.Location|Workspace.UISourceCode.UILocation|
                             SDK.CSSModel.CSSLocation): Promise<void> {
    if (source instanceof SDK.DebuggerModel.Location) {
      await this.#debuggerWorkspaceBinding.createLiveLocation(source, this.#updateLocation.bind(this),
                                                              this.#locationPool);
    } else if (source instanceof SDK.CSSModel.CSSLocation) {
      await this.#cssWorkspaceBinding.createLiveLocation(source, this.#updateLocation.bind(this), this.#locationPool);
    } else if (source instanceof Workspace.UISourceCode.UILocation) {
      if (!this.#liveLocation) {  // Don't "downgrade" the location if a debugger or css mapping was already successful
        this.#liveLocation = new FrozenLiveLocation(source, this.#updateLocation.bind(this), this.#locationPool);
        await this.#liveLocation.update();
      }
    }
  }

  async #updateLocation(liveLocation: LiveLocation): Promise<void> {
    if (this.#uiSourceCode) {
      this.#uiSourceCode.removeMessage(this.#message);
    }
    if (liveLocation !== this.#liveLocation) {
      this.#uiSourceCode?.removeMessage(this.#message);
      this.#liveLocation?.dispose();
      this.#liveLocation = liveLocation;
    }
    const uiLocation = await liveLocation.uiLocation();
    if (!uiLocation) {
      return;
    }
    this.#message.range =
        TextUtils.TextRange.TextRange.createFromLocation(uiLocation.lineNumber, uiLocation.columnNumber || 0);
    this.#uiSourceCode = uiLocation.uiSourceCode;
    this.#uiSourceCode.addMessage(this.#message);
  }

  dispose(): void {
    this.#uiSourceCode?.removeMessage(this.#message);
    this.#liveLocation?.dispose();
  }
}
