/*
 * Copyright (c) 2026 Huawei Device Co., Ltd.
 * 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 { Apng } from './Apng';
import { AnimationCallback } from '../types/ApngTypes';
import entry from 'libentry.so';
import image from '@ohos.multimedia.image';
import { fileIo as fs } from '@kit.CoreFileKit';

// Use OpenHarmony PixelMap type
type PixelMap = image.PixelMap;

/**
 * Native wrapper object returned from C++ for a frame.
 * pixelBuffer 为 BGRA_8888,供 image.createPixelMap 使用。
 */
interface NativePixelMapWrapper {
  pixelBuffer: ArrayBuffer;
  width: number;
  height: number;
  targetWidth: number;
  targetHeight: number;
}

/**
 * Size definition for PixelMap initialization.
 */
interface PixelMapSize {
  width: number;
  height: number;
}

/**
 * Initialization options for image.createPixelMap.
 */
interface PixelMapInitOptions {
  size: PixelMapSize;
}

/**
 * Constants for loop count
 */
export const LOOP_FOREVER = 0;
export const LOOP_INTRINSIC = -1;

/**
 * An animated drawable that plays the frames of an animated PNG.
 */
export class ApngDrawable {
  /** 防止 setInterval 叠帧或切后台后单帧 Δt 过大导致时间轴暴走、提前结束 */
  private static readonly MAX_FRAME_DELTA_MS = 100;

  private apng: Apng;
  private loopCount: number;
  private isStarted: boolean = false;
  private animationElapsedTimeMillis: number = 0;
  private animationPrevDrawTimeMillis: number | null = null;
  private frameStartTimes: number[] = [];
  private callbacks: AnimationCallback[] = [];
  private pixelMap: PixelMap | null = null;
  private frameBuffer: ArrayBuffer;
  private targetWidth: number | undefined;
  private targetHeight: number | undefined;

  // Constants
  public readonly durationMillis: number;
  public readonly frameCount: number;
  public readonly frameDurations: number[];
  public readonly frameByteCount: number;
  public readonly allocationByteCount: number;

  constructor(apng: Apng, width?: number, height?: number) {
    this.apng = apng;
    this.durationMillis = apng.duration;
    this.frameCount = apng.frameCount;
    this.frameDurations = apng.frameDurations;
    this.frameByteCount = apng.width * apng.height * 4;
    this.allocationByteCount = apng.allFrameByteCount + this.frameByteCount;
    this.loopCount = apng.loopCount === 0 ? LOOP_FOREVER : apng.loopCount;
    this.targetWidth = width;
    this.targetHeight = height;

    // Calculate frame start times
    this.frameStartTimes = new Array(this.frameCount);
    this.frameStartTimes[0] = 0;
    for (let i = 1; i < this.frameCount; i++) {
      this.frameStartTimes[i] = this.frameStartTimes[i - 1] + this.frameDurations[i - 1];
    }

    // Create frame buffer
    this.frameBuffer = new ArrayBuffer(this.frameByteCount);
  }

  /**
   * The number of times to loop this APNG image.
   */
  public getLoopCount(): number {
    return this.loopCount;
  }

  public setLoopCount(count: number): void {
    if (count < LOOP_INTRINSIC) {
      throw new Error(`loopCount must be >= ${LOOP_INTRINSIC}`);
    }
    this.loopCount = count === LOOP_INTRINSIC ? this.apng.loopCount : count;
  }

  /**
   * Whether this drawable has already been destroyed or not.
   */
  public get isRecycled(): boolean {
    return this.apng.isRecycled;
  }

  /**
   * The number indicating the current loop index.
   */
  public get currentLoopIndex(): number {
    if (this.durationMillis === 0) {
      return 0;
    }
    const index = Math.floor(this.animationElapsedTimeMillis / this.durationMillis);
    return Math.min(index, this.loopCount === LOOP_FOREVER ? index : this.loopCount - 1);
  }

  /**
   * The corresponding frame index with the elapsed time of the animation.
   */
  public get currentFrameIndex(): number {
    let progressMillisInCurrentLoop =
      this.durationMillis === 0 ? 0 : this.animationElapsedTimeMillis % this.durationMillis;
    progressMillisInCurrentLoop += this.exceedsRepeatCountLimitation() ? this.durationMillis : 0;
    return this.calculateCurrentFrameIndex(0, this.frameCount - 1, progressMillisInCurrentLoop);
  }

  /**
   * Whether the animation is running.
   */
  public isRunning(): boolean {
    return this.isStarted;
  }

  /**
   * Starts the animation.
   * 参考 Android ApngDrawable.start():设置 isStarted 和 animationPrevDrawTimeMillis=null,
   * 由外部驱动(Android 的 invalidateSelf,Harmony 的 setInterval)持续调用 getCurrentFrame 更新帧。
   */
  public start(): void {
    this.isStarted = true;
    this.animationElapsedTimeMillis = 0; // 每次 start 从头开始,与用户预期一致
    this.animationPrevDrawTimeMillis = null;
  }

