import 'dart:math' as Math;
import 'dart:typed_data';

import '../../image_exception.dart';
import '../../internal/internal.dart';
import '../../hdr/hdr_image.dart';
import '../../util/input_buffer.dart';
import 'exr_attribute.dart';
import 'exr_channel.dart';
import 'exr_compressor.dart';

class ExrPart {
  /// The framebuffer for this exr part.
  HdrImage framebuffer = new HdrImage();
  /// The channels present in this part.
  List<ExrChannel> channels = [];
  /// The extra attributes read from the part header.
  Map<String, ExrAttribute> attributes = {};
  /// The display window (see the openexr documentation).
  List<int> displayWindow;
  /// The data window (see the openexr documentation).
  List<int> dataWindow;
  /// width of the data window
  int width;
  /// Height of the data window
  int height;
  double pixelAspectRatio = 1.0;
  double screenWindowCenterX = 0.0;
  double screenWindowCenterY = 0.0;
  double screenWindowWidth = 1.0;
  Float32List chromaticities;

  ExrPart(this._tiled, InputBuffer input) {
    _type = _tiled ? ExrPart.TYPE_TILE : ExrPart.TYPE_SCANLINE;

    while (true) {
      String name = input.readString();
      if (name == null || name.isEmpty) {
        break;
      }

      String type = input.readString();
      int size = input.readUint32();
      InputBuffer value = input.readBytes(size);

      attributes[name] = new ExrAttribute(name, type, size, value);

      switch (name) {
        case 'channels':
          while (true) {
            ExrChannel channel = new ExrChannel(value);
            if (!channel.isValid) {
              break;
            }
            channels.add(channel);
          }
          break;
        case 'chromaticities':
          chromaticities = new Float32List(8);
          chromaticities[0] = value.readFloat32();
          chromaticities[1] = value.readFloat32();
          chromaticities[2] = value.readFloat32();
          chromaticities[3] = value.readFloat32();
          chromaticities[4] = value.readFloat32();
          chromaticities[5] = value.readFloat32();
          chromaticities[6] = value.readFloat32();
          chromaticities[7] = value.readFloat32();
          break;
        case 'compression':
          _compressionType = value.readByte();
          if (_compressionType > 7) {
            throw new ImageException('EXR Invalid compression type');
          }
          break;
        case 'dataWindow':
          dataWindow = [value.readInt32(), value.readInt32(),
                        value.readInt32(), value.readInt32()];
          width = (dataWindow[2] - dataWindow[0]) + 1;
          height = (dataWindow[3] - dataWindow[1]) + 1;
          break;
        case 'displayWindow':
          displayWindow = [value.readInt32(), value.readInt32(),
                           value.readInt32(), value.readInt32()];
          break;
        case 'lineOrder':
          _lineOrder = value.readByte();
          break;
        case 'pixelAspectRatio':
          pixelAspectRatio = value.readFloat32();
          break;
        case 'screenWindowCenter':
          screenWindowCenterX = value.readFloat32();
          screenWindowCenterY = value.readFloat32();
          break;
        case 'screenWindowWidth':
          screenWindowWidth = value.readFloat32();
          break;
        case 'tiles':
          _tileWidth = value.readUint32();
          _tileHeight = value.readUint32();
          int mode = value.readByte();
          _tileLevelMode = mode & 0xf;
          _tileRoundingMode = (mode >> 4) & 0xf;
          break;
        case 'type':
          String s = value.readString();
          if (s == 'deepscanline') {
            this._type = TYPE_DEEP_SCANLINE;
          } else if (s == 'deeptile') {
            this._type = TYPE_DEEP_TILE;
          } else {
            throw new ImageException('EXR Invalid type: $s');
          }
          break;
        default:
          break;
      }
    }

    if (_tiled) {
      _numXLevels = _calculateNumXLevels(left, right, top, bottom);
      _numYLevels = _calculateNumYLevels(left, right, top, bottom);
      if (_tileLevelMode != RIPMAP_LEVELS) {
        _numYLevels = 1;
      }

      _numXTiles = new List<int>(_numXLevels);
      _numYTiles = new List<int>(_numYLevels);

      _calculateNumTiles(_numXTiles, _numXLevels, left, right, _tileWidth,
                         _tileRoundingMode);

      _calculateNumTiles(_numYTiles, _numYLevels, top, bottom, _tileHeight,
                         _tileRoundingMode);

      _bytesPerPixel = _calculateBytesPerPixel();
      _maxBytesPerTileLine = _bytesPerPixel * _tileWidth;
      _tileBufferSize = _maxBytesPerTileLine * _tileHeight;

      _compressor = new ExrCompressor(_compressionType, this,
                                      _maxBytesPerTileLine, _tileHeight);

      _offsets = new List<Uint32List>(_numXLevels * _numYLevels);
      for (int ly = 0, l = 0; ly < _numYLevels; ++ly) {
        for (int lx = 0; lx < _numXLevels; ++lx, ++l) {
          _offsets[l] = new Uint32List(_numXTiles[lx] * _numYTiles[ly]);
        }
      }
    } else {
      _bytesPerLine = new Uint32List(height + 1);
      for (ExrChannel ch in channels) {
        int nBytes = ch.size * width ~/ ch.xSampling;
        for (int y = 0; y < height; ++y) {
          if ((y + top) % ch.ySampling == 0) {
            _bytesPerLine[y] += nBytes;
          }
        }
      }

      int maxBytesPerLine = 0;
      for (int y = 0; y < height; ++y) {
        maxBytesPerLine = Math.max(maxBytesPerLine, _bytesPerLine[y]);
      }

      _compressor = new ExrCompressor(_compressionType, this, maxBytesPerLine);

      _linesInBuffer = _compressor.numScanLines();
      _lineBufferSize = maxBytesPerLine * _linesInBuffer;

      _offsetInLineBuffer = new Uint32List(_bytesPerLine.length);

      int offset = 0;
      for (int i = 0; i <= _bytesPerLine.length - 1; ++i) {
        if (i % _linesInBuffer == 0) {
          offset = 0;
        }
        _offsetInLineBuffer[i] = offset;
        offset += _bytesPerLine[i];
      }

      int numOffsets = ((height + _linesInBuffer) ~/ _linesInBuffer) - 1;
      _offsets = [new Uint32List(numOffsets)];
    }
  }

