// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */

import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
import type * as TextEditor from '../../ui/components/text_editor/text_editor.js';
import type * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';

import {Plugin} from './Plugin.js';

// Defines plugins that show profiling information in the editor
// gutter when available.

const UIStrings = {
  /**
   * @description The milisecond unit
   */
  ms: 'ms',
  /**
   * @description Unit for data size in DevTools
   */
  mb: 'MB',
  /**
   * @description A unit
   */
  kb: 'kB',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/sources/ProfilePlugin.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

class MemoryMarker extends CodeMirror.GutterMarker {
  constructor(readonly value: number) {
    super();
  }

  override eq(other: MemoryMarker): boolean {
    return this.value === other.value;
  }

  override toDOM(): HTMLElement {
    const element = document.createElement('div');
    element.className = 'cm-profileMarker';
    let value = this.value;
    const intensity = Platform.NumberUtilities.clamp(Math.log10(1 + 2e-3 * value) / 5, 0.02, 1);
    element.style.backgroundColor = `hsla(217, 100%, 70%, ${intensity.toFixed(3)})`;
    value /= 1e3;
    let units;
    let fractionDigits;
    if (value >= 1e3) {
      units = i18nString(UIStrings.mb);
      value /= 1e3;
      fractionDigits = value >= 20 ? 0 : 1;
    } else {
      units = i18nString(UIStrings.kb);
      fractionDigits = 0;
    }
    element.textContent = value.toFixed(fractionDigits);
    const unitElement = element.appendChild(document.createElement('span'));
    unitElement.className = 'cm-units';
    unitElement.textContent = units;
    return element;
  }
}

class PerformanceMarker extends CodeMirror.GutterMarker {
  constructor(readonly value: number) {
    super();
  }

  override eq(other: MemoryMarker): boolean {
    return this.value === other.value;
  }

  override toDOM(): HTMLElement {
    const element = document.createElement('div');
    element.className = 'cm-profileMarker';
    const intensity = Platform.NumberUtilities.clamp(Math.log10(1 + 10 * this.value) / 5, 0.02, 1);
    element.textContent = this.value.toFixed(1);
    element.style.backgroundColor = `hsla(44, 100%, 50%, ${intensity.toFixed(3)})`;
    const span = document.createElement('span');
    span.className = 'cm-units';
    span.textContent = i18nString(UIStrings.ms);
    element.appendChild(span);
    return element;
  }
}

function markersFromProfileData(
    map: Workspace.UISourceCode.LineColumnProfileMap, state: CodeMirror.EditorState,
    type: Workspace.UISourceCode.DecoratorType): CodeMirror.RangeSet<CodeMirror.GutterMarker> {
  const markerType = type === Workspace.UISourceCode.DecoratorType.PERFORMANCE ? PerformanceMarker : MemoryMarker;
  const markers: Array<CodeMirror.Range<CodeMirror.GutterMarker>> = [];
  const aggregatedByLine = new Map<number, number>();
  for (const [line, value] of map) {
    if (line <= state.doc.lines) {
      for (const [, data] of value) {
        aggregatedByLine.set(line, (aggregatedByLine.get(line) || 0) + data);
      }
    }
  }
  for (const [line, value] of aggregatedByLine) {
    const {from} = state.doc.line(line);
    markers.push(new markerType(value).range(from));
  }
  return CodeMirror.RangeSet.of(markers, true);
}

const makeLineLevelProfilePlugin = (type: Workspace.UISourceCode.DecoratorType): typeof Plugin =>
    class ProfilePlugin extends Plugin {
  updateEffect = CodeMirror.StateEffect.define<Workspace.UISourceCode.LineColumnProfileMap>();
  field: CodeMirror.StateField<CodeMirror.RangeSet<CodeMirror.GutterMarker>>;
  gutter: CodeMirror.Extension;
  compartment: CodeMirror.Compartment = new CodeMirror.Compartment();
  readonly #transformer: SourceFrame.SourceFrame.Transformer;

  constructor(uiSourceCode: Workspace.UISourceCode.UISourceCode, transformer: SourceFrame.SourceFrame.Transformer) {
    super(uiSourceCode);

    this.field = CodeMirror.StateField.define<CodeMirror.RangeSet<CodeMirror.GutterMarker>>({
      create(): CodeMirror.RangeSet<CodeMirror.GutterMarker> {
        return CodeMirror.RangeSet.empty;
      },
      update: (markers, tr) => {
        return tr.effects.reduce((markers, effect) => {
          return effect.is(this.updateEffect) ? markersFromProfileData(effect.value, tr.state, type) : markers;
        }, markers.map(tr.changes));
      },
    });

    this.gutter = CodeMirror.gutter({
      markers: view => view.state.field(this.field),
      class: `cm-${type}Gutter`,
    });

    this.#transformer = transformer;
  }

  static override accepts(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
    return uiSourceCode.contentType().hasScripts();
  }

  private getLineMap(): Workspace.UISourceCode.LineColumnProfileMap|undefined {
    const uiSourceCodeProfileMap = this.uiSourceCode.getDecorationData(type);
    if (!uiSourceCodeProfileMap) {
      return undefined;
    }

    return Workspace.UISourceCode.createMappedProfileData(uiSourceCodeProfileMap, (line, column) => {
      const editorLocation = this.#transformer.uiLocationToEditorLocation(line, column);
      return [editorLocation.lineNumber, editorLocation.columnNumber];
    });
  }

  override editorExtension(): CodeMirror.Extension {
    const map = this.getLineMap();
    return this.compartment.of(
        !map ? [] : [this.field.init(state => markersFromProfileData(map, state, type)), this.gutter, theme]);
  }

  override decorationChanged(type: Workspace.UISourceCode.DecoratorType, editor: TextEditor.TextEditor.TextEditor):
      void {
    const installed = Boolean(editor.state.field(this.field, false));
    const map = this.getLineMap();
    if (!map) {
      if (installed) {
        editor.dispatch({effects: this.compartment.reconfigure([])});
      }
    } else if (!installed) {
      editor.dispatch({
        effects: this.compartment.reconfigure(
            [this.field.init(state => markersFromProfileData(map, state, type)), this.gutter, theme]),
      });
    } else {
      editor.dispatch({effects: this.updateEffect.of(map)});
    }
  }
};

const theme = CodeMirror.EditorView.baseTheme({
  '.cm-line::selection': {
    backgroundColor: 'transparent',
    color: 'currentColor',
  },
  '.cm-performanceGutter': {
    width: '60px',
    backgroundColor: 'var(--sys-color-cdt-base-container)',
    marginLeft: '3px',
  },
  '.cm-memoryGutter': {
    width: '48px',
    backgroundColor: 'var(--sys-color-cdt-base-container)',
    marginLeft: '3px',
  },
  '.cm-profileMarker': {
    textAlign: 'right',
    paddingRight: '3px',
  },
  '.cm-profileMarker .cm-units': {
    color: 'var(--sys-color-token-subtle)',
    fontSize: '75%',
    marginLeft: '3px',
  },
});

export const MemoryProfilePlugin = makeLineLevelProfilePlugin(Workspace.UISourceCode.DecoratorType.MEMORY);

export const PerformanceProfilePlugin = makeLineLevelProfilePlugin(Workspace.UISourceCode.DecoratorType.PERFORMANCE);
