import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';

/// A unified widget to display and cache Wolf3D assets.
class WolfAssetPainter extends StatefulWidget {
  /// Decoded sprite source, when painting a sprite asset.
  final Sprite? sprite;

  /// Decoded VGA image source, when painting a VGA asset.
  final VgaImage? vgaImage;

  /// Pre-rendered game frame, when painting live gameplay output.
  final ui.Image? frame;

  /// Creates a painter for a palette-indexed [Sprite].
  const WolfAssetPainter.sprite(this.sprite, {super.key})
    : vgaImage = null,
      frame = null;

  /// Creates a painter for a planar VGA image.
  const WolfAssetPainter.vga(this.vgaImage, {super.key})
    : sprite = null,
      frame = null;

  /// Creates a painter for an already decoded [ui.Image] frame.
  const WolfAssetPainter.frame(this.frame, {super.key})
    : sprite = null,
      vgaImage = null;

  @override
  State<WolfAssetPainter> createState() => _WolfAssetPainterState();
}

class _WolfAssetPainterState extends State<WolfAssetPainter> {
  ui.Image? _cachedImage;

  // Only images created inside this widget should be disposed here. Frames
  // handed in from elsewhere remain owned by their producer.
  bool _ownsImage = false;

  @override
  void initState() {
    super.initState();
    _prepareImage();
  }

  @override
  void didUpdateWidget(covariant WolfAssetPainter oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.sprite != oldWidget.sprite ||
        widget.vgaImage != oldWidget.vgaImage ||
        widget.frame != oldWidget.frame) {
      _prepareImage();
    }
  }

  @override
  void dispose() {
    if (_ownsImage) {
      _cachedImage?.dispose();
    }
    super.dispose();
  }

  Future<void> _prepareImage() async {
    // Dispose previously generated images before creating a replacement so the
    // widget does not retain stale native image allocations.
    if (_ownsImage && _cachedImage != null) {
      _cachedImage!.dispose();
      _cachedImage = null;
    }

    // Pre-decoded frames can be used as-is and stay owned by the caller.
    if (widget.frame != null) {
      _ownsImage = false;
      if (mounted) {
        setState(() => _cachedImage = widget.frame);
      }
      return;
    }

    ui.Image? newImage;
    if (widget.sprite != null) {
      newImage = await _buildSpriteImage(widget.sprite!);
    } else if (widget.vgaImage != null) {
      newImage = await _buildVgaImage(widget.vgaImage!);
    }

    if (mounted) {
      setState(() {
        _ownsImage = true;
        _cachedImage = newImage;
      });
    } else {
      // If the widget was unmounted while work completed, dispose the image
      // immediately to avoid leaking native resources.
      newImage?.dispose();
    }
  }

  /// Converts a sprite's indexed palette data into a Flutter [ui.Image].
  Future<ui.Image> _buildSpriteImage(Sprite sprite) {
    final completer = Completer<ui.Image>();
    final pixels = Uint8List(64 * 64 * 4); // 4 bytes per pixel (RGBA)

    for (int x = 0; x < 64; x++) {
      for (int y = 0; y < 64; y++) {
        int colorByte = sprite.pixels[x * 64 + y];
        int offset = (y * 64 + x) * 4; // Row-major layout for ui.decodeImage

        if (colorByte != 255) {
          // 255 is transparency
          int abgr = ColorPalette.vga32Bit[colorByte];
          pixels[offset] = abgr & 0xFF; // R
          pixels[offset + 1] = (abgr >> 8) & 0xFF; // G
          pixels[offset + 2] = (abgr >> 16) & 0xFF; // B
          pixels[offset + 3] = (abgr >> 24) & 0xFF; // A
        } else {
          pixels[offset + 3] = 0; // Alpha 0
        }
      }
    }

    ui.decodeImageFromPixels(
      pixels,
      64,
      64,
      ui.PixelFormat.rgba8888,
      (img) => completer.complete(img),
    );
    return completer.future;
  }

  /// Converts a planar VGA image into a row-major Flutter [ui.Image].
  Future<ui.Image> _buildVgaImage(VgaImage image) {
    final completer = Completer<ui.Image>();
    final pixels = Uint8List(image.width * image.height * 4);

    int planeWidth = image.width ~/ 4;
    int planeSize = planeWidth * image.height;

    for (int y = 0; y < image.height; y++) {
      for (int x = 0; x < image.width; x++) {
        int plane = x % 4;
        int sx = x ~/ 4;
        int colorByte =
            image.pixels[(plane * planeSize) + (y * planeWidth) + sx];
        int offset = (y * image.width + x) * 4;

        if (colorByte != 255) {
          int abgr = ColorPalette.vga32Bit[colorByte];
          pixels[offset] = abgr & 0xFF; // R
          pixels[offset + 1] = (abgr >> 8) & 0xFF; // G
          pixels[offset + 2] = (abgr >> 16) & 0xFF; // B
          pixels[offset + 3] = (abgr >> 24) & 0xFF; // A
        } else {
          pixels[offset + 3] = 0; // Alpha 0
        }
      }
    }

    ui.decodeImageFromPixels(
      pixels,
      image.width,
      image.height,
      ui.PixelFormat.rgba8888,
      (img) => completer.complete(img),
    );
    return completer.future;
  }

  @override
  Widget build(BuildContext context) {
    if (_cachedImage == null) {
      return const Center(
        child: SizedBox(
          width: 24,
          height: 24,
          child: CircularProgressIndicator(
            color: Colors.white24,
            strokeWidth: 2,
          ),
        ),
      );
    }

    return CustomPaint(
      painter: _ImagePainter(_cachedImage!),
      child: const SizedBox.expand(),
    );
  }
}

class _ImagePainter extends CustomPainter {
  /// Image already decoded into Flutter's native image representation.
  final ui.Image image;

  _ImagePainter(this.image);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..filterQuality = FilterQuality.none;
    final srcRect = Rect.fromLTWH(
      0,
      0,
      image.width.toDouble(),
      image.height.toDouble(),
    );
    final dstRect = Rect.fromLTWH(0, 0, size.width, size.height);

    // Draw the entire cached image in one pass
    canvas.drawImageRect(image, srcRect, dstRect, paint);
  }

  @override
  bool shouldRepaint(covariant _ImagePainter oldDelegate) =>
      oldDelegate.image != image;
}