  int get left => dataWindow[0];

  int get top => dataWindow[1];

  int get right => dataWindow[2];

  int get bottom => dataWindow[3];

  /**
   * Was this part successfully decoded?
   */
  bool get isValid => width != null;

  int _calculateNumXLevels(int minX, int maxX, int minY, int maxY) {
    int num = 0;

    switch (_tileLevelMode) {
      case ONE_LEVEL:
        num = 1;
        break;
      case MIPMAP_LEVELS:
        int w = maxX - minX + 1;
        int h = maxY - minY + 1;
        num = _roundLog2(Math.max(w, h), _tileRoundingMode) + 1;
        break;
      case RIPMAP_LEVELS:
        int w = maxX - minX + 1;
        num = _roundLog2(w, _tileRoundingMode) + 1;
        break;
      default:
        throw new ImageException("Unknown LevelMode format.");
    }

    return num;
  }


  int _calculateNumYLevels(int minX, int maxX, int minY, int maxY) {
    int num = 0;

    switch (_tileLevelMode) {
      case ONE_LEVEL:
        num = 1;
        break;
      case MIPMAP_LEVELS:
        int w = (maxX - minX) + 1;
        int h = (maxY - minY) + 1;
        num = _roundLog2(Math.max(w, h), _tileRoundingMode) + 1;
        break;
      case RIPMAP_LEVELS:
        int h = (maxY - minY) + 1;
        num = _roundLog2(h, _tileRoundingMode) + 1;
        break;
      default:
        throw new ImageException('Unknown LevelMode format.');
    }

    return num;
  }

