// 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 * as i18n from '../../../../core/i18n/i18n.js';
import type * as Trace from '../../../../models/trace/trace.js';
import * as UI from '../../../../ui/legacy/legacy.js';
import {Directives, html, nothing, render, type TemplateResult} from '../../../../ui/lit/lit.js';

import timespanBreakdownOverlayStyles from './timespanBreakdownOverlay.css.js';

export interface Input {
  sections: Trace.Types.Overlays.TimespanBreakdownEntryBreakdown[]|null;
  positions: SectionPosition[];
  left: number|null;
  width: number|null;
  maxHeight: number|null;
  top: number|null;
  className: string;
}
export interface SectionPosition {
  left: number|null;
  width: number|null;
}

type View = (input: Input, _output: undefined, target: HTMLElement) => void;

const renderSection =
    (section: Trace.Types.Overlays.TimespanBreakdownEntryBreakdown, position: SectionPosition): TemplateResult => {
      const style = Directives.styleMap(
          {left: position ? `${position.left}px` : undefined, width: position ? `${position.width}px` : undefined});

      // clang-format off
    return html`
      <div class="timespan-breakdown-overlay-section" style=${style}>
        <div class="timespan-breakdown-overlay-label">
          ${
            section.showDuration ?
                html`<span class="duration-text">${i18n.TimeUtilities.formatMicroSecondsAsMillisFixed(section.bounds.range)}</span> ` :
                nothing
          }
          <span class="section-label-text">${section.label}</span>
        </div>
      </div>`;
      // clang-format on
    };

export const DEFAULT_VIEW = (input: Input, _output: undefined, target: HTMLElement): void => {
  const style = Directives.styleMap({
    left: input.left ? `${input.left}px` : undefined,
    width: input.width ? `${input.width}px` : undefined,
    top: input.top ? `${input.top}px` : undefined,
    maxHeight: input.maxHeight ? `${input.maxHeight}px` : undefined,
    position: 'relative'
  });
  // clang-format off
  render(
      html`
        <style>${timespanBreakdownOverlayStyles}</style>
        <div style=${style} class=${input.className}>
          ${input.sections?.map((curr, index) => {
            return renderSection(curr, input.positions[index]);
          })}
        </div>`,
      target, {container: {classes: ['devtools-timespan-breakdown-overlay']}});
  // clang-format off
};

export class TimespanBreakdownOverlay extends UI.Widget.Widget {
  #canvasRect: DOMRect|null = null;
  #sections: Trace.Types.Overlays.TimespanBreakdownEntryBreakdown[]|null = null;
  #sectionsPositions: SectionPosition[] = [];
  #left: number|null = null;
  #width: number|null = null;
  #maxHeight: number|null = null;
  #top: number|null = null;

  #view: View;

  constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
    super(element);
    this.#view = view;
    this.requestUpdate();
  }

  set top(top: number) {
    this.#top = top;
    this.requestUpdate();
  }

  set maxHeight(maxHeight: number) {
    this.#maxHeight = maxHeight;
    this.requestUpdate();
  }

  set width(width: number) {
    this.#width = width;
    this.requestUpdate();
  }

  set left(left: number) {
    this.#left = left;
    this.requestUpdate();
  }

  set isBelowEntry(isBelow: boolean) {
    this.element.classList.toggle('is-below', isBelow);
  }

  set canvasRect(rect: DOMRect|null) {
    if (this.#canvasRect && rect && this.#canvasRect.width === rect.width && this.#canvasRect.height === rect.height) {
      return;
    }
    this.#canvasRect = rect;
    this.requestUpdate();
  }

  set widths(widths: SectionPosition[]) {
    if (widths === this.#sectionsPositions) {
      return;
    }

    this.#sectionsPositions = widths;
    this.requestUpdate();
  }

  set sections(sections: Trace.Types.Overlays.TimespanBreakdownEntryBreakdown[]|null) {
    if (sections === this.#sections) {
      return;
    }
    this.#sections = sections;
    this.requestUpdate();
  }

  /**
   * We use this method after the overlay has been positioned in order to move
   * the section label as required to keep it on screen.
   * If the label is off to the left or right, we fix it to that corner and
   * align the text so the label is visible as long as possible.
   */
  checkSectionLabelPositioning(): void {
    const sections = this.element.querySelectorAll<HTMLElement>('.timespan-breakdown-overlay-section');
    if (!sections) {
      return;
    }

    if (!this.#canvasRect) {
      return;
    }

    // On the RHS of the panel a scrollbar can be shown which means the canvas
    // has a 9px gap on the right hand edge. We use this value when calculating
    // values and label positioning from the left hand side in order to be
    // consistent on both edges of the UI.
    const paddingForScrollbar = 9;

    // Fetch the rects for each section and label now, rather than in the loop,
    // to avoid causing a bunch of recalcStyles
    const sectionLayoutData = new Map<HTMLElement, {sectionRect: DOMRect, labelRect: DOMRect, label: HTMLElement}>();
    for (const section of sections) {
      const label = section.querySelector<HTMLElement>('.timespan-breakdown-overlay-label');
      if (!label) {
        continue;
      }
      const sectionRect = section.getBoundingClientRect();
      const labelRect = label.getBoundingClientRect();
      sectionLayoutData.set(section, {sectionRect, labelRect, label});
    }

    const minSectionWidthToShowAnyLabel = 30;

    // Align the labels for all the breakdown sections.
    for (const section of sections) {
      const layoutData = sectionLayoutData.get(section);
      if (!layoutData) {
        break;
      }
      const {labelRect, sectionRect, label} = layoutData;

      const labelHidden = sectionRect.width < minSectionWidthToShowAnyLabel;
      // Subtract 5 from the section width to allow a tiny bit of padding.
      const labelTruncated = sectionRect.width - 5 <= labelRect.width;
      // We differentiate between hidden + truncated; if it is truncated we
      // will show the text with ellipsis for overflow, but if the section is
      // really small we just hide the label entirely.
      label.classList.toggle('labelHidden', labelHidden);
      label.classList.toggle('labelTruncated', labelTruncated);

      if (labelHidden || labelTruncated) {
        // Label is hidden or doesn't fully fit, so we don't need to do the
        // logic to left/right align if it needs it.
        continue;
      }

      // Check if label is off the LHS of the screen.
      const labelLeftMarginToCenter = (sectionRect.width - labelRect.width) / 2;
      const newLabelX = sectionRect.x + labelLeftMarginToCenter;

      const labelOffLeftOfScreen = newLabelX < this.#canvasRect.x;
      label.classList.toggle('offScreenLeft', labelOffLeftOfScreen);

      // Check if label is off the RHS of the screen
      const rightBound = this.#canvasRect.x + this.#canvasRect.width;
      // The label's right hand edge is the gap from the left of the range to the
      // label, and then the width of the label.
      const labelRightEdge = sectionRect.x + labelLeftMarginToCenter + labelRect.width;
      const labelOffRightOfScreen = labelRightEdge > rightBound;
      label.classList.toggle('offScreenRight', labelOffRightOfScreen);

      if (labelOffLeftOfScreen) {
        // If the label is off the left of the screen, we adjust by the
        // difference between the X that represents the start of the cavnas, and
        // the X that represents the start of the overlay.
        // We then take the absolute value of this - because if the canvas starts
        // at 0, and the overlay is -200px, we have to adjust the label by +200.
        // Add on 9 pixels to pad from the left; this is the width of the sidebar
        // on the RHS so we match it so the label is equally padded on either
        // side.
        label.style.marginLeft = `${Math.abs(this.#canvasRect.x - sectionRect.x) + paddingForScrollbar}px`;

      } else if (labelOffRightOfScreen) {
        // To calculate how far left to push the label, we take the right hand
        // bound (the canvas width and subtract the label's width).
        // Finally, we subtract the X position of the overlay (if the overlay is
        // 200px within the view, we don't need to push the label that 200px too
        // otherwise it will be off-screen)
        const leftMargin = rightBound - labelRect.width - sectionRect.x;

        label.style.marginLeft = `${leftMargin}px`;

      } else {
        // Keep the label central.
        label.style.marginLeft = `${labelLeftMarginToCenter}px`;
      }
    }
  }

  override performUpdate(): void {
    let className = 'timeline-segment-container';

    if (this.#sections) {
      if(this.#sections.length % 2 === 0) {
        className += ' even-number-of-sections';
      } else {
        className += ' odd-number-of-sections';
      }
    }

    this.#view({sections: this.#sections,
      positions: this.#sectionsPositions,
      left: this.#left,
      width: this.#width,
      top: this.#top,
      maxHeight: this.#maxHeight,
      className
    }, undefined, this.contentElement);

    this.checkSectionLabelPositioning();
  }
}
