/*
 * 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 type {
  FontHandler,
  TextRenderer,
  TrProps,
  TextRenderInfo,
} from './text-rendering/TextRenderer.js';
import {
  CoreNode,
  CoreNodeRenderState,
  UpdateType,
  type CoreNodeProps,
} from './CoreNode.js';
import type { Stage } from './Stage.js';
import type {
  NodeTextFailedPayload,
  NodeTextLoadedPayload,
  NodeTextureLoadedPayload,
} from '../common/CommonTypes.js';
import type { RectWithValid } from './lib/utils.js';
import type { CoreRenderer } from './renderers/CoreRenderer.js';
import type { TextureLoadedEventHandler } from './textures/Texture.js';
import { Matrix3d } from './lib/Matrix3d.js';
import { BufferCollection } from './renderers/webgl/internal/BufferCollection.js';
import type { SdfShaderProps } from './shaders/webgl/SdfShader.js';
import type { WebGlRenderer } from './renderers/webgl/WebGlRenderer.js';
import type { WebGlCtxTexture } from './renderers/webgl/WebGlCtxTexture.js';
import { mergeColorAlpha } from '../utils.js';
export interface CoreTextNodeProps extends CoreNodeProps, TrProps {
  /**
   * Force Text Node to use a specific Text Renderer
   */
  textRendererOverride?: string | null;
  forceLoad: boolean;
}

export enum TextConstraint {
  'none' = 0,
  'width' = 1,
  'height' = 2,
  'both' = 3,
}