  int _roundLog2(int x, int rmode) {
    return (rmode == ROUND_DOWN) ? _floorLog2(x) : _ceilLog2(x);
  }

  int _floorLog2(int x) {
    int y = 0;

    while (x > 1) {
      y +=  1;
      x >>= 1;
    }

    return y;
  }


  int _ceilLog2(int x) {
    int y = 0;
    int r = 0;

    while (x > 1) {
      if (x & 1 != 0) {
        r = 1;
      }

      y +=  1;
      x >>= 1;
    }

    return y + r;
  }

  int _calculateBytesPerPixel() {
    int bytesPerPixel = 0;

    for (ExrChannel ch in channels) {
      bytesPerPixel += ch.size;
    }

    return bytesPerPixel;
  }

  void _calculateNumTiles(List<int> numTiles, int numLevels,
                          int min, int max, int size, int rmode) {
    for (int i = 0; i < numLevels; i++) {
      numTiles[i] = (_levelSize(min, max, i, rmode) + size - 1) ~/ size;
    }
  }

  int _levelSize(int min, int max, int l, int rmode) {
    if (l < 0) {
      throw new ImageException('Argument not in valid range.');
    }

    int a = (max - min) + 1;
    int b = (1 << l);
    int size = a ~/ b;

    if (rmode == ROUND_UP && size * b < a) {
      size += 1;
    }

    return Math.max(size, 1);
  }

  static const int TYPE_SCANLINE = 0;
  static const int TYPE_TILE = 1;
  static const int TYPE_DEEP_SCANLINE = 2;
  static const int TYPE_DEEP_TILE = 3;

  static const int INCREASING_Y = 0;
  static const int DECREASING_Y = 1;
  static const int RANDOM_Y = 2;

  static const int ONE_LEVEL = 0;
  static const int MIPMAP_LEVELS = 1;
  static const int RIPMAP_LEVELS = 2;

  static const int ROUND_DOWN = 0;
  static const int ROUND_UP = 1;

  int _type;
  int _lineOrder = INCREASING_Y;
  int _compressionType = ExrCompressor.NO_COMPRESSION;
  List<Uint32List> _offsets;

  Uint32List _bytesPerLine;
  ExrCompressor _compressor;
  int _linesInBuffer;
  int _lineBufferSize;
  Uint32List _offsetInLineBuffer;

  bool _tiled;
  int _tileWidth;
  int _tileHeight;
  int _tileLevelMode;
  int _tileRoundingMode;
  List<int> _numXTiles;
  List<int> _numYTiles;
  int _numXLevels;
  int _numYLevels;
  int _bytesPerPixel;
  int _maxBytesPerTileLine;
  int _tileBufferSize;
}

@internal
class InternalExrPart extends ExrPart {
  InternalExrPart(bool tiled, InputBuffer input) : super(tiled, input);

  List<Uint32List> get offsets => _offsets;

  ExrCompressor get compressor => _compressor;
  int get linesInBuffer => _linesInBuffer;
  Uint32List get offsetInLineBuffer => _offsetInLineBuffer;

  bool get tiled => _tiled;
  int get tileWidth => _tileWidth;
  int get tileHeight => _tileHeight;
  List<int> get numXTiles => _numXTiles;
  List<int> get numYTiles => _numYTiles;
  int get numXLevels => _numXLevels;
  int get numYLevels => _numYLevels;

  void readOffsets(InputBuffer input) {
    if (_tiled) {
      for (int i = 0; i < _offsets.length; ++i) {
        for (int j = 0; j < _offsets[i].length; ++j) {
          _offsets[i][j] = input.readUint64();
        }
      }
    } else {
      int numOffsets = _offsets[0].length;
      for (int i = 0; i < numOffsets; ++i) {
        _offsets[0][i] = input.readUint64();
      }
    }
  }
}