  /**
   * Stops the animation.
   */
  public stop(): void {
    this.isStarted = false;
  }

  /**
   * Seeks frame to given position.
   */
  public seekTo(positionMillis: number): void {
    if (positionMillis < 0) {
      throw new Error('positionMillis must be positive value');
    }
    this.animationPrevDrawTimeMillis = null;
    this.animationElapsedTimeMillis = positionMillis;
  }

  /**
   * Seeks frame to given frame index with loop index.
   */
  public seekToFrame(loopIndex: number, frameIndex: number): void {
    if (loopIndex < 0) {
      throw new Error('loopIndex must be positive value');
    }
    if (frameIndex < 0) {
      throw new Error('frameIndex must be positive value');
    }
    if (loopIndex >= this.loopCount && this.loopCount !== LOOP_FOREVER) {
      throw new Error(`loopIndex must be less than loopCount. loopIndex = ${loopIndex}, loopCount = ${this.loopCount}`);
    }
    if (frameIndex >= this.frameCount) {
      throw new Error(`frameIndex must be less than frameCount. frameIndex = ${frameIndex}, frameCount = ${this.frameCount}`);
    }
    this.seekTo(loopIndex * this.durationMillis + this.frameStartTimes[frameIndex]);
  }

  /**
   * Registers animation callback.
   */
  public registerAnimationCallback(callback: AnimationCallback): void {
    this.callbacks.push(callback);
  }

  /**
   * Unregisters animation callback.
   */
  public unregisterAnimationCallback(callback: AnimationCallback): boolean {
    const index = this.callbacks.indexOf(callback);
    if (index >= 0) {
      this.callbacks.splice(index, 1);
      return true;
    }
    return false;
  }

  /**
   * Clears all animation callbacks.
   */
  public clearAnimationCallbacks(): void {
    this.callbacks = [];
  }

  /**
   * Creates a copy of this drawable.
   */
  public copy(): ApngDrawable {
    const copiedApng = this.apng.copy();
    const drawable = new ApngDrawable(copiedApng);
    drawable.setLoopCount(this.loopCount);
    return drawable;
  }

  /**
   * Releases resources managed by the native layer.
   */
  public recycle(): void {
    this.apng.recycle();
    this.callbacks = [];
  }

  /**
   * Gets the current frame as PixelMap.
   */
  public async getCurrentFrame(): Promise<PixelMap | null> {
    if (this.isRecycled) {
      return null;
    }

    // Update animation time if started
    if (this.isStarted) {
      this.progressAnimationElapsedTime();
    }

    const frameIndex = this.currentFrameIndex;
    if (!this.apng.drawWithIndex(frameIndex, this.frameBuffer)) {
      return null;
    }

    // Create PixelMap from RGBA buffer using C++ NAPI + OpenHarmony image API
    try {
      // Determine target size (support scaling)
      const targetWidth = this.targetWidth ?? this.apng.width;
      const targetHeight = this.targetHeight ?? this.apng.height;

      // Call C++ NAPI function to create a wrapper object that contains RGBA data and size info
      const wrapper = entry.apng_drawable.createPixelMapFromBuffer(
        this.frameBuffer,
        this.apng.width,
        this.apng.height,
        targetWidth,
        targetHeight
      ) as NativePixelMapWrapper | null;

      if (!wrapper) {
        console.error('Failed to create PixelMap wrapper from native layer');
        return null;
      }

      // Use OpenHarmony image.createPixelMap:官方要求 colors 为 BGRA_8888;native createPixelMapFromBuffer 已从 RGBA 转换。
      // wrapper.width/height 为缩放后的尺寸(C++ 层已处理缩放)
      const size: PixelMapSize = {
        width: wrapper.width,  // 已经是 targetWidth(缩放后)
        height: wrapper.height // 已经是 targetHeight(缩放后)
      };

      const initOptions: PixelMapInitOptions = {
        size: size
      };

      const pixelMap = await image.createPixelMap(wrapper.pixelBuffer, initOptions);
      return pixelMap as PixelMap;
    } catch (e) {
      const errorMsg = e instanceof Error ? e.message : String(e);
      console.error('Failed to create PixelMap:', errorMsg);
      return null;
    }
  }

  /**
   * Exports the current frame as a Bitmap (PixelMap).
   * This is equivalent to getCurrentFrame() - returns the current frame as PixelMap.
   * @returns Promise resolving to PixelMap of the current frame, or null if failed
   */
  public async exportAsBitmap(): Promise<PixelMap | null> {
    return this.getCurrentFrame();
  }

  /**
   * Saves the current frame as a PNG file.
   * @param filePath The path to save the PNG file
   * @returns Promise resolving to true if successful, false otherwise
   */
  public async saveAsPng(filePath: string): Promise<boolean> {
    const pixelMap = await this.getCurrentFrame();
    if (!pixelMap) {
      console.error('saveAsPng: Failed to get current frame');
      return false;
    }

    try {
      const packer = image.createImagePacker();
      const packOpts: image.PackingOption = { format: 'image/png', quality: 100 };

      const file = fs.openSync(filePath, fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE);
      await packer.packToFile(pixelMap, file.fd, packOpts);
      fs.closeSync(file.fd);

      packer.release();

      return true;
    } catch (e) {
      const errorMsg = e instanceof Error ? e.message : String(e);
      console.error('saveAsPng: Failed to save image:', errorMsg);
      return false;
    }
  }

  private progressAnimationElapsedTime(): void {
    const lastFrame = this.currentFrameIndex;
    const currentTimeMillis = Date.now();
    const animationPrevDrawTimeMillisSnapshot = this.animationPrevDrawTimeMillis;

    let deltaMs = 0;
    if (animationPrevDrawTimeMillisSnapshot !== null) {
      const raw = currentTimeMillis - animationPrevDrawTimeMillisSnapshot;
      deltaMs = Math.min(Math.max(raw, 0), ApngDrawable.MAX_FRAME_DELTA_MS);
    }
    this.animationElapsedTimeMillis = animationPrevDrawTimeMillisSnapshot === null
      ? this.animationElapsedTimeMillis
      : this.animationElapsedTimeMillis + deltaMs;
    this.animationPrevDrawTimeMillis = currentTimeMillis;

    const frameChanged = this.currentFrameIndex !== lastFrame;

    if (this.isStarted) {
      if (
        this.isFirstFrame() &&
        this.isFirstLoop() &&
        animationPrevDrawTimeMillisSnapshot === null
      ) {
        // Animation start
        this.callbacks.forEach(cb => cb.onAnimationStart?.());
      } else if (
        this.isLastFrame() &&
        this.hasNextLoop() &&
        frameChanged
      ) {
        // Animation repeat
        const nextLoopIndex = this.currentLoopIndex + 1;
        this.callbacks.forEach(cb => cb.onAnimationRepeat?.(nextLoopIndex));
      }
    }

    if (this.exceedsRepeatCountLimitation()) {
      this.isStarted = false;
      this.callbacks.forEach(cb => cb.onAnimationEnd?.());
    }
  }

  private isFirstFrame(): boolean {
    return this.currentFrameIndex === 0;
  }

  private isLastFrame(): boolean {
    return this.currentFrameIndex === this.frameCount - 1;
  }

  private isFirstLoop(): boolean {
    return Math.floor(this.animationElapsedTimeMillis / this.durationMillis) === 0;
  }

  private hasNextLoop(): boolean {
    if (this.loopCount === LOOP_FOREVER) {
      return true;
    }
    return Math.floor(this.animationElapsedTimeMillis / this.durationMillis) < this.loopCount - 1;
  }

  private exceedsRepeatCountLimitation(): boolean {
    if (this.loopCount === LOOP_FOREVER) {
      return false;
    }
    return Math.floor(this.animationElapsedTimeMillis / this.durationMillis) > this.loopCount - 1;
  }

  private calculateCurrentFrameIndex(
    lowerBoundIndex: number,
    upperBoundIndex: number,
    progressMillisInCurrentLoop: number
  ): number {
    const middleIndex = Math.floor((lowerBoundIndex + upperBoundIndex) / 2);

    if (
      this.frameStartTimes.length > middleIndex + 1 &&
      progressMillisInCurrentLoop >= this.frameStartTimes[middleIndex + 1]
    ) {
      return this.calculateCurrentFrameIndex(
        middleIndex + 1,
        upperBoundIndex,
        progressMillisInCurrentLoop
      );
    }

    if (
      lowerBoundIndex !== upperBoundIndex &&
      progressMillisInCurrentLoop < this.frameStartTimes[middleIndex]
    ) {
      return this.calculateCurrentFrameIndex(
        lowerBoundIndex,
        middleIndex,
        progressMillisInCurrentLoop
      );
    }

    return middleIndex;
  }

  /**
   * Static method to decode APNG from ArrayBuffer.
   */
  public static decode(buffer: ArrayBuffer, width?: number, height?: number): ApngDrawable {
    if ((width === undefined) !== (height === undefined)) {
      throw new Error('Can not specify only one side of size');
    }
    if (width !== undefined && width <= 0) {
      throw new Error(`Can not specify 0 or negative as width value. width = ${width}`);
    }
    if (height !== undefined && height <= 0) {
      throw new Error(`Can not specify 0 or negative as height value. height = ${height}`);
    }

    const apng = Apng.decode(buffer);
    return new ApngDrawable(apng, width, height);
  }

  /**
   * Static method to check if buffer is APNG.
   */
  public static isApng(buffer: ArrayBuffer): boolean {
    return Apng.isApng(buffer);
  }
}