export namespace MiniRender {
  
  export interface RendeCanvasInterface {
    initCanvas(width: number, height: number): Promise<void>;
    createImage(src: string): Promise<any>;
    clearCanvas(): void;
    requestAnimationFrame(draw: (ts: number) => void): number;
    cancelAnimationFrame(id: number): void;
    getCtx(): CanvasRenderingContext2D;
  }

  export type RenderObjectId = string;

  export interface Disposable {
    dispose(): void;
  }

  export interface RenderContext {
    readonly scene: Scene;
    readonly canvas: RendeCanvasInterface;
    readonly ctx: CanvasRenderingContext2D;
  }

  export interface IRenderable {
    render(rc: RenderContext): void;
  }

  export interface IUpdatable {
    update(dtMs: number): void;
  }

  export interface TransformLike {
    x: number;
    y: number;
    width: number;
    height: number;
    rotation: number; // radians
    alpha: number; // 0..1
    scaleX: number;
    scaleY: number;
    anchorX: number; // 0..1 (relative to width)
    anchorY: number; // 0..1 (relative to height)
  }

  export type RenderObjectEventName = "added" | "removed" | "click" | "touchstart" | "touchmove" | "touchend" | "touchcancel";
  export type RenderObjectEventHandler = (obj: RenderObject, data?: any) => void;

  export class RenderObject implements TransformLike, IRenderable, IUpdatable {
    public readonly id: RenderObjectId;
    public name?: string;

    public x = 0;
    public y = 0;
    public width = 0;
    public height = 0;
    public rotation = 0;
    public alpha = 1;
    public scaleX = 1;
    public scaleY = 1;
    public anchorX = 0;
    public anchorY = 0;

    public visible = true;
    public zIndex = 0;
    public interactive = false;

    public parent: Container | null = null;

    public data: any = null;

    private listeners = new Map<RenderObjectEventName, Set<RenderObjectEventHandler>>();

    constructor(id?: RenderObjectId) {
      this.id = id ?? RenderObject.newId();
    }