export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
  private textRenderer: TextRenderer;
  private fontHandler: FontHandler;

  private _layoutGenerated = false;
  private _waitingForFont = false;
  private _containType: TextConstraint = TextConstraint.none;

  private _sdfBuffer: WebGLBuffer | null = null;
  private _sdfQuadCollection: BufferCollection | null = null;
  private _sdfShaderProps: Partial<SdfShaderProps> | null = null;

  // Text renderer properties - stored directly on the node
  textProps: CoreTextNodeProps;

  private _renderInfo: TextRenderInfo | null = null;

  constructor(
    stage: Stage,
    props: CoreTextNodeProps,
    textRenderer: TextRenderer,
  ) {
    super(stage, props);
    this.textRenderer = textRenderer;
    this.fontHandler = textRenderer.font;

    // Initialize text properties from props
    // Props are guaranteed to have all defaults resolved by Stage.createTextNode
    this.textProps = props;
    this._containType = TextConstraint[props.contain];

    this.setUpdateType(UpdateType.All);
  }

  protected override onTextureLoaded: TextureLoadedEventHandler = (
    _,
    dimensions,
  ) => {
    // If parent has a render texture, flag that we need to update
    if (this.parentHasRenderTexture) {
      this.notifyParentRTTOfUpdate();
    }
    // ignore 1x1 pixel textures
    if (dimensions.w > 1 && dimensions.h > 1) {
      this.emit('loaded', {
        type: 'texture',
        dimensions,
      } satisfies NodeTextureLoadedPayload);
    }
    this.setUpdateType(UpdateType.IsRenderable);
  };

  /**
   * Delete the cached WebGLBuffer held by the SDF renderer ref and reset the
   * ref so the next renderQuads call allocates a fresh one.
   * Safe to call from destroy() or on text change.
   */
  private releaseSdfBuffer(): void {
    const buf = this._sdfBuffer;
    if (buf === null) return;
    this.stage.renderer.deleteBuffer(buf);
    this._sdfBuffer = null;
    this._sdfQuadCollection = null;
  }

  allowTextGeneration() {
    const p = this.props.parent;
    if (p === null) {
      return false;
    }
    if (p.worldAlpha > 0 && p.renderState > CoreNodeRenderState.OutOfBounds) {
      return true;
    }
    return false;
  }

  override updateLocalTransform() {
    const p = this.props;
    let { x, y, w, h } = p;
    const mountX = p.mountX;
    const mountY = p.mountY;
    let mountTranslateX = p.mountX * w;
    let mountTranslateY = p.mountY * h;

    let localTextTransform: Matrix3d | null = null;

    const tProps = this.textProps;
    const { textAlign, verticalAlign, maxWidth, maxHeight } = tProps;
    const contain = this._containType;

    const hasMaxWidth = maxWidth > 0;
    const hasMaxHeight = maxHeight > 0;

    if (contain > 0 && (hasMaxWidth || hasMaxHeight)) {
      let containX = 0;
      let containY = 0;
      if (contain & TextConstraint.width && hasMaxWidth === true) {
        if (textAlign === 'right') {
          containX = maxWidth - w;
        } else if (textAlign === 'center') {
          containX = (maxWidth - w) * 0.5;
        }
        mountTranslateX = mountX * maxWidth;
      }
      if (contain & TextConstraint.height && maxHeight > 0) {
        if (verticalAlign === 'bottom') {
          containY = maxHeight - h;
        } else if (verticalAlign === 'middle') {
          containY = (maxHeight - h) * 0.5;
        }
        mountTranslateY = mountY * maxHeight;
      }
      localTextTransform = Matrix3d.translate(containX, containY);
    }

    if (p.rotation !== 0 || p.scaleX !== 1 || p.scaleY !== 1) {
      const scaleRotate = Matrix3d.rotate(p.rotation).scale(p.scaleX, p.scaleY);
      const pivotW =
        contain & TextConstraint.width && maxWidth > 0 ? maxWidth : w;
      const pivotH =
        contain & TextConstraint.height && maxHeight > 0 ? maxHeight : h;
      const pivotTranslateX = p.pivotX * pivotW;
      const pivotTranslateY = p.pivotY * pivotH;

      this.localTransform = Matrix3d.translate(
        x - mountTranslateX + pivotTranslateX,
        y - mountTranslateY + pivotTranslateY,
        this.localTransform,
      )
        .multiply(scaleRotate)
        .translate(-pivotTranslateX, -pivotTranslateY);
    } else {
      this.localTransform = Matrix3d.translate(
        x - mountTranslateX,
        y - mountTranslateY,
        this.localTransform,
      );
    }

    if (localTextTransform !== null) {
      this.localTransform = this.localTransform.multiply(localTextTransform);
    }
  }

  /**
   * Override CoreNode's update method to handle text-specific updates
   */
  override update(delta: number, parentClippingRect: RectWithValid): void {
    const hasValidText =
      typeof this.textProps.text === 'string' && this.textProps.text.length > 0;

    if (
      hasValidText === true &&
      (this.textProps.forceLoad === true ||
        this.allowTextGeneration() === true) &&
      this._layoutGenerated === false
    ) {
      if (this.fontHandler.isFontLoaded(this.textProps.fontFamily) === true) {
        this._waitingForFont = false;
        this._renderInfo = null; // Clear any previous render info before generating new layout
        this.releaseSdfBuffer(); // Free the cached WebGLBuffer
        const resp = this.textRenderer.renderText(this.textProps);
        this.handleRenderResult(resp);
        this._layoutGenerated = true;
      } else if (this._waitingForFont === false) {
        this.fontHandler.waitingForFont(this.textProps.fontFamily, this);
        this._waitingForFont = true;
      }
    } else if (hasValidText === false) {
      this.props.w = 0;
      this.props.h = 0;
      // If text is invalid, ensure node is not renderable
      this.setRenderable(false);
      this._layoutGenerated = false;
      this._renderInfo = null;
      this.releaseSdfBuffer(); // Free the cached WebGLBuffer
    }

    // First run the standard CoreNode update
    super.update(delta, parentClippingRect);
  }

  /**
   * Override is renderable check for SDF text nodes
   */
  override updateIsRenderable(): void {
    // Guard: Text nodes are never renderable without valid text
    const hasValidText =
      typeof this.textProps.text === 'string' && this.textProps.text.length > 0;

    const renderInfo = this._renderInfo;
    if (hasValidText === false || renderInfo === null) {
      this.setRenderable(false);
      return;
    }

    // SDF text nodes are always renderable if they have a valid layout
    if (renderInfo.type === 'canvas') {
      super.updateIsRenderable();
      return;
    }
    this.setRenderable(true);
  }

  /**
   * Handle the result of text rendering for both Canvas and SDF renderers
   */
  private handleRenderResult(result: TextRenderInfo): void {
    // Host paths on top
    const textRendererType = result.type;
    let width = result.width;
    let height = result.height;

    // Handle zero-dimension case (can happen with certain text inputs or font issues)
    if (width === 0 || height === 0) {
      this.emit('failed', {
        type: 'text',
        error: new Error('Text rendering failed, width or height zero'),
      } satisfies NodeTextFailedPayload);
      return;
    }

    // Handle Canvas renderer (uses ImageData)
    if (textRendererType === 'canvas') {
      if (result.imageData === undefined) {
        this.emit('failed', {
          type: 'text',
          error: new Error(
            'Canvas text rendering failed, no image data returned',
          ),
        } satisfies NodeTextFailedPayload);
        return;
      }

      this.texture = this.stage.txManager.createTexture('ImageTexture', {
        premultiplyAlpha: true,
        src: result.imageData as ImageData,
      });

      this.props.w = width;
      this.props.h = height;
      // It isn't renderable until the texture is loaded we have to set it to false here to avoid it
      // being detected as a renderable default color node in the next frame
      // it will be corrected once the texture is loaded
      this.setRenderable(false);

      if (this.renderState > CoreNodeRenderState.OutOfBounds) {
        // We do want the texture to load immediately
        this.texture.setRenderableOwner(this._id, true);
      }
    } else {
      const layout = result.layout;
      // For SDF, we rely on the presence of a valid layout to determine renderability
      if (layout === undefined) {
        this.emit('failed', {
          type: 'text',
          error: new Error(
            'SDF text rendering failed, no layout data returned',
          ),
        } satisfies NodeTextFailedPayload);
        return;
      }

      this.props.w = width;
      this.props.h = height;
      this.setUpdateType(UpdateType.Local);
      this.setRenderable(true);
      this.numQuads = layout.glyphCount;

      this._sdfShaderProps = {
        size: layout.fontScale,
        distanceRange: layout.distanceRange,
      };

      this.renderOpTextures = [result.atlasTexture as WebGlCtxTexture];
    }

    this._renderInfo = result;
    queueMicrotask(this.emitTextLoadedEvent);
  }

  // Reusable bound method for emitting loaded event
  private emitTextLoadedEvent = () => {
    if (this._renderInfo === null) return; // Guard against unexpected null

    this.emit('loaded', {
      type: 'text',
      dimensions: {
        w: this._renderInfo.width,
        h: this._renderInfo.height,
      },
    } satisfies NodeTextLoadedPayload);
  };

  /**
   * Override renderQuads to handle SDF vs Canvas rendering
   */
  override renderQuads(renderer: CoreRenderer): void {
    if (this.parentHasRenderTexture === true) {
      const rtt = renderer.renderToTextureActive;
      if (rtt === false || this.parentRenderTexture !== renderer.activeRttNode)
        return;
    }

    // Early return if no renderInfo
    if (this._renderInfo === null) {
      return;
    }

    // Canvas renderer: use standard texture rendering via CoreNode
    if (this._renderInfo.type === 'canvas') {
      super.renderQuads(renderer);
      return;
    }

    if (this._sdfBuffer === null) {
      const glw = (this.stage.renderer as WebGlRenderer).glw;
      this._sdfBuffer = glw.createBuffer();
      if (this._sdfBuffer === null) {
        console.error('Failed to create WebGL buffer for SDF text rendering');
        return;
      }
      glw.arrayBufferData(
        this._sdfBuffer,
        this._renderInfo.layout.vertexBuffer,
        glw.STATIC_DRAW,
      );

      this._sdfQuadCollection = new BufferCollection([
        {
          buffer: this._sdfBuffer,
          attributes: {
            a_position: {
              name: 'a_position',
              size: 2,
              type: glw.FLOAT as number,
              normalized: false,
              stride: 4 * Float32Array.BYTES_PER_ELEMENT,
              offset: 0,
            },
            a_textureCoords: {
              name: 'a_textureCoords',
              size: 2,
              type: glw.FLOAT as number,
              normalized: false,
              stride: 4 * Float32Array.BYTES_PER_ELEMENT,
              offset: 2 * Float32Array.BYTES_PER_ELEMENT,
            },
          },
        },
      ]);
    }

    this.sdfShaderProps!.transform = this.globalTransform!.getFloatArr();
    this.sdfShaderProps!.color = mergeColorAlpha(
      this.props.color,
      this.worldAlpha,
    );

    this.textRenderer.renderQuads(this);
  }

  override updateRenderState(renderState: CoreNodeRenderState): void {
    super.updateRenderState(renderState);
    if (
      this._renderInfo !== null &&
      renderState === CoreNodeRenderState.OutOfBounds
    ) {
      this.releaseSdfBuffer();
    }
  }

  override destroy(): void {
    if (this._waitingForFont === true && this.fontHandler) {
      this.fontHandler.stopWaitingForFont(this.textProps.fontFamily, this);
    }

    // Clear cached layout and vertex buffer
    this._renderInfo = null;
    this.releaseSdfBuffer(); // Delete the cached WebGLBuffer before losing stage ref

    this.fontHandler = null!; // Clear reference to avoid memory leaks
    this.textRenderer = null!; // Clear reference to avoid memory leaks

    super.destroy();
  }

  /**
   * used in webgl SDF shader to get the quad buffer collection for rendering text quads
   */
  override get quadBufferCollection(): BufferCollection {
    return this._sdfQuadCollection || super.quadBufferCollection;
  }

  /**
   * used in webgl SDF shader to get the SDF shader props for rendering text quads
   */
  get sdfShaderProps(): SdfShaderProps {
    return this._sdfShaderProps as SdfShaderProps;
  }

  override get isSdfRenderOp(): boolean {
    return this.textRenderer.type === 'sdf';
  }

  override draw(renderer: WebGlRenderer) {
    if (this.textRenderer.type === 'canvas') {
      super.draw(renderer);
      return;
    }

    const { glw, stage } = renderer;
    const canvas = stage.platform!.canvas!;
    const shader = this.props.shader as any;

    stage.shManager.useShader(shader.program);
    shader.program.bindRenderOp(this);

    const clippingRect = this.clippingRect;

    // Clipping
    if (clippingRect.valid === true) {
      const pixelRatio = this.parentHasRenderTexture ? 1 : stage.pixelRatio;

      const clipX = Math.round(clippingRect.x * pixelRatio);
      const clipWidth = Math.round(clippingRect.w * pixelRatio);
      const clipHeight = Math.round(clippingRect.h * pixelRatio);
      let clipY = Math.round(
        canvas.height - clipHeight - clippingRect.y * pixelRatio,
      );
      // if parent has render texture, we need to adjust the scissor rect
      // to be relative to the parent's framebuffer
      if (this.parentHasRenderTexture) {
        const parentFramebufferDimensions = this.parentFramebufferDimensions;
        clipY =
          parentFramebufferDimensions !== null
            ? parentFramebufferDimensions.h - this.props.h
            : 0;
      }

      glw.setScissorTest(true);
      glw.scissor(clipX, clipY, clipWidth, clipHeight);
    } else {
      glw.setScissorTest(false);
    }

    // SDF rendering uses drawArrays with explicit triangle vertices (6 vertices per quad)
    // Note: buffers should be bound by bindRenderOp -> bindBufferCollection
    glw.drawArrays(glw.TRIANGLES, 0, 6 * this.numQuads);
  }

  override set w(value: number) {
    this.maxWidth = value;
  }

  override get w(): number {
    return this.props.w;
  }

  override set h(value: number) {
    this.maxHeight = value;
  }

  override get h(): number {
    return this.props.h;
  }

  get maxWidth() {
    return this.textProps.maxWidth;
  }

  set maxWidth(value: number) {
    if (this.textProps.maxWidth !== value) {
      this.textProps.maxWidth = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  // Property getters and setters
  get maxHeight() {
    return this.textProps.maxHeight;
  }

  set maxHeight(value: number) {
    if (this.textProps.maxHeight !== value) {
      this.textProps.maxHeight = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get contain(): TrProps['contain'] {
    return this.textProps.contain;
  }

  set contain(value: TrProps['contain']) {
    if (this.textProps.contain !== value) {
      this.textProps.contain = value;
      this._containType = TextConstraint[value];
      this.setUpdateType(UpdateType.Local);
    }
  }

  get text(): string {
    return this.textProps.text;
  }

  set text(value: string) {
    let normalizedValue = value;
    if (value === undefined || value === null) {
      normalizedValue = '';
    } else if (typeof value !== 'string') {
      normalizedValue = String(value);
    }

    if (this.textProps.text !== normalizedValue) {
      this.textProps.text = normalizedValue;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get fontSize(): number {
    return this.textProps.fontSize;
  }

  set fontSize(value: number) {
    if (this.textProps.fontSize !== value) {
      this.textProps.fontSize = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get fontFamily(): string {
    return this.textProps.fontFamily;
  }

  set fontFamily(value: string) {
    if (this.textProps.fontFamily !== value) {
      if (this._waitingForFont === true) {
        this.fontHandler.stopWaitingForFont(this.textProps.fontFamily, this);
      }
      this.textProps.fontFamily = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get fontStyle(): TrProps['fontStyle'] {
    return this.textProps.fontStyle;
  }

  set fontStyle(value: TrProps['fontStyle']) {
    if (this.textProps.fontStyle !== value) {
      this.textProps.fontStyle = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get textAlign(): TrProps['textAlign'] {
    return this.textProps.textAlign;
  }

  set textAlign(value: TrProps['textAlign']) {
    if (this.textProps.textAlign !== value) {
      this.textProps.textAlign = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get letterSpacing(): number {
    return this.textProps.letterSpacing;
  }

  set letterSpacing(value: number) {
    if (this.textProps.letterSpacing !== value) {
      this.textProps.letterSpacing = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get lineHeight(): number {
    return this.textProps.lineHeight;
  }

  set lineHeight(value: number) {
    if (this.textProps.lineHeight !== value) {
      this.textProps.lineHeight = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get maxLines(): number {
    return this.textProps.maxLines;
  }

  set maxLines(value: number) {
    if (this.textProps.maxLines !== value) {
      this.textProps.maxLines = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get verticalAlign(): TrProps['verticalAlign'] {
    return this.textProps.verticalAlign;
  }

  set verticalAlign(value: TrProps['verticalAlign']) {
    if (this.textProps.verticalAlign !== value) {
      this.textProps.verticalAlign = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get overflowSuffix(): string {
    return this.textProps.overflowSuffix;
  }

  set overflowSuffix(value: string) {
    if (this.textProps.overflowSuffix !== value) {
      this.textProps.overflowSuffix = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get wordBreak(): TrProps['wordBreak'] {
    return this.textProps.wordBreak;
  }

  set wordBreak(value: TrProps['wordBreak']) {
    if (this.textProps.wordBreak !== value) {
      this.textProps.wordBreak = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get offsetY(): number {
    return this.textProps.offsetY;
  }

  set offsetY(value: number) {
    if (this.textProps.offsetY !== value) {
      this.textProps.offsetY = value;
      this._layoutGenerated = false;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get forceLoad() {
    return this.textProps.forceLoad;
  }

  set forceLoad(value: boolean) {
    if (this.textProps.forceLoad !== value) {
      this.textProps.forceLoad = value;
      this.setUpdateType(UpdateType.Local);
    }
  }

  get renderInfo(): TextRenderInfo | null {
    return this._renderInfo;
  }
}
