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

import '../../../ui/components/settings/settings.js';

import * as Common from '../../../core/common/common.js';
import * as i18n from '../../../core/i18n/i18n.js';
import * as Platform from '../../../core/platform/platform.js';
import * as Trace from '../../../models/trace/trace.js';
import * as TraceBounds from '../../../services/trace_bounds/trace_bounds.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../../ui/legacy/theme_support/theme_support.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';

import {AnnotationHoverOut, HoverAnnotation, RemoveAnnotation, RevealAnnotation} from './Sidebar.js';
import sidebarAnnotationsTabStyles from './sidebarAnnotationsTab.css.js';

const {html, render} = Lit;

const diagramImageUrl = new URL('../../../Images/performance-panel-diagram.svg', import.meta.url).toString();
const entryLabelImageUrl = new URL('../../../Images/performance-panel-entry-label.svg', import.meta.url).toString();
const timeRangeImageUrl = new URL('../../../Images/performance-panel-time-range.svg', import.meta.url).toString();
const deleteAnnotationImageUrl =
    new URL('../../../Images/performance-panel-delete-annotation.svg', import.meta.url).toString();

const UIStrings = {
  /**
   * @description Title for entry label.
   */
  annotationGetStarted: 'Annotate a trace for yourself and others',
  /**
   * @description Title for entry label.
   */
  entryLabelTutorialTitle: 'Label an item',
  /**
   * @description Text for how to create an entry label.
   */
  entryLabelTutorialDescription: 'Double-click or press Enter on an item and type to create an item label.',
  /**
   * @description  Title for diagram.
   */
  entryLinkTutorialTitle: 'Connect two items',
  /**
   * @description Text for how to create a diagram between entries.
   */
  entryLinkTutorialDescription:
      'Double-click on an item, click on the adjacent rightward arrow, then select the destination item.',
  /**
   * @description  Title for time range.
   */
  timeRangeTutorialTitle: 'Define a time range',
  /**
   * @description Text for how to create a time range selection and add note.
   */
  timeRangeTutorialDescription: 'Shift-drag in the flamechart then type to create a time range annotation.',
  /**
   * @description  Title for deleting annotations.
   */
  deleteAnnotationTutorialTitle: 'Delete an annotation',
  /**
   * @description Text for how to access an annotation delete function.
   */
  deleteAnnotationTutorialDescription:
      'Hover over the list in the sidebar with Annotations tab selected to access the delete function.',
  /**
   * @description Text used to describe the delete button to screen readers.
   * @example {"A paint event annotated with the text hello world"} PH1
   **/
  deleteButton: 'Delete annotation: {PH1}',
  /**
   * @description label used to describe an annotation on an entry
   * @example {Paint} PH1
   * @example {"Hello world"} PH2
   */
  entryLabelDescriptionLabel: 'A "{PH1}" event annotated with the text "{PH2}"',
  /**
   * @description label used to describe a time range annotation
   * @example {2.5 milliseconds} PH1
   * @example {13.5 milliseconds} PH2
   */
  timeRangeDescriptionLabel: 'A time range starting at {PH1} and ending at {PH2}',
  /**
   * @description label used to describe a link from one entry to another.
   * @example {Paint} PH1
   * @example {Recalculate styles} PH2
   */
  entryLinkDescriptionLabel: 'A link between a "{PH1}" event and a "{PH2}" event',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/SidebarAnnotationsTab.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export interface SidebarAnnotationsTabViewInput {
  annotations: readonly Trace.Types.File.Annotation[];
  annotationsHiddenSetting: Common.Settings.Setting<boolean>;
  annotationEntryToColorMap: ReadonlyMap<Trace.Types.Events.Event|Trace.Types.Events.LegacyTimelineFrame, string>;
  onAnnotationClick: (annotation: Trace.Types.File.Annotation) => void;
  onAnnotationHover: (annotation: Trace.Types.File.Annotation) => void;
  onAnnotationHoverOut: () => void;
  onAnnotationDelete: (annotation: Trace.Types.File.Annotation) => void;
}

export class SidebarAnnotationsTab extends UI.Widget.Widget {
  #annotations: Trace.Types.File.Annotation[] = [];
  // A map with annotated entries and the colours that are used to display them in the FlameChart.
  // We need this map to display the entries in the sidebar with the same colours.
  #annotationEntryToColorMap = new Map<Trace.Types.Events.Event|Trace.Types.Events.LegacyTimelineFrame, string>();

  readonly #annotationsHiddenSetting: Common.Settings.Setting<boolean>;
  #view: typeof DEFAULT_VIEW;

  constructor(view = DEFAULT_VIEW) {
    super();
    this.#view = view;
    this.#annotationsHiddenSetting = Common.Settings.Settings.instance().moduleSetting('annotations-hidden');
  }

  deduplicatedAnnotations(): readonly Trace.Types.File.Annotation[] {
    return this.#annotations;
  }

  setData(data: {
    annotations: Trace.Types.File.Annotation[],
    annotationEntryToColorMap: Map<Trace.Types.Events.Event, string>,
  }): void {
    this.#annotations = this.#processAnnotationsList(data.annotations);
    this.#annotationEntryToColorMap = data.annotationEntryToColorMap;
    this.requestUpdate();
  }

  #processAnnotationsList(annotations: Trace.Types.File.Annotation[]): Trace.Types.File.Annotation[] {
    // When an entry is double-clicked, we create two annotations (a label and an entries connection) for the user to choose from.
    // The one not selected is deleted when the user makes their selection.
    // To avoid excessive activity in the sidebar (adding and removing annotations), only show one 'not started' annotation associated with an entry.
    //
    // If we encounter an annotation for an entry that hasn't started creation, add that entry to the 'entriesWithNotStartedAnnotation'
    // set. This allows us to filter out any subsequent not started annotations for the same entry.
    const entriesWithNotStartedAnnotation = new Set();

    const processedAnnotations = annotations.filter(annotation => {
      if (this.#isAnnotationCreationStarted(annotation)) {
        return true;
      }

      if (annotation.type === 'ENTRIES_LINK' || annotation.type === 'ENTRY_LABEL') {
        const annotationEntry = annotation.type === 'ENTRIES_LINK' ? annotation.entryFrom : annotation.entry;

        if (entriesWithNotStartedAnnotation.has(annotationEntry)) {
          return false;
        }

        entriesWithNotStartedAnnotation.add(annotationEntry);
      }

      return true;
    });

    // Sort annotations by timestamp.
    processedAnnotations.sort(
        (firstAnnotation, secondAnnotation) =>
            this.#getAnnotationTimestamp(firstAnnotation) - this.#getAnnotationTimestamp(secondAnnotation),
    );

    return processedAnnotations;
  }

  #getAnnotationTimestamp(annotation: Trace.Types.File.Annotation): Trace.Types.Timing.Micro {
    switch (annotation.type) {
      case 'ENTRY_LABEL': {
        return annotation.entry.ts;
      }
      case 'ENTRIES_LINK': {
        return annotation.entryFrom.ts;
      }
      case 'TIME_RANGE': {
        return annotation.bounds.min;
      }
      default: {
        Platform.assertNever(annotation, `Invalid annotation type ${annotation}`);
      }
    }
  }

  #isAnnotationCreationStarted(annotation: Trace.Types.File.Annotation): boolean {
    // Consider the annotation not started if:
    // ENTRY_LABEL - label is empty
    // ENTRIES_LINK - the connection annotation does not have the 'to' entry
    // TIME_RANGE - range is over zero
    switch (annotation.type) {
      case 'ENTRY_LABEL': {
        return annotation.label.length > 0;
      }
      case 'ENTRIES_LINK': {
        return Boolean(annotation.entryTo);
      }
      case 'TIME_RANGE': {
        return annotation.bounds.range > 0;
      }
    }
  }

  override performUpdate(): void {
    const input: SidebarAnnotationsTabViewInput = {
      annotations: this.#annotations,
      annotationsHiddenSetting: this.#annotationsHiddenSetting,
      annotationEntryToColorMap: this.#annotationEntryToColorMap,
      onAnnotationClick: (annotation: Trace.Types.File.Annotation) => {
        this.contentElement.dispatchEvent(new RevealAnnotation(annotation));
      },
      onAnnotationHover: (annotation: Trace.Types.File.Annotation) => {
        this.contentElement.dispatchEvent(new HoverAnnotation(annotation));
      },
      onAnnotationHoverOut: () => {
        this.contentElement.dispatchEvent(new AnnotationHoverOut());
      },
      onAnnotationDelete: (annotation: Trace.Types.File.Annotation) => {
        this.contentElement.dispatchEvent(new RemoveAnnotation(annotation));
      },
    };
    this.#view(input, {}, this.contentElement);
  }
}

function detailedAriaDescriptionForAnnotation(annotation: Trace.Types.File.Annotation): string {
  switch (annotation.type) {
    case 'ENTRY_LABEL': {
      const name = Trace.Name.forEntry(annotation.entry);
      return i18nString(UIStrings.entryLabelDescriptionLabel, {
        PH1: name,
        PH2: annotation.label,
      });
    }
    case 'TIME_RANGE': {
      const from = i18n.TimeUtilities.formatMicroSecondsAsMillisFixedExpanded(annotation.bounds.min);
      const to = i18n.TimeUtilities.formatMicroSecondsAsMillisFixedExpanded(annotation.bounds.max);
      return i18nString(UIStrings.timeRangeDescriptionLabel, {
        PH1: from,
        PH2: to,
      });
    }
    case 'ENTRIES_LINK': {
      if (!annotation.entryTo) {
        // Only label it if it is completed.
        return '';
      }
      const nameFrom = Trace.Name.forEntry(annotation.entryFrom);
      const nameTo = Trace.Name.forEntry(annotation.entryTo);
      return i18nString(UIStrings.entryLinkDescriptionLabel, {
        PH1: nameFrom,
        PH2: nameTo,
      });
    }
    default:
      Platform.assertNever(annotation, 'Unsupported annotation');
  }
}

function findTextColorForContrast(bgColorText: string): string {
  const bgColor = Common.Color.parse(bgColorText)?.asLegacyColor();
  // Let's default to black text, since the entries' titles are black in the flamechart.
  const darkColorToken = '--app-color-performance-sidebar-label-text-dark';
  const darkColorText =
      Common.Color.parse(ThemeSupport.ThemeSupport.instance().getComputedValue(darkColorToken))?.asLegacyColor();
  if (!bgColor || !darkColorText) {
    // This part of code shouldn't be reachable unless background color is invalid or something wrong with the color
    // parsing. If so let's fall back to the dark color,
    return `var(${darkColorToken})`;
  }

  const contrastRatio = Common.ColorUtils.contrastRatio(bgColor.rgba(), darkColorText.rgba());
  return contrastRatio >= 4.5 ? `var(${darkColorToken})` : 'var(--app-color-performance-sidebar-label-text-light)';
}

function renderAnnotationIdentifier(
    annotation: Trace.Types.File.Annotation,
    annotationEntryToColorMap: ReadonlyMap<Trace.Types.Events.Event|Trace.Types.Events.LegacyTimelineFrame, string>):
    Lit.LitTemplate {
  switch (annotation.type) {
    case 'ENTRY_LABEL': {
      const entryName = Trace.Name.forEntry(annotation.entry);
      const backgroundColor = annotationEntryToColorMap.get(annotation.entry) ?? '';
      const color = findTextColorForContrast(backgroundColor);
      const styleForAnnotationIdentifier = {
        backgroundColor,
        color,
      };
      return html`
            <span class="annotation-identifier" style=${Lit.Directives.styleMap(styleForAnnotationIdentifier)}>
              ${entryName}
            </span>
      `;
    }
    case 'TIME_RANGE': {
      const minTraceBoundsMilli =
          TraceBounds.TraceBounds.BoundsManager.instance().state()?.milli.entireTraceBounds.min ?? 0;

      const timeRangeStartInMs =
          Math.round(Trace.Helpers.Timing.microToMilli(annotation.bounds.min) - minTraceBoundsMilli);
      const timeRangeEndInMs =
          Math.round(Trace.Helpers.Timing.microToMilli(annotation.bounds.max) - minTraceBoundsMilli);

      return html`
            <span class="annotation-identifier time-range">
              ${timeRangeStartInMs} - ${timeRangeEndInMs} ms
            </span>
      `;
    }
    case 'ENTRIES_LINK': {
      const entryFromName = Trace.Name.forEntry(annotation.entryFrom);
      const fromBackgroundColor = annotationEntryToColorMap.get(annotation.entryFrom) ?? '';
      const fromTextColor = findTextColorForContrast(fromBackgroundColor);
      const styleForFromAnnotationIdentifier = {
        backgroundColor: fromBackgroundColor,
        color: fromTextColor,
      };
      // clang-format off
      return html`
        <div class="entries-link">
          <span class="annotation-identifier" style=${Lit.Directives.styleMap(styleForFromAnnotationIdentifier)}>
            ${entryFromName}
          </span>
          <devtools-icon name="arrow-forward" class="inline-icon large">
          </devtools-icon>
          ${renderEntryToIdentifier(annotation, annotationEntryToColorMap)}
        </div>
    `;
      // clang-format on
    }
    default:
      Platform.assertNever(annotation, 'Unsupported annotation type');
  }
}

/**
 * Renders the Annotation 'identifier' or 'name' in the annotations list.
 * This is the text rendered before the annotation label that we use to identify the annotation.
 *
 * Annotations identifiers for different annotations:
 * Entry label -> Entry name
 * Labelled range -> Start to End Range of the label in ms
 * Connected entries -> Connected entries names
 *
 * All identifiers have a different colour background.
 */
function renderEntryToIdentifier(
    annotation: Trace.Types.File.EntriesLinkAnnotation,
    annotationEntryToColorMap: ReadonlyMap<Trace.Types.Events.Event|Trace.Types.Events.LegacyTimelineFrame, string>):
    Lit.LitTemplate {
  if (annotation.entryTo) {
    const entryToName = Trace.Name.forEntry(annotation.entryTo);
    const toBackgroundColor = annotationEntryToColorMap.get(annotation.entryTo) ?? '';
    const toTextColor = findTextColorForContrast(toBackgroundColor);
    const styleForToAnnotationIdentifier = {
      backgroundColor: toBackgroundColor,
      color: toTextColor,
    };
    // clang-format off
    return html`
      <span class="annotation-identifier" style=${Lit.Directives.styleMap(styleForToAnnotationIdentifier)}>
        ${entryToName}
      </span>`;
    // clang-format on
  }
  return Lit.nothing;
}

function jslogForAnnotation(annotation: Trace.Types.File.Annotation): string {
  switch (annotation.type) {
    case 'ENTRY_LABEL':
      return 'entry-label';
    case 'TIME_RANGE':
      return 'time-range';
    case 'ENTRIES_LINK':
      return 'entries-link';
    default:
      Platform.assertNever(annotation, 'unknown annotation type');
  }
}

function renderTutorial(): Lit.LitTemplate {
  // clang-format off
  return html`<div class="annotation-tutorial-container">
    ${i18nString(UIStrings.annotationGetStarted)}
      <div class="tutorial-card">
        <div class="tutorial-image"><img src=${entryLabelImageUrl}></div>
        <div class="tutorial-title">${i18nString(UIStrings.entryLabelTutorialTitle)}</div>
        <div class="tutorial-description">${i18nString(UIStrings.entryLabelTutorialDescription)}</div>
      </div>
      <div class="tutorial-card">
        <div class="tutorial-image"><img src=${diagramImageUrl}></div>
        <div class="tutorial-title">${i18nString(UIStrings.entryLinkTutorialTitle)}</div>
        <div class="tutorial-description">${i18nString(UIStrings.entryLinkTutorialDescription)}</div>
      </div>
      <div class="tutorial-card">
        <div class="tutorial-image"><img src=${timeRangeImageUrl}></div>
        <div class="tutorial-title">${i18nString(UIStrings.timeRangeTutorialTitle)}</div>
        <div class="tutorial-description">${i18nString(UIStrings.timeRangeTutorialDescription)}</div>
      </div>
      <div class="tutorial-card">
        <div class="tutorial-image"><img src=${deleteAnnotationImageUrl}></div>
        <div class="tutorial-title">${i18nString(UIStrings.deleteAnnotationTutorialTitle)}</div>
        <div class="tutorial-description">${i18nString(UIStrings.deleteAnnotationTutorialDescription)}</div>
      </div>
    </div>` ;
  // clang-format on
}

export const DEFAULT_VIEW: (input: SidebarAnnotationsTabViewInput, output: object, target: HTMLElement) => void =
    (input, _output, target) => {
      // clang-format off
      render(html`
      <style>${sidebarAnnotationsTabStyles}</style>
      <span class="annotations">
        ${input.annotations.length === 0 ? renderTutorial():
          html`
            ${input.annotations.map(annotation => {
              const label = detailedAriaDescriptionForAnnotation(annotation);
              return html`
                <div class="annotation-container"
                  @click=${() => input.onAnnotationClick(annotation)}
                  @mouseover=${() => (annotation.type === 'ENTRY_LABEL') ? input.onAnnotationHover(annotation): null}
                  @mouseout=${() => (annotation.type === 'ENTRY_LABEL') ? input.onAnnotationHoverOut() : null}
                  aria-label=${label}
                  tabindex="0"
                  jslog=${VisualLogging.item(`timeline.annotation-sidebar.annotation-${jslogForAnnotation(annotation)}`).track({click: true, resize: true})}
                >
                  <div class="annotation">
                    ${renderAnnotationIdentifier(annotation, input.annotationEntryToColorMap)}
                    <span class="label">
                      ${(annotation.type === 'ENTRY_LABEL' || annotation.type === 'TIME_RANGE') ? annotation.label : ''}
                    </span>
                  </div>
                  <button class="delete-button" aria-label=${i18nString(UIStrings.deleteButton, {PH1: label})} @click=${(event: Event) => {
                    // Stop propagation to not zoom into the annotation when
                    // the delete button is clicked
                    event.stopPropagation();
                    input.onAnnotationDelete(annotation);
                  }} jslog=${VisualLogging.action('timeline.annotation-sidebar.delete').track({click: true})}>
                    <devtools-icon class="bin-icon extra-large" name="bin"></devtools-icon>
                  </button>
                </div>`;
            })}
            <setting-checkbox class="visibility-setting" .data=${{
              setting: input.annotationsHiddenSetting,
              textOverride: 'Hide annotations',
            }}>
            </setting-checkbox>`
    }
    </span>`,
  target);
};