    protected static newId(): RenderObjectId {
      return `ro_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
    }

    public on(event: RenderObjectEventName, handler: RenderObjectEventHandler): () => void {
      const set = this.listeners.get(event) ?? new Set<RenderObjectEventHandler>();
      set.add(handler);
      this.listeners.set(event, set);
      return () => this.off(event, handler);
    }

    public off(event: RenderObjectEventName, handler: RenderObjectEventHandler): void {
      this.listeners.get(event)?.delete(handler);
    }

    public emit(event: RenderObjectEventName, data?: any): void {
      const set = this.listeners.get(event);
      if (!set) return;
      for (const h of set) h(this, data);
    }

    public hasListener(event: RenderObjectEventName): boolean {
      const set = this.listeners.get(event);
      return !!set && set.size > 0;
    }

    protected withTransform(rc: RenderContext, draw: () => void): void {
      const { ctx } = rc;
      ctx.save();

      const ax = this.anchorX * this.width;
      const ay = this.anchorY * this.height;

      ctx.translate(this.x + ax, this.y + ay);
      if (this.rotation) ctx.rotate(this.rotation);
      if (this.scaleX !== 1 || this.scaleY !== 1) ctx.scale(this.scaleX, this.scaleY);

      const prevAlpha = ctx.globalAlpha;
      ctx.globalAlpha = (prevAlpha ?? 1) * this.alpha;

      ctx.translate(-ax, -ay);
      draw();
      ctx.restore();
    }

    protected draw(_rc: RenderContext): void {}

    public render(rc: RenderContext): void {
      if (!this.visible || this.alpha <= 0) return;
      this.withTransform(rc, () => this.draw(rc));
    }

    public update(_dtMs: number): void {}

    public parentToLocal(px: number, py: number): { x: number; y: number } | null {
      const ax = this.anchorX * this.width;
      const ay = this.anchorY * this.height;

      let lx = px - (this.x + ax);
      let ly = py - (this.y + ay);

      if (this.rotation) {
        const cos = Math.cos(-this.rotation);
        const sin = Math.sin(-this.rotation);
        const rx = lx * cos - ly * sin;
        const ry = lx * sin + ly * cos;
        lx = rx;
        ly = ry;
      }

      if (this.scaleX === 0 || this.scaleY === 0) return null;
      lx /= this.scaleX;
      ly /= this.scaleY;

      lx += ax;
      ly += ay;

      return { x: lx, y: ly };
    }

    public hitTest(px: number, py: number): RenderObject | null {
      if (!this.visible || this.alpha <= 0) return null;
      if (!this.interactive) return null;

      const local = this.parentToLocal(px, py);
      if (!local) return null;

      if (local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height) {
        return this;
      }
      return null;
    }
  }

  export class Container extends RenderObject {
    private _children: RenderObject[] = [];

    public get children(): readonly RenderObject[] {
      return this._children;
    }

    public add(child: RenderObject): this {
      if (child.parent) child.parent.remove(child);
      child.parent = this;
      this._children.push(child);
      this.sortChildren();
      child.emit("added");
      return this;
    }

    public remove(child: RenderObject): this {
      const idx = this._children.indexOf(child);
      if (idx < 0) return this;
      this._children.splice(idx, 1);
      child.parent = null;
      child.emit("removed");
      return this;
    }

    public removeAll(): this {
      for (const c of this._children) {
        c.parent = null;
        c.emit("removed");
      }
      this._children = [];
      return this;
    }

    public sortChildren(): void {
      this._children.sort((a, b) => a.zIndex - b.zIndex);
    }

    public override update(dtMs: number): void {
      for (const c of this._children) {
        (c as unknown as IUpdatable).update?.(dtMs);
      }
    }

    protected override draw(rc: RenderContext): void {
      for (const c of this._children) c.render(rc);
    }

    public override hitTest(px: number, py: number): RenderObject | null {
      if (!this.visible || this.alpha <= 0) return null;

      const local = this.parentToLocal(px, py);
      if (!local) return null;

      for (let i = this._children.length - 1; i >= 0; i--) {
        const hit = this._children[i].hitTest(local.x, local.y);
        if (hit) return hit;
      }

      if (this.interactive && this.hasListener("click")) {
        if (local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height) {
          return this;
        }
      }

      return null;
    }
  }

  export class Rect extends RenderObject {
    public fillStyle: string | null = "#000000";
    public strokeStyle: string | null = null;
    public lineWidth = 1;
    public radius = 0;

    protected override draw(rc: RenderContext): void {
      const { ctx } = rc;
      const w = this.width;
      const h = this.height;
      if (w <= 0 || h <= 0) return;

      if (this.radius > 0) {
        const r = Math.max(0, Math.min(this.radius, Math.min(w, h) / 2));
        ctx.beginPath();
        ctx.moveTo(r, 0);
        ctx.lineTo(w - r, 0);
        ctx.arcTo(w, 0, w, r, r);
        ctx.lineTo(w, h - r);
        ctx.arcTo(w, h, w - r, h, r);
        ctx.lineTo(r, h);
        ctx.arcTo(0, h, 0, h - r, r);
        ctx.lineTo(0, r);
        ctx.arcTo(0, 0, r, 0, r);
        ctx.closePath();

        if (this.fillStyle) {
          ctx.fillStyle = this.fillStyle;
          ctx.fill();
        }
        if (this.strokeStyle) {
          ctx.strokeStyle = this.strokeStyle;
          ctx.lineWidth = this.lineWidth;
          ctx.stroke();
        }
        return;
      }

      if (this.fillStyle) {
        ctx.fillStyle = this.fillStyle;
        ctx.fillRect(0, 0, w, h);
      }
      if (this.strokeStyle) {
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.strokeRect(0, 0, w, h);
      }
    }
  }

  export class Sprite extends RenderObject {
    public src: string = "";
    public image: any | null = null;

    protected override draw(rc: RenderContext): void {
      const { ctx, scene } = rc;
      if (!this.src) return;
      const w = this.width;
      const h = this.height;
      if (w <= 0 || h <= 0) return;

      const img = this.image ?? scene.assets.getImageSync(this.src);
      if (!img) {
        scene.assets
          .getImage(this.src)
          .then((loaded) => {
            this.image = loaded;
          })
          .catch(() => {});
        return;
      }
      ctx.drawImage(img, 0, 0, w, h);
    }
  }

  export type TextAlign = CanvasTextAlign;
  export type TextBaseline = CanvasTextBaseline;

  export interface TextStyle {
    fontSize?: number;
    fontFamily?: string;
    fontWeight?: string | number;
    fontStyle?: string;
    color?: string;
    strokeColor?: string | null;
    strokeWidth?: number;
    align?: TextAlign;
    baseline?: TextBaseline;
    lineHeight?: number;
    /**
     * - "nowrap": 单行（遇到 \n 也会分行）
     * - "wrap": 自动按 maxWidth/width 换行
     */
    wrap?: "nowrap" | "wrap";
    /** 指定自动换行宽度；若不设置则使用 `width` */
    maxWidth?: number;
  }

  export class Text extends RenderObject {
    public text = "";
    public style: Required<TextStyle> = {
      fontSize: 16,
      fontFamily: "sans-serif",
      fontWeight: "normal",
      fontStyle: "normal",
      color: "#000000",
      strokeColor: null,
      strokeWidth: 1,
      align: "left",
      baseline: "top",
      lineHeight: 20,
      wrap: "nowrap",
      maxWidth: 0,
    };

    constructor(text?: string, style?: TextStyle) {
      super();
      if (typeof text === "string") this.text = text;
      if (style) this.setStyle(style);
    }

    public setStyle(style: TextStyle): void {
      this.style = { ...this.style, ...style };
      if (!style.lineHeight && style.fontSize) {
        // 常见经验值
        this.style.lineHeight = Math.round(style.fontSize * 1.25);
      }
    }

    private applyTextStyle(ctx: CanvasRenderingContext2D): void {
      const s = this.style;
      ctx.font = `${s.fontStyle} ${s.fontWeight} ${s.fontSize}px ${s.fontFamily}`.trim();
      ctx.fillStyle = s.color;
      ctx.textAlign = s.align;
      ctx.textBaseline = s.baseline;
      if (s.strokeColor) {
        ctx.strokeStyle = s.strokeColor;
        ctx.lineWidth = s.strokeWidth;
      }
    }

    private splitLines(ctx: CanvasRenderingContext2D): string[] {
      const raw = (this.text ?? "").toString();
      const baseLines = raw.split(/\r?\n/);

      if (this.style.wrap !== "wrap") return baseLines;

      const max =
        (this.style.maxWidth > 0 ? this.style.maxWidth : 0) ||
        (this.width > 0 ? this.width : 0);
      if (max <= 0) return baseLines;

      const out: string[] = [];
      for (const line of baseLines) {
        if (!line) {
          out.push("");
          continue;
        }

        let current = "";
        for (const ch of [...line]) {
          const test = current + ch;
          const w = ctx.measureText(test).width;
          if (w > max && current) {
            out.push(current);
            current = ch;
          } else {
            current = test;
          }
        }
        out.push(current);
      }
      return out;
    }

    private computeAutoSize(ctx: CanvasRenderingContext2D, lines: string[]): void {
      // 未指定 width 时，使用最长行宽；指定了 maxWidth 时以 maxWidth 为上限
      if (this.width <= 0) {
        let maxLine = 0;
        for (const l of lines) maxLine = Math.max(maxLine, ctx.measureText(l).width);
        const limit = this.style.maxWidth > 0 ? this.style.maxWidth : 0;
        this.width = limit > 0 ? Math.min(limit, maxLine) : maxLine;
      }
      if (this.height <= 0) {
        this.height = lines.length * this.style.lineHeight;
      }
    }

    protected override draw(rc: RenderContext): void {
      const { ctx } = rc;
      if (!this.text) return;

      this.applyTextStyle(ctx);

      const lines = this.splitLines(ctx);
      this.computeAutoSize(ctx, lines);

      const s = this.style;

      // 对齐偏移：以对象局部坐标系 (0,0) 为绘制起点
      const xBase =
        s.align === "center" ? this.width / 2 : s.align === "right" || s.align === "end" ? this.width : 0;

      let y = 0;
      for (const line of lines) {
        if (s.strokeColor) ctx.strokeText(line, xBase, y, s.maxWidth > 0 ? s.maxWidth : undefined);
        ctx.fillText(line, xBase, y, s.maxWidth > 0 ? s.maxWidth : undefined);
        y += s.lineHeight;
      }
    }
  }

  export type AnimateSpriteFrame = [
    x: number,
    y: number,
    width: number,
    height: number,
    originX?: number,
    originY?: number,
    imageIndex?: number,
  ];

  export interface AnimateSpriteAnimation {
    /** 指向 `frames` 的索引数组 */
    frames: number[];
    /** 播放速度倍率（1 = 使用 framerate） */
    speed?: number;
    /** 是否循环覆盖全局 playOnce */
    loop?: boolean;
  }

  export interface AnimateSpriteConfig {
    framerate: number;
    images: string[];
    frames: AnimateSpriteFrame[];
    animations: Record<string, AnimateSpriteAnimation>;
    playOnce?: boolean;
    currentAnimation: string;
  }

  export class AnimateSprite extends RenderObject {
    public framerate = 7;
    public images: string[] = [];
    public frames: AnimateSpriteFrame[] = [];
    public animations: Record<string, AnimateSpriteAnimation> = {};
    public playOnce = false;
    public currentAnimation = "";

    public playing = true;

    private _frameCursor = 0;
    private _accMs = 0;
    private _resolvedImages: Array<any | null> = [];

    constructor(config?: Partial<AnimateSpriteConfig>) {
      super();
      if (config) Object.assign(this, config);
      this._resolvedImages = new Array(this.images.length).fill(null);
      if (this.currentAnimation && this.animations && !this.animations?.[this.currentAnimation]) {
        // 若未配置 animations，则允许把 currentAnimation 当成 “默认帧序列” 的别名
        this.animations[this.currentAnimation] = { frames: [0] };
      }
    }

    public play(name?: string, opts?: { reset?: boolean; once?: boolean }): void {
      if (name) this.currentAnimation = name;
      if (opts?.reset ?? true) {
        this._frameCursor = 0;
        this._accMs = 0;
      }
      if (typeof opts?.once === "boolean") this.playOnce = opts.once;
      this.playing = true;
    }

    public stop(): void {
      this.playing = false;
    }

    public gotoAndStop(frameCursor: number): void {
      this._frameCursor = Math.max(0, frameCursor | 0);
      this._accMs = 0;
      this.playing = false;
    }

    public get currentFrameIndex(): number {
      const seq = this.getAnimationSequence();
      if (seq.length <= 0) return 0;
      const cursor = Math.max(0, Math.min(this._frameCursor, seq.length - 1));
      return seq[cursor] ?? 0;
    }

    private getAnimationSequence(): number[] {
      const anim = this.animations[this.currentAnimation];
      if (anim?.frames?.length) return anim.frames;
      // fallback：没有动画就按全部 frames 依次播放
      return this.frames.map((_, i) => i);
    }

    private getEffectiveFrameDurationMs(): number {
      const fps = Math.max(0.0001, this.framerate);
      const base = 1000 / fps;
      const anim = this.animations[this.currentAnimation];
      const speed = anim?.speed ?? 1;
      return base / Math.max(0.0001, speed);
    }

    public override update(dtMs: number): void {
      if (!this.playing) return;

      const seq = this.getAnimationSequence();
      if (seq.length <= 1) return;

      this._accMs += dtMs;
      const frameDur = this.getEffectiveFrameDurationMs();
      if (this._accMs < frameDur) return;

      const steps = Math.floor(this._accMs / frameDur);
      this._accMs = this._accMs % frameDur;

      this._frameCursor += steps;

      const anim = this.animations[this.currentAnimation];
      const loop = anim?.loop ?? !this.playOnce;
      if (loop) {
        this._frameCursor = this._frameCursor % seq.length;
      } else {
        if (this._frameCursor >= seq.length - 1) {
          this._frameCursor = seq.length - 1;
          this.playing = false;
        }
      }
    }

    protected override draw(rc: RenderContext): void {
      const { ctx, scene } = rc;
      if (!this.frames.length) return;

      const fIdx = this.currentFrameIndex;
      const frame = this.frames[fIdx];
      if (!frame) return;

      const [sx, sy, sw, sh, originX = 0, originY = 0, imageIndex = 0] = frame;
      if (sw <= 0 || sh <= 0) return;

      const src = this.images[imageIndex] ?? this.images[0];
      if (!src) return;

      // 默认使用帧尺寸作为显示尺寸
      if (this.width <= 0) this.width = sw;
      if (this.height <= 0) this.height = sh;

      let img = this._resolvedImages[imageIndex] ?? null;
      img = img ?? scene.assets.getImageSync(src);
      if (!img) {
        scene.assets
          .getImage(src)
          .then((loaded) => {
            this._resolvedImages[imageIndex] = loaded;
          })
          .catch(() => {});
        return;
      }

      // drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
      ctx.drawImage(img, sx, sy, sw, sh, -originX, -originY, this.width, this.height);
    }
  }

  export class AssetManager {
    private imageCache = new Map<string, Promise<any>>();
    private imageResolved = new Map<string, any>();

    constructor(private canvas: RendeCanvasInterface) {}

    public getImage(src: string): Promise<any> {
      if (this.imageResolved.has(src)) return Promise.resolve(this.imageResolved.get(src));
      const inflight = this.imageCache.get(src);
      if (inflight) return inflight;

      const p = this.canvas.createImage(src).then((img) => {
        this.imageResolved.set(src, img);
        return img;
      });
      this.imageCache.set(src, p);
      return p;
    }

    public getImageSync(src: string): any | null {
      return this.imageResolved.get(src) ?? null;
    }

    public clear(): void {
      this.imageCache.clear();
      this.imageResolved.clear();
    }
  }

  export type CanvasAdapterFactory = (...args: any[]) => RendeCanvasInterface;

  export class CanvasAdapterRegistry {
    private static registry = new Map<string, CanvasAdapterFactory>();

    public static register(name: string, factory: CanvasAdapterFactory): void {
      this.registry.set(name, factory);
    }

    public static get(name: string): CanvasAdapterFactory | undefined {
      return this.registry.get(name);
    }

    public static create(name: string, ...args: any[]): RendeCanvasInterface {
      const f = this.registry.get(name);
      if (!f) throw new Error(`CanvasAdapter 未注册: ${name}`);
      return f(...args);
    }

    public static has(name: string): boolean {
      return this.registry.has(name);
    }

    public static names(): string[] {
      return [...this.registry.keys()];
    }
  }

  export class Scene implements Disposable {
    public readonly root: Container = new Container("root");
    public readonly assets: AssetManager;

    private rafId: number | null = null;
    private lastTs: number | null = null;
    private currentDragObject: RenderObject | null = null;

    public readonly canvas: RendeCanvasInterface;
    public readonly width: number;
    public readonly height: number;
    public readonly backgroundColor: string;


    public readonly events = {
      handleTouchStart: (e: any) => {
        const pos = this.getEventPosition(e);
        const hit = this.root.hitTest(pos.x, pos.y);
        if (hit) {
          hit.emit("touchstart", e);
          this.currentDragObject = hit;
        }
      },
      handleTouchMove: (e: any) => {
        if (this.currentDragObject) {
          this.currentDragObject.emit("touchmove", e);
        }
      },
      handleTouchEnd: (e: any) => {
        if (this.currentDragObject) {
          this.currentDragObject.emit("touchend", e);
          this.currentDragObject.emit("click", e);
          this.currentDragObject = null;
        }
      },
      handleTouchCancel: (e: any) => {
        if (this.currentDragObject) 
          this.currentDragObject.emit("touchcancel", e);
        this.currentDragObject = null;
      },
    };

    constructor(
      public options: {
        canvas: RendeCanvasInterface,
        width: number,
        height: number,
        backgroundColor: string,
      },
      public onInit: (this: Scene) => void,
      public updateFn: (this: Scene, dtMs: number) => void,
    ) {
      this.canvas = options.canvas;
      this.width = options.width;
      this.height = options.height;
      this.backgroundColor = options.backgroundColor;
      this.assets = new AssetManager(this.canvas);
    }

    public async init() {
      console.log(`[Canvas] init canvas ${this.width}x${this.height}`);
      
      await this.canvas.initCanvas(this.width, this.height);
      this.onInit.call(this);
    }

    public clear(): void {
      this.canvas.clearCanvas();
      this.canvas.getCtx().fillStyle = this.backgroundColor;
      this.canvas.getCtx().fillRect(0, 0, this.width, this.height);
    }

    public renderOnce(): void {
      this.clear();
      const rc: RenderContext = {
        scene: this,
        canvas: this.canvas,
        ctx: this.canvas.getCtx(),
      };
      this.root.render(rc);
    }

    public tick(ts: number): void {
      const dt = this.lastTs === null ? 16 : Math.max(0, ts - this.lastTs);
      this.lastTs = ts;
      this.updateFn.call(this, dt);
      this.root.update(dt);
      this.renderOnce();
    }

    public start(): void {
      if (this.rafId !== null) return;
      const loop = (ts: number) => {
        this.tick(ts);
        this.rafId = this.canvas.requestAnimationFrame(loop);
      };
      this.rafId = this.canvas.requestAnimationFrame(loop);
    }

    public stop(): void {
      if (this.rafId === null) return;
      this.canvas.cancelAnimationFrame(this.rafId);
      this.rafId = null;
      this.lastTs = null;
    }

    public dispose(): void {
      this.stop();
      this.assets.clear();
      this.root.removeAll();
    }

    public precentXToPixel(precent: number): number {
      return this.width * precent;
    }
    public precentYToPixel(precent: number): number {
      return this.height * precent;
    }

    private getEventPosition(e: any): { x: number; y: number } {
      if (e?.detail?.x !== undefined) return { x: e.detail.x, y: e.detail.y };
      if (e?.offsetX !== undefined) return { x: e.offsetX, y: e.offsetY };
      if (e?.touches?.[0]) return { x: e.touches[0].x, y: e.touches[0].y };
      return { x: 0, y: 0 };
    }
  }
}