import 'dart:typed_data';

import '../../color.dart';
import '../../image.dart';
import '../../image_exception.dart';
import '../../util/output_buffer.dart';
import 'pvrtc_bit_utility.dart';
import 'pvrtc_color.dart';
import 'pvrtc_color_bounding_box.dart';
import 'pvrtc_packet.dart';

/**
 * Ported from Jeffrey Lim's PVRTC encoder/decoder,
 * https://bitbucket.org/jthlim/pvrtccompressor
 */
class PvrtcEncoder {
  // PVR Format
  static const int PVR_AUTO = -1;
  static const int PVR_RGB_2BPP = 0;
  static const int PVR_RGBA_2BPP = 1;
  static const int PVR_RGB_4BPP = 2;
  static const int PVR_RGBA_4BPP = 3;

  Uint8List encodePvr(Image bitmap, {int format: PVR_AUTO}) {
    OutputBuffer output = new OutputBuffer();

    var pvrtc;
    if (format == PVR_AUTO) {
       if (bitmap.format == Image.RGB) {
        pvrtc = encodeRgb4Bpp(bitmap);
        format = PVR_RGB_4BPP;
      } else {
        pvrtc = encodeRgba4Bpp(bitmap);
        format = PVR_RGBA_4BPP;
      }
    } else if (format == PVR_RGB_2BPP) {
      //pvrtc = encodeRgb2Bpp(bitmap);
      pvrtc = encodeRgb4Bpp(bitmap);
    } else if (format == PVR_RGBA_2BPP) {
      //pvrtc = encodeRgba2Bpp(bitmap);
      pvrtc = encodeRgba4Bpp(bitmap);
    } else if (format == PVR_RGB_4BPP) {
      pvrtc = encodeRgb4Bpp(bitmap);
    } else if (format == PVR_RGBA_4BPP) {
      pvrtc = encodeRgba4Bpp(bitmap);
    }

    int version = 55727696;
    int flags = 0;
    int pixelFormat = format;
    int channelOrder = 0;
    int colorSpace = 0;
    int channelType = 0;
    int height = bitmap.height;
    int width = bitmap.width;
    int depth = 1;
    int numSurfaces = 1;
    int numFaces = 1;
    int mipmapCount = 1;
    int metaDataSize = 0;

    output.writeUint32(version);
    output.writeUint32(flags);
    output.writeUint32(pixelFormat);
    output.writeUint32(channelOrder);
    output.writeUint32(colorSpace);
    output.writeUint32(channelType);
    output.writeUint32(height);
    output.writeUint32(width);
    output.writeUint32(depth);
    output.writeUint32(numSurfaces);
    output.writeUint32(numFaces);
    output.writeUint32(mipmapCount);
    output.writeUint32(metaDataSize);

    output.writeBytes(pvrtc);

    return output.getBytes();
  }

  Uint8List encodeRgb4Bpp(Image bitmap) {
    if (bitmap.width != bitmap.height) {
      throw new ImageException('PVRTC requires a square image.');
    }

    if (!BitUtility.isPowerOf2(bitmap.width)) {
      throw new ImageException('PVRTC requires a power-of-two sized image.');
    }

    final int size = bitmap.width;
    final int blocks = size ~/ 4;
    final int blockMask = blocks - 1;

    var bitmapData = bitmap.getBytes();

    // Allocate enough data for encoding the image.
    var outputData = new Uint8List((bitmap.width * bitmap.height) ~/ 2);
    var packet = new PvrtcPacket(outputData);
    var p0 = new PvrtcPacket(outputData);
    var p1 = new PvrtcPacket(outputData);
    var p2 = new PvrtcPacket(outputData);
    var p3 = new PvrtcPacket(outputData);

    for (int y = 0; y < blocks; ++y) {
      for (int x = 0; x < blocks; ++x) {
        packet.setBlock(x, y);
        packet.usePunchthroughAlpha = 0;
        var cbb = _calculateBoundingBoxRgb(bitmap, x, y);
        packet.setColorRgbA(cbb.min);
        packet.setColorRgbB(cbb.max);
      }
    }

    const factors = PvrtcPacket.BILINEAR_FACTORS;

    for (int y = 0; y < blocks; ++y) {
      for (int x = 0; x < blocks; ++x) {
        int factorIndex = 0;
        final pixelIndex = (y * 4 * size + x * 4) * 4;

        int modulationData = 0;

        for (int py = 0; py < 4; ++py) {
          final int yOffset = (py < 2) ? -1 : 0;
          final int y0 = (y + yOffset) & blockMask;
          final int y1 = (y0 + 1) & blockMask;

          for(int px = 0; px < 4; ++px) {
            final int xOffset = (px < 2) ? -1 : 0;
            final int x0 = (x + xOffset) & blockMask;
            final int x1 = (x0 + 1) & blockMask;

            p0.setBlock(x0, y0);
            p1.setBlock(x1, y0);
            p2.setBlock(x0, y1);
            p3.setBlock(x1, y1);

            var ca = p0.getColorRgbA() * factors[factorIndex][0] +
                     p1.getColorRgbA() * factors[factorIndex][1] +
                     p2.getColorRgbA() * factors[factorIndex][2] +
                     p3.getColorRgbA() * factors[factorIndex][3];

            var cb = p0.getColorRgbB() * factors[factorIndex][0] +
                     p1.getColorRgbB() * factors[factorIndex][1] +
                     p2.getColorRgbB() * factors[factorIndex][2] +
                     p3.getColorRgbB() * factors[factorIndex][3];

            int pi = pixelIndex + ((py * size + px) * 4);
            int r = bitmapData[pi];
            int g = bitmapData[pi + 1];
            int b = bitmapData[pi + 2];

            var d = cb - ca;
            var p = new PvrtcColorRgb(r * 16, g * 16, b * 16);
            var v = p - ca;

            // PVRTC uses weightings of 0, 3/8, 5/8 and 1
            // The boundaries for these are 3/16, 1/2 (=8/16), 13/16
            int projection = v.dotProd(d) * 16;
            int lengthSquared = d.dotProd(d);
            if (projection > 3 * lengthSquared) {
              modulationData++;
            }
            if (projection > 8 * lengthSquared) {
              modulationData++;
            }
            if (projection > 13 * lengthSquared) {
              modulationData++;
            }

            modulationData = BitUtility.rotateRight(modulationData, 2);

            factorIndex++;
          }
        }

        packet.setBlock(x, y);
        packet.modulationData = modulationData;
      }
    }

    return outputData;
  }


  Uint8List encodeRgba4Bpp(Image bitmap) {
    if (bitmap.width != bitmap.height) {
      throw new ImageException('PVRTC requires a square image.');
    }

    if (!BitUtility.isPowerOf2(bitmap.width)) {
      throw new ImageException('PVRTC requires a power-of-two sized image.');
    }

    final int size = bitmap.width;
    final int blocks = size ~/ 4;
    final int blockMask = blocks - 1;

    var bitmapData = bitmap.getBytes();

    // Allocate enough data for encoding the image.
    var outputData = new Uint8List((bitmap.width * bitmap.height) ~/ 2);
    var packet = new PvrtcPacket(outputData);
    var p0 = new PvrtcPacket(outputData);
    var p1 = new PvrtcPacket(outputData);
    var p2 = new PvrtcPacket(outputData);
    var p3 = new PvrtcPacket(outputData);

    for (int y = 0; y < blocks; ++y) {
      for (int x = 0; x < blocks; ++x) {
        packet.setBlock(x, y);
        packet.usePunchthroughAlpha = 0;
        var cbb = _calculateBoundingBoxRgba(bitmap, x, y);
        packet.setColorRgbaA(cbb.min);
        packet.setColorRgbaB(cbb.max);
      }
    }

    const factors = PvrtcPacket.BILINEAR_FACTORS;

    for (int y = 0; y < blocks; ++y) {
      for (int x = 0; x < blocks; ++x) {
        int factorIndex = 0;
        final pixelIndex = (y * 4 * size + x * 4) * 4;

        int modulationData = 0;

        for (int py = 0; py < 4; ++py) {
          final int yOffset = (py < 2) ? -1 : 0;
          final int y0 = (y + yOffset) & blockMask;
          final int y1 = (y0 + 1) & blockMask;

          for(int px = 0; px < 4; ++px) {
            final int xOffset = (px < 2) ? -1 : 0;
            final int x0 = (x + xOffset) & blockMask;
            final int x1 = (x0 + 1) & blockMask;

            p0.setBlock(x0, y0);
            p1.setBlock(x1, y0);
            p2.setBlock(x0, y1);
            p3.setBlock(x1, y1);

            var ca = p0.getColorRgbaA() * factors[factorIndex][0] +
                     p1.getColorRgbaA() * factors[factorIndex][1] +
                     p2.getColorRgbaA() * factors[factorIndex][2] +
                     p3.getColorRgbaA() * factors[factorIndex][3];

            var cb = p0.getColorRgbaB() * factors[factorIndex][0] +
                     p1.getColorRgbaB() * factors[factorIndex][1] +
                     p2.getColorRgbaB() * factors[factorIndex][2] +
                     p3.getColorRgbaB() * factors[factorIndex][3];

            int pi = pixelIndex + ((py * size + px) * 4);
            int r = bitmapData[pi];
            int g = bitmapData[pi + 1];
            int b = bitmapData[pi + 2];
            int a = bitmapData[pi + 3];

            var d = cb - ca;
            var p = new PvrtcColorRgba(r * 16, g * 16, b * 16, a * 16);
            var v = p - ca;

            // PVRTC uses weightings of 0, 3/8, 5/8 and 1
            // The boundaries for these are 3/16, 1/2 (=8/16), 13/16
            int projection = v.dotProd(d) * 16;
            int lengthSquared = d.dotProd(d);

            if (projection > 3 * lengthSquared) {
              modulationData++;
            }
            if (projection > 8 * lengthSquared) {
              modulationData++;
            }
            if (projection > 13 * lengthSquared) {
              modulationData++;
            }

            modulationData = BitUtility.rotateRight(modulationData, 2);

            factorIndex++;
          }
        }

        packet.setBlock(x, y);
        packet.modulationData = modulationData;
      }
    }

    return outputData;
  }

