import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data';

import { getDisplayedFieldValue } from './LogLine';
import { LogListModel } from './processing';

let ctx: CanvasRenderingContext2D | null = null;
let gridSize = 8;
let paddingBottom = gridSize * 0.75;
let lineHeight = 22;
let measurementMode: 'canvas' | 'dom' = 'canvas';
const iconWidth = 24;

// Controls the space between fields in the log line, timestamp, level, displayed fields, and log line body
export const FIELD_GAP_MULTIPLIER = 1.5;

export const getLineHeight = () => lineHeight;

export function init(theme: GrafanaTheme2) {
  const font = `${theme.typography.fontSize}px ${theme.typography.fontFamilyMonospace}`;
  const letterSpacing = theme.typography.body.letterSpacing;

  initDOMmeasurement(font, letterSpacing);
  initCanvasMeasurement(font, letterSpacing);

  gridSize = theme.spacing.gridSize;
  paddingBottom = gridSize * 0.75;
  lineHeight = theme.typography.fontSize * theme.typography.body.lineHeight;

  widthMap = new Map<number, number>();
  resetLogLineSizes();

  determineMeasurementMode();

  return true;
}

function determineMeasurementMode() {
  if (!ctx) {
    measurementMode = 'dom';
    return;
  }
  const canvasCharWidth = ctx.measureText('e').width;
  const domCharWidth = measureTextWidthWithDOM('e');
  const diff = domCharWidth - canvasCharWidth;
  if (diff >= 0.1) {
    console.warn('Virtualized log list: falling back to DOM for measurement');
    measurementMode = 'dom';
  }
}

function initCanvasMeasurement(font: string, letterSpacing: string | undefined) {
  const canvas = document.createElement('canvas');
  ctx = canvas.getContext('2d');
  if (!ctx) {
    return;
  }
  ctx.font = font;
  ctx.fontKerning = 'normal';
  ctx.fontStretch = 'normal';
  ctx.fontVariantCaps = 'normal';
  ctx.textRendering = 'optimizeLegibility';
  if (letterSpacing) {
    ctx.letterSpacing = letterSpacing;
  }
}

const span = document.createElement('span');
function initDOMmeasurement(font: string, letterSpacing: string | undefined) {
  span.style.font = font;
  span.style.visibility = 'hidden';
  span.style.position = 'absolute';
  span.style.wordBreak = 'break-all';
  if (letterSpacing) {
    span.style.letterSpacing = letterSpacing;
  }
}

let widthMap = new Map<number, number>();
export function measureTextWidth(text: string): number {
  if (!ctx) {
    throw new Error(`Measuring context canvas is not initialized. Call init() before.`);
  }
  const key = text.length;

  const storedWidth = widthMap.get(key);
  if (storedWidth) {
    return storedWidth;
  }

  const width = measurementMode === 'canvas' ? ctx.measureText(text).width : measureTextWidthWithDOM(text);
  widthMap.set(key, width);

  return width;
}

function measureTextWidthWithDOM(text: string) {
  span.textContent = text;

  document.body.appendChild(span);
  const width = span.getBoundingClientRect().width;
  document.body.removeChild(span);

  return width;
}

export function measureTextHeight(text: string, maxWidth: number, beforeWidth = 0) {
  let logLines = 0;
  const charWidth = measureTextWidth('e');
  let logLineCharsLength = Math.round(maxWidth / charWidth);
  const firstLineCharsLength = Math.floor((maxWidth - beforeWidth) / charWidth) - 2 * charWidth;
  const textLines = text.split('\n');

  // Skip unnecessary measurements
  if (textLines.length === 1 && text.length < firstLineCharsLength) {
    return {
      lines: 1,
      height: lineHeight + paddingBottom,
    };
  }

  for (const textLine of textLines) {
    for (let start = 0; start < textLine.length; ) {
      let testLogLine: string;
      let width = 0;
      let delta = 0;
      let availableWidth = maxWidth - beforeWidth;
      do {
        testLogLine = textLine.substring(start, start + logLineCharsLength - delta);
        width = measureTextWidth(testLogLine);
        delta += 1;
      } while (width >= availableWidth);
      if (beforeWidth) {
        beforeWidth = 0;
      }
      logLines += 1;
      start += testLogLine.length;
    }
  }

  const height = logLines * lineHeight + paddingBottom;

  return {
    lines: logLines,
    height,
  };
}

interface DisplayOptions {
  wrap: boolean;
  showTime: boolean;
}

export function getLogLineSize(
  logs: LogListModel[],
  container: HTMLDivElement | null,
  displayedFields: string[],
  { wrap, showTime }: DisplayOptions,
  index: number
) {
  if (!container) {
    return 0;
  }
  // !logs[index] means the line is not yet loaded by infinite scrolling
  if (!wrap || !logs[index]) {
    return lineHeight + paddingBottom;
  }
  const storedSize = retrieveLogLineSize(logs[index].uid, container);
  if (storedSize) {
    return storedSize;
  }

  let textToMeasure = '';
  const gap = gridSize * FIELD_GAP_MULTIPLIER;
  let optionsWidth = 0;
  if (showTime) {
    optionsWidth += gap;
    textToMeasure += logs[index].timestamp;
  }
  if (logs[index].logLevel) {
    optionsWidth += gap;
    textToMeasure += logs[index].logLevel;
  }
  for (const field of displayedFields) {
    textToMeasure = getDisplayedFieldValue(field, logs[index]) + textToMeasure;
  }
  if (!displayedFields.length) {
    textToMeasure += logs[index].body;
  }

  const { height } = measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth);
  return height;
}

export function hasUnderOrOverflow(element: HTMLDivElement, calculatedHeight?: number): number | null {
  const height = calculatedHeight ?? element.clientHeight;
  if (element.scrollHeight > height) {
    return element.scrollHeight;
  }
  const child = element.children[1];
  if (child instanceof HTMLDivElement && child.clientHeight < height) {
    return child.clientHeight;
  }
  return null;
}

const scrollBarWidth = getScrollbarWidth();

export function getLogContainerWidth(container: HTMLDivElement) {
  return container.clientWidth - scrollBarWidth - iconWidth;
}

export function getScrollbarWidth() {
  const hiddenDiv = document.createElement('div');

  hiddenDiv.style.width = '100px';
  hiddenDiv.style.height = '100px';
  hiddenDiv.style.overflow = 'scroll';
  hiddenDiv.style.position = 'absolute';
  hiddenDiv.style.top = '-9999px';

  document.body.appendChild(hiddenDiv);
  const width = hiddenDiv.offsetWidth - hiddenDiv.clientWidth;
  document.body.removeChild(hiddenDiv);

  return width;
}

let logLineSizesMap = new Map<string, number>();
export function resetLogLineSizes() {
  logLineSizesMap = new Map<string, number>();
}

export function storeLogLineSize(id: string, container: HTMLDivElement, height: number) {
  const key = `${id}_${getLogContainerWidth(container)}`;
  logLineSizesMap.set(key, height);
}

export function retrieveLogLineSize(id: string, container: HTMLDivElement) {
  const key = `${id}_${getLogContainerWidth(container)}`;
  return logLineSizesMap.get(key);
}

export interface ScrollToLogsEventPayload {
  scrollTo: 'top' | 'bottom';
}

export class ScrollToLogsEvent extends BusEventWithPayload<ScrollToLogsEventPayload> {
  static type = 'logs-panel-scroll-to';
}
