import 'dart:typed_data';

import '../../image_exception.dart';
import '../../formats/decode_info.dart';
import '../../hdr/hdr_image.dart';
import '../../hdr/hdr_slice.dart';
import '../../util/input_buffer.dart';
import 'exr_channel.dart';
import 'exr_compressor.dart';
import 'exr_part.dart';


class ExrImage extends DecodeInfo {
  /// An EXR image has one or more parts, each of which contains a framebuffer.
  List<InternalExrPart> _parts = [];

  ExrImage(List<int> bytes) {
    InputBuffer input = new InputBuffer(bytes);
    int magic = input.readUint32();
    if (magic != MAGIC) {
      throw new ImageException('File is not an OpenEXR image file.');
    }

    version = input.readByte();
    if (version != EXR_VERSION) {
      throw new ImageException('Cannot read version $version image files.');
    }

    flags = input.readUint24();
    if (!_supportsFlags(flags)) {
      throw new ImageException('The file format version number\'s flag field '
                               'contains unrecognized flags.');
    }

    if (!_isMultiPart()) {
      ExrPart part = new InternalExrPart(_isTiled(), input);
      if (part.isValid) {
        _parts.add(part);
      }
    } else {
      while (true) {
        ExrPart part = new InternalExrPart(_isTiled(), input);
        if (!part.isValid) {
          break;
        }
        _parts.add(part);
      }
    }

    if (_parts.isEmpty) {
      throw new ImageException('Error reading image header');
    }

    for (InternalExrPart part in _parts) {
      part.readOffsets(input);
    }

    _readImage(input);
  }

  List<ExrPart> get parts => _parts;

  int get numFrames => 1;

  /**
   * Parse just enough of the file to identify that it's an EXR image.
   */
  static bool isValidFile(List<int> bytes) {
    InputBuffer input = new InputBuffer(bytes);

    int magic = input.readUint32();
    if (magic != MAGIC) {
      return false;
    }

    int version = input.readByte();
    if (version != EXR_VERSION) {
      return false;
    }

    int flags = input.readUint24();
    if (!_supportsFlags(flags)) {
      return false;
    }

    return true;
  }

  int numParts() => _parts.length;

  ExrPart getPart(int i) => _parts[i];

  bool _isTiled()  {
    return (flags & TILED_FLAG) != 0;
  }

  bool _isMultiPart() {
    return flags & MULTI_PART_FILE_FLAG != 0;
  }

  /*bool _isNonImage() {
    return flags & NON_IMAGE_FLAG != 0;
  }*/

  static bool _supportsFlags(int flags) {
    return (flags & ~ALL_FLAGS) == 0;
  }

  void _readImage(InputBuffer input) {
    //final bool multiPart = _isMultiPart();

    for (int pi = 0; pi < _parts.length; ++pi) {
      InternalExrPart part = _parts[pi];
      HdrImage framebuffer = part.framebuffer;

      for (int ci = 0; ci < part.channels.length; ++ci) {
        ExrChannel ch = part.channels[ci];
        if (!framebuffer.hasChannel(ch.name)) {
          width = part.width;
          height = part.height;
          framebuffer.addSlice(new HdrSlice(ch.name, part.width, part.height,
                                            ch.type));
        }
      }

      if (part.tiled) {
        _readTiledPart(pi, input);
      } else {
        _readScanlinePart(pi, input);
      }
    }
  }