  static PvrtcColorBoundingBox _calculateBoundingBoxRgb(Image bitmap,
                                                        int blockX,
                                                        int blockY) {
    int size = bitmap.width;
    int pi = (blockY * 4 * size + blockX * 4);

    _pixel(i) {
      int c = bitmap[pi + i];
      return new PvrtcColorRgb(getRed(c), getGreen(c), getBlue(c));
    }

    var cbb = new PvrtcColorBoundingBox(_pixel(0), _pixel(0));
    cbb.add(_pixel(1));
    cbb.add(_pixel(2));
    cbb.add(_pixel(3));

    cbb.add(_pixel(size));
    cbb.add(_pixel(size + 1));
    cbb.add(_pixel(size + 2));
    cbb.add(_pixel(size + 3));

    cbb.add(_pixel(2 * size));
    cbb.add(_pixel(2 * size + 1));
    cbb.add(_pixel(2 * size + 2));
    cbb.add(_pixel(2 * size + 3));

    cbb.add(_pixel(3 * size));
    cbb.add(_pixel(3 * size + 1));
    cbb.add(_pixel(3 * size + 2));
    cbb.add(_pixel(3 * size + 3));

    return cbb;
  }

  static PvrtcColorBoundingBox _calculateBoundingBoxRgba(Image bitmap,
                                                         int blockX,
                                                         int blockY) {
    int size = bitmap.width;
    int pi = (blockY * 4 * size + blockX * 4);

    _pixel(i) {
      int c = bitmap[pi + i];
      return new PvrtcColorRgba(getRed(c), getGreen(c), getBlue(c), getAlpha(c));
    }

    var cbb = new PvrtcColorBoundingBox(_pixel(0), _pixel(0));
    cbb.add(_pixel(1));
    cbb.add(_pixel(2));
    cbb.add(_pixel(3));

    cbb.add(_pixel(size));
    cbb.add(_pixel(size + 1));
    cbb.add(_pixel(size + 2));
    cbb.add(_pixel(size + 3));

    cbb.add(_pixel(2 * size));
    cbb.add(_pixel(2 * size + 1));
    cbb.add(_pixel(2 * size + 2));
    cbb.add(_pixel(2 * size + 3));

    cbb.add(_pixel(3 * size));
    cbb.add(_pixel(3 * size + 1));
    cbb.add(_pixel(3 * size + 2));
    cbb.add(_pixel(3 * size + 3));

    return cbb;
  }

  static void _getPacket(packet, packetData, index) {
    index *= 2;
    packet.modulationData = packetData[index];
    packet.colorData = packetData[index + 1];
  }

  static const MODULATION_LUT =
      const [ 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3 ];
}
