import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';

import 'package:ffi/ffi.dart';
import 'package:image/image.dart' as img;

// ── Windows constants ────────────────────────────────────────────────────────

const _cfDib = 8;
const _cfDibV4 = 20;

// ── Native function typedefs ─────────────────────────────────────────────────

typedef _OpenClipboardNative = Int32 Function(IntPtr hwnd);
typedef _OpenClipboard = int Function(int hwnd);

typedef _CloseClipboardNative = Int32 Function();
typedef _CloseClipboard = int Function();

typedef _IsFormatAvailableNative = Int32 Function(Uint32 format);
typedef _IsFormatAvailable = int Function(int format);

typedef _GetClipboardDataNative = IntPtr Function(Uint32 format);
typedef _GetClipboardData = int Function(int format);

typedef _GlobalSizeNative = IntPtr Function(IntPtr hMem);
typedef _GlobalSize = int Function(int hMem);

typedef _GlobalLockNative = Pointer<Uint8> Function(IntPtr hMem);
typedef _GlobalLock = Pointer<Uint8> Function(int hMem);

typedef _GlobalUnlockNative = Int32 Function(IntPtr hMem);
typedef _GlobalUnlock = int Function(int hMem);

typedef _RegisterFormatNative = Uint32 Function(Pointer<Utf16> name);
typedef _RegisterFormat = int Function(Pointer<Utf16> name);

// ── Public API ───────────────────────────────────────────────────────────────

/// Returns image bytes (PNG) from the Windows clipboard, or null if there is
/// no image or the platform is not Windows.
///
/// Checks the following formats in order:
///   1. "PNG"  – custom registered format used by browsers / screenshot tools
///   2. CF_DIBV4 / CF_DIB – Device-Independent Bitmap (Win+Shift+S, etc.)
Uint8List? getClipboardImageBytes() {
  if (!Platform.isWindows) return null;
  try {
    return _readClipboardImage();
  } catch (_) {
    return null;
  }
}

// ── Implementation ───────────────────────────────────────────────────────────

Uint8List? _readClipboardImage() {
  final user32 = DynamicLibrary.open('user32.dll');
  final kernel32 = DynamicLibrary.open('kernel32.dll');

  final openClipboard =
      user32.lookupFunction<_OpenClipboardNative, _OpenClipboard>('OpenClipboard');
  final closeClipboard =
      user32.lookupFunction<_CloseClipboardNative, _CloseClipboard>('CloseClipboard');
  final isFormatAvailable =
      user32.lookupFunction<_IsFormatAvailableNative, _IsFormatAvailable>(
          'IsClipboardFormatAvailable');
  final getClipboardData =
      user32.lookupFunction<_GetClipboardDataNative, _GetClipboardData>('GetClipboardData');
  final registerFormat =
      user32.lookupFunction<_RegisterFormatNative, _RegisterFormat>(
          'RegisterClipboardFormatW');
  final globalSize =
      kernel32.lookupFunction<_GlobalSizeNative, _GlobalSize>('GlobalSize');
  final globalLock =
      kernel32.lookupFunction<_GlobalLockNative, _GlobalLock>('GlobalLock');
  final globalUnlock =
      kernel32.lookupFunction<_GlobalUnlockNative, _GlobalUnlock>('GlobalUnlock');

  // Register custom "PNG" format (Chrome, Edge, Firefox, Snipping Tool export)
  final pngNamePtr = 'PNG'.toNativeUtf16();
  final pngFormat = registerFormat(pngNamePtr);
  malloc.free(pngNamePtr);

  if (openClipboard(0) == 0) return null;

  try {
    // 1. Try custom PNG format — bytes are ready-to-use PNG
    if (pngFormat != 0 && isFormatAvailable(pngFormat) != 0) {
      final bytes = _readGlobalBytes(
          getClipboardData(pngFormat), globalSize, globalLock, globalUnlock);
      if (bytes != null && bytes.isNotEmpty) return bytes;
    }

    // 2. Try CF_DIBV4 then CF_DIB — convert DIB to PNG via the image package
    for (final fmt in [_cfDibV4, _cfDib]) {
      if (isFormatAvailable(fmt) != 0) {
        final dibBytes = _readGlobalBytes(
            getClipboardData(fmt), globalSize, globalLock, globalUnlock);
        if (dibBytes != null && dibBytes.isNotEmpty) {
          final png = _dibToPng(dibBytes);
          if (png != null) return png;
        }
      }
    }

    return null;
  } finally {
    closeClipboard();
  }
}

/// Copy the bytes pointed to by a global memory handle.
Uint8List? _readGlobalBytes(
  int handle,
  _GlobalSize globalSize,
  _GlobalLock globalLock,
  _GlobalUnlock globalUnlock,
) {
  if (handle == 0) return null;
  final size = globalSize(handle);
  if (size == 0) return null;
  final ptr = globalLock(handle);
  if (ptr.address == 0) return null;
  try {
    return Uint8List.fromList(ptr.asTypedList(size));
  } finally {
    globalUnlock(handle);
  }
}

/// Convert a CF_DIB / CF_DIBV4 block to PNG bytes using the `image` package.
///
/// A DIB is essentially a BMP without the 14-byte file header. We prepend the
/// header and let the `image` package decode it.
Uint8List? _dibToPng(Uint8List dib) {
  if (dib.length < 40) return null; // minimum BITMAPINFOHEADER size

  final bd = ByteData.sublistView(dib);

  final headerSize = bd.getUint32(0, Endian.little); // biSize
  final bitCount = bd.getUint16(14, Endian.little);  // biBitCount
  final compression = bd.getUint32(16, Endian.little); // biCompression
  final clrUsed = bd.getUint32(32, Endian.little);     // biClrUsed

  // Calculate color table size (in bytes, each entry = 4 bytes RGBQUAD)
  int colorTableEntries;
  if (bitCount <= 8) {
    colorTableEntries = clrUsed != 0 ? clrUsed : (1 << bitCount);
  } else if (compression == 3 /* BI_BITFIELDS */ || compression == 6 /* BI_ALPHABITFIELDS */) {
    colorTableEntries = 0;
  } else {
    colorTableEntries = clrUsed;
  }
  final colorTableBytes = colorTableEntries * 4;

  // Offset to pixel data from start of FILE (= 14 file header bytes + DIB)
  final pixelOffset = 14 + headerSize + colorTableBytes;

  // Build the 14-byte BITMAPFILEHEADER
  final fileSize = 14 + dib.length;
  final fileHeader = Uint8List(14);
  final fh = ByteData.sublistView(fileHeader);
  fileHeader[0] = 0x42; // 'B'
  fileHeader[1] = 0x4D; // 'M'
  fh.setUint32(2, fileSize, Endian.little);
  fh.setUint32(6, 0, Endian.little);
  fh.setUint32(10, pixelOffset, Endian.little);

  final bmp = Uint8List(fileHeader.length + dib.length);
  bmp.setRange(0, fileHeader.length, fileHeader);
  bmp.setRange(fileHeader.length, bmp.length, dib);

  try {
    final decoded = img.decodeBmp(bmp);
    if (decoded == null) return null;
    return Uint8List.fromList(img.encodePng(decoded));
  } catch (_) {
    return null;
  }
}