  void _readTiledPart(int pi, InputBuffer input) {
    InternalExrPart part = _parts[pi];
    final bool multiPart = _isMultiPart();
    HdrImage framebuffer = part.framebuffer;
    ExrCompressor compressor = part.compressor;
    List<Uint32List> offsets = part.offsets;
    //Uint32List fbi = new Uint32List(part.channels.length);

    InputBuffer imgData = new InputBuffer.from(input);
    for (int ly = 0, l = 0; ly < part.numYLevels; ++ly) {
      for (int lx = 0; lx < part.numXLevels; ++lx, ++l) {
        for (int ty = 0, oi = 0; ty < part.numYTiles[ly]; ++ty) {
          for (int tx = 0; tx < part.numXTiles[lx]; ++tx, ++oi) {
            // TODO support sub-levels (for rip/mip-mapping).
            if (l != 0) {
              break;
            }
            int offset = offsets[l][oi];
            imgData.offset = offset;

            if (multiPart) {
              int p = imgData.readUint32();
              if (p != pi) {
                throw new ImageException('Invalid Image Data');
              }
            }

            int tileX = imgData.readUint32();
            int tileY = imgData.readUint32();
            /*int levelX =*/ imgData.readUint32();
            /*int levelY =*/ imgData.readUint32();
            int dataSize = imgData.readUint32();
            InputBuffer data = imgData.readBytes(dataSize);

            int ty = tileY * part.tileHeight;
            int tx = tileX * part.tileWidth;

            int tileWidth = compressor.decodedWidth;
            int tileHeight = compressor.decodedHeight;

            if (tx + tileWidth > width) {
              tileWidth = width - tx;
            }
            if (ty + tileHeight > height) {
              tileHeight = height - ty;
            }

            Uint8List uncompressedData;
            if (compressor != null) {
              uncompressedData = compressor.uncompress(data, tx, ty,
                                                       part.tileWidth,
                                                       part.tileHeight);
              tileWidth = compressor.decodedWidth;
              tileHeight = compressor.decodedHeight;
            } else {
              uncompressedData = data.toUint8List();
            }

            int si = 0;
            int len = uncompressedData.length;
            int numChannels = part.channels.length;
            //int lineCount = 0;
            for (int yi = 0; yi < tileHeight && ty < height; ++yi, ++ty) {
              for (int ci = 0; ci < numChannels; ++ci) {
                ExrChannel ch = part.channels[ci];
                Uint8List slice = framebuffer[ch.name].getBytes();
                if (si >= len) {
                  break;
                }

                int tx = tileX * part.tileWidth;
                for (int xx = 0; xx < tileWidth; ++xx, ++tx) {
                  for (int bi = 0; bi < ch.size; ++bi) {
                    if (tx < part.width && ty < part.height) {
                      int di = (ty * part.width + tx) * ch.size + bi;
                      slice[di] = uncompressedData[si++];
                    } else {
                      si++;
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  void _readScanlinePart(int pi, InputBuffer input) {
    InternalExrPart part = _parts[pi];
    final bool multiPart = _isMultiPart();
    HdrImage framebuffer = part.framebuffer;
    ExrCompressor compressor = part.compressor;
    List<int> offsets = part.offsets[0];

    //int scanLineMin = part.top;
    //int scanLineMax = part.bottom;
    int linesInBuffer = part.linesInBuffer;

    //int minY = part.top;
    //int maxY = minY + part.linesInBuffer - 1;

    Uint32List fbi = new Uint32List(part.channels.length);
    //int total = 0;

    //int xx = 0;
    int yy = 0;

    InputBuffer imgData = new InputBuffer.from(input);
    for (int offset in offsets) {
      imgData.offset = offset;

      if (multiPart) {
        int p = imgData.readUint32();
        if (p != pi) {
          throw new ImageException('Invalid Image Data');
        }
      }

      /*int y =*/ imgData.readInt32();
      int dataSize = imgData.readInt32();
      InputBuffer data = imgData.readBytes(dataSize);

      Uint8List uncompressedData;
      if (compressor != null) {
        uncompressedData = compressor.uncompress(data, 0, yy);
      } else {
        uncompressedData = data.toUint8List();
      }

      int si = 0;
      int len = uncompressedData.length;
      int numChannels = part.channels.length;
      //int lineCount = 0;
      for (int yi = 0; yi < linesInBuffer && yy < height; ++yi, ++yy) {
        si = part.offsetInLineBuffer[yy];
        if (si >= len) {
          break;
        }

        for (int ci = 0; ci < numChannels; ++ci) {
          ExrChannel ch = part.channels[ci];
          Uint8List slice = framebuffer[ch.name].getBytes();
          if (si >= len) {
            break;
          }
          for (int xx = 0; xx < part.width; ++xx) {
            for (int bi = 0; bi < ch.size; ++bi) {
              slice[fbi[ci]++] = uncompressedData[si++];
            }
          }
        }
      }
    }
  }

  int version;
  int flags;

  /// The MAGIC number is stored in the first four bytes of every
  /// OpenEXR image file.  This can be used to quickly test whether
  /// a given file is an OpenEXR image file (see isImfMagic(), below).
  static const int MAGIC = 20000630;

  /// Value that goes into VERSION_NUMBER_FIELD.
  static const int EXR_VERSION = 2;

  /// File is tiled
  static const int TILED_FLAG = 0x000002;

  /// File contains long attribute or channel names
  static const int LONG_NAMES_FLAG = 0x000004;

  /// File has at least one part which is not a regular scanline image or
  /// regular tiled image (that is, it is a deep format).
  static const int NON_IMAGE_FLAG = 0x000008;

  /// File has multiple parts.
  static const int MULTI_PART_FILE_FLAG = 0x000010;

  /// Bitwise OR of all supported flags.
  static const int ALL_FLAGS = TILED_FLAG | LONG_NAMES_FLAG;
}
