/*
 * If not stated otherwise in this file or this component's LICENSE file the
 * following copyright and licenses apply:
 *
 * Copyright 2023 Comcast Cable Communications Management, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the License);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { assertTruthy } from '../../utils.js';
import type { Stage } from '../Stage.js';
import type {
  CanvasRenderInfo,
  FontHandler,
  TextLineStruct,
  TextRenderInfo,
} from './TextRenderer.js';
import * as CanvasFontHandler from './CanvasFontHandler.js';
import type { CoreTextNodeProps } from '../CoreTextNode.js';
import { getLayoutCacheKey, hasZeroWidthSpace } from './Utils.js';
import { mapTextLayout } from './TextLayoutEngine.js';
import { parseRichText, ParseResult } from './RichTextParser.js';
import { normalizeCanvasColor } from '../lib/colorCache.js';

const type = 'canvas' as const;
const font: FontHandler = CanvasFontHandler;

let stage: Stage | null = null;
let canvas: HTMLCanvasElement | OffscreenCanvas | null = null;
let context:
  | CanvasRenderingContext2D
  | OffscreenCanvasRenderingContext2D
  | null = null;

// Whether the drawing canvas is an OffscreenCanvas (supports transferToImageBitmap)
let isOffscreen = false;

// Separate canvas and context for text measurements
let measureCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
type MeasureContext =
  | CanvasRenderingContext2D
  | OffscreenCanvasRenderingContext2D
  | null;
let measureContext: MeasureContext = null;

// Cache for text layout calculations
const renderInfoCache = new Map<string, CanvasRenderInfo>();

// Pre-allocated rich text parse result — reused across renderText calls.
// Safe because renderText is synchronous and JS is single-threaded.
const _richTextResult = new ParseResult();

// Initialize the Text Renderer
const init = (_stage: Stage): void => {
  stage = _stage;
  const dpr = stage.options.devicePhysicalPixelRatio;

  // Drawing canvas and context
  canvas = stage.platform.createOffscreenCanvas();
  if (canvas !== null) {
    isOffscreen = true;
    measureCanvas = stage.platform.createOffscreenCanvas();
  } else {
    isOffscreen = false;
    canvas = stage.platform.createCanvas();
    measureCanvas = stage.platform.createCanvas();
  }

  context = canvas.getContext('2d') as
    | CanvasRenderingContext2D
    | OffscreenCanvasRenderingContext2D;
  assertTruthy(context, '.getContext(2d) failed');

  context.setTransform(dpr, 0, 0, dpr, 0, 0);
  context.textRendering = 'optimizeSpeed';

  assertTruthy(measureCanvas, 'measureCanvas is not initialized');
  measureContext = measureCanvas.getContext('2d') as MeasureContext;
  assertTruthy(measureContext, '.getContext(2d) failed');
  measureContext.setTransform(dpr, 0, 0, dpr, 0, 0);
  measureContext.textRendering = 'optimizeSpeed';

  // Set up a minimal size for the measuring canvas since we only use it for measurements
  measureCanvas.width = 1;
  measureCanvas.height = 1;

  CanvasFontHandler.init(context, measureContext);
};

/**
 * Canvas text renderer
 *
 * @param stage - Stage instance for font resolution
 * @param props - Text rendering properties
 * @returns Object containing ImageData and dimensions
 */
const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
  assertTruthy(canvas, 'Canvas is not initialized');
  assertTruthy(context, 'Canvas context is not available');
  assertTruthy(measureContext, 'Canvas measureContext is not available');
  const cacheKey = getLayoutCacheKey(props);

  let layout = renderInfoCache.get(cacheKey);
  if (layout !== undefined) {
    return layout;
  }
  // Extract already normalized properties
  const {
    text,
    fontFamily,
    fontStyle,
    fontSize,
    textAlign,
    maxLines,
    lineHeight,
    overflowSuffix,
    maxWidth,
    maxHeight,
    wordBreak,
    richText,
    color,
  } = props;

  // When richText is enabled, parse BB-code tags from text and use the
  // stripped plain text for layout. The parse result is stored in the
  // module-level singleton (safe — renderText is synchronous).
  let layoutText = text;
  if (richText === true) {
    parseRichText(text, _richTextResult);
    layoutText = _richTextResult.stripped;
  }

  // Rename to baseFont to avoid shadowing the module-level FontHandler
  const baseFont = `${fontStyle} ${fontSize}px Unknown, ${fontFamily}`;
  // Get font metrics and calculate line height
  measureContext.font = baseFont;
  measureContext.textBaseline = 'hanging';

  const metrics = CanvasFontHandler.getFontMetrics(fontFamily, fontSize);

  const letterSpacing = props.letterSpacing || 0;

  const [
    lines,
    remainingLines,
    hasRemainingText,
    _bareLineHeight,
    _lineHeightPx,
    effectiveWidth,
    effectiveHeight,
  ] = mapTextLayout(
    CanvasFontHandler.measureText,
    metrics,
    layoutText,
    textAlign,
    fontFamily,
    lineHeight,
    overflowSuffix,
    wordBreak,
    letterSpacing,
    maxLines,
    maxWidth,
    maxHeight,
  );
  const lineAmount = lines.length;
  const canvasW = Math.ceil(effectiveWidth);
  const canvasH = Math.ceil(effectiveHeight);

  // For OffscreenCanvas we reuse the shared canvas and snapshot via
  // transferToImageBitmap at the end. For HTMLCanvasElement we allocate a
  // fresh canvas per text node so it can be passed directly as the texture
  // source — no pixel readback or intermediate copy needed.
  let drawCanvas: HTMLCanvasElement | OffscreenCanvas;
  if (isOffscreen) {
    drawCanvas = canvas;
  } else {
    assertTruthy(stage, 'Stage is not available');
    drawCanvas = stage.platform.createCanvas();
    context = drawCanvas.getContext('2d') as CanvasRenderingContext2D;
  }

  drawCanvas.width = canvasW;
  drawCanvas.height = canvasH;
  context.fillStyle = 'white';
  context.font = baseFont;
  context.textBaseline = 'hanging';

  // Performance optimization for large fonts
  if (fontSize >= 128) {
    context.globalAlpha = 0.01;
    context.fillRect(0, 0, 0.01, 0.01);
    context.globalAlpha = 1.0;
  }

  if (richText === true) {
    // -------------------------------------------------------------------------
    // Rich text draw path — segment-by-segment with per-span font and color.
    //
    // strippedPos tracks the absolute character position in stripped text as
    // we walk through layout lines. curSpanIdx advances monotonically with
    // strippedPos (spans are sorted by start, no backward scan is needed).
    //
    // Note: layout (mapTextLayout) used the base font metrics, so bold text
    // may render slightly wider than its measured width — acceptable for MVP.
    // -------------------------------------------------------------------------
    const spanCount = _richTextResult.spanCount;
    const spans = _richTextResult.spans;

    // CSS color string for uncolored spans — inherits the node's text color.
    // normalizeCanvasColor caches the string, so no allocation in hot path.
    const nodeColor = normalizeCanvasColor(color, true);

    let strippedPos = 0;
    let curSpanIdx = 0;
    let activeFont = baseFont;
    let activeFillStyle = nodeColor;

    // Prime the context with the node color; individual segments may override.
    context.fillStyle = activeFillStyle;

    // Pre-computed decoration geometry — all integers, zero allocation in the
    // draw loop. Positions are offsets from currentY (top of the em box with
    // textBaseline:'hanging'). metrics.ascender is the distance from the
    // hanging baseline down to the alphabetic baseline in pixels.
    const ascenderPx = metrics.ascender;
    const decoThickness = Math.max(1, Math.round(fontSize / 20));
    const decoUnderlineBase =
      Math.ceil(ascenderPx) + Math.max(1, Math.round(fontSize * 0.08));
    const decoStrikeBase = Math.ceil(ascenderPx) - Math.round(ascenderPx * 0.4);

    for (let i = 0; i < lineAmount; i++) {
      const line = lines[i] as TextLineStruct;
      const textLine = line[0];
      const lineLen = textLine.length;
      let currentX = Math.ceil(line[3]);
      const currentY = Math.ceil(line[4]);

      // Advance span pointer to cover strippedPos (first char of this line).
      while (
        curSpanIdx < spanCount - 1 &&
        strippedPos >= spans[curSpanIdx]!.end
      ) {
        curSpanIdx++;
      }

      let segStartJ = 0;
      let segSpanIdx = curSpanIdx;

      // Iterate one past the line end so the final segment is always flushed.
      for (let j = 1; j <= lineLen; j++) {
        // Determine the span index for character at (strippedPos + j).
        // At end-of-line (j === lineLen) we force a flush without advancing.
        let nextSpanIdx = segSpanIdx;
        if (j < lineLen) {
          while (
            nextSpanIdx < spanCount - 1 &&
            strippedPos + j >= spans[nextSpanIdx]!.end
          ) {
            nextSpanIdx++;
          }
          curSpanIdx = nextSpanIdx;
        }

        if (j === lineLen || nextSpanIdx !== segSpanIdx) {
          // Flush segment [segStartJ, j) with the style from spans[segSpanIdx].
          const span = spans[segSpanIdx]!;

          // Build the CSS font string for this segment.
          // Bold/italic from the span override the node-level fontStyle.
          const spanStyle = span.italic === true ? 'italic' : fontStyle;
          const spanFont =
            span.bold === true
              ? `${spanStyle} bold ${fontSize}px Unknown, ${fontFamily}`
              : `${spanStyle} ${fontSize}px Unknown, ${fontFamily}`;

          if (spanFont !== activeFont) {
            context.font = spanFont;
            activeFont = spanFont;
          }

          // Colored span uses span.color; uncolored span inherits node color.
          const spanFillStyle =
            span.color !== 0
              ? normalizeCanvasColor(span.color, true)
              : nodeColor;

          if (spanFillStyle !== activeFillStyle) {
            context.fillStyle = spanFillStyle;
            activeFillStyle = spanFillStyle;
          }

          const segStartX = currentX;
          if (letterSpacing === 0) {
            const segment = textLine.substring(segStartJ, j);
            context.fillText(segment, currentX, currentY);
            currentX += CanvasFontHandler.measureText(segment, fontFamily, 0);
          } else {
            for (let k = segStartJ; k < j; k++) {
              const char = textLine.charAt(k);
              if (hasZeroWidthSpace(char) === false) {
                context.fillText(char, currentX, currentY);
              }
              currentX += CanvasFontHandler.measureText(
                char,
                fontFamily,
                letterSpacing,
              );
            }
          }

          // Draw underline and/or strikethrough using the same fillStyle as
          // the segment text (decorations inherit span color, matching CSS).
          if (span.underline === true || span.strikethrough === true) {
            const segWidth = currentX - segStartX;
            if (segWidth > 0) {
              if (span.underline === true) {
                context.fillRect(
                  segStartX,
                  currentY + decoUnderlineBase,
                  segWidth,
                  decoThickness,
                );
              }
              if (span.strikethrough === true) {
                context.fillRect(
                  segStartX,
                  currentY + decoStrikeBase,
                  segWidth,
                  decoThickness,
                );
              }
            }
          }

          segStartJ = j;
          segSpanIdx = nextSpanIdx;
        }
      }

      // Advance strippedPos past this line's characters plus the line-break
      // character (space consumed by wrapText, or \n for explicit newlines)
      // that separates layout lines. The final line has no trailing break.
      strippedPos += lineLen + (i < lineAmount - 1 ? 1 : 0);
    }
  } else {
    // -------------------------------------------------------------------------
    // Plain text draw path — unchanged from original implementation.
    // -------------------------------------------------------------------------
    for (let i = 0; i < lineAmount; i++) {
      const line = lines[i] as TextLineStruct;
      const textLine = line[0];
      let currentX = Math.ceil(line[3]);
      const currentY = Math.ceil(line[4]);
      if (letterSpacing === 0) {
        context.fillText(textLine, currentX, currentY);
      } else {
        const textLineLength = textLine.length;
        for (let j = 0; j < textLineLength; j++) {
          const char = textLine.charAt(j);
          if (hasZeroWidthSpace(char) === true) {
            continue;
          }
          context.fillText(char, currentX, currentY);
          currentX += CanvasFontHandler.measureText(
            char,
            fontFamily,
            letterSpacing,
          );
        }
      }
    }
  }

  // Capture the rendered text as a texture source.
  // OffscreenCanvas: transferToImageBitmap detaches the backing bitmap
  //   synchronously (zero-copy). The shared canvas gets a fresh empty bitmap.
  // HTMLCanvasElement: drawCanvas was allocated specifically for this text
  //   node, so it can be passed directly — no copy needed.
  let imageData: ImageBitmap | HTMLCanvasElement | null = null;
  if (drawCanvas.width > 0 && drawCanvas.height > 0) {
    if (isOffscreen) {
      assertTruthy(
        drawCanvas instanceof OffscreenCanvas,
        'drawCanvas is not an OffscreenCanvas',
      );
      imageData = drawCanvas.transferToImageBitmap();
    } else {
      assertTruthy(
        drawCanvas instanceof HTMLCanvasElement,
        'drawCanvas is not an HTMLCanvasElement',
      );
      imageData = drawCanvas;
    }
  }
  const renderInfo = {
    type,
    imageData,
    width: effectiveWidth,
    height: effectiveHeight,
    remainingLines,
    hasRemainingText,
  } as CanvasRenderInfo;
  renderInfoCache.set(cacheKey, renderInfo);
  return renderInfo;
};

/**
 * Clear layout cache for memory management
 */
const clearCache = (): void => {
  renderInfoCache.clear();
};

/**
 * Render quads for Canvas renderer (Canvas doesn't use quad-based rendering)
 */
const renderQuads = (): void => {
  // Canvas renderer doesn't use quad-based rendering
  // This method is for interface compatibility only
};

/**
 * Canvas Text Renderer - implements TextRenderer interface
 */
const CanvasTextRenderer = {
  type,
  font,
  renderText,
  renderQuads,
  init,
  clearCache,
};

export default CanvasTextRenderer;
