import 'dart:collection';
import 'dart:convert';
import 'dart:typed_data';
import 'types.dart';

/// Main class to read a value out of a FlexBuffer.
///
/// This class let you access values stored in the buffer in a lazy fashion.
class Reference {
  final ByteData _buffer;
  final int _offset;
  final BitWidth _parentWidth;
  final String _path;
  int _byteWidth;
  ValueType _valueType;
  int _length;

  Reference._(this._buffer, this._offset, this._parentWidth, int packedType, this._path) {
    _byteWidth = 1 << (packedType & 3);
    _valueType = ValueTypeUtils.fromInt(packedType >> 2);
  }

  /// Use this method to access the root value of a FlexBuffer.
  static Reference fromBuffer(ByteBuffer buffer) {
    final len = buffer.lengthInBytes;
    if (len < 3) {
      throw UnsupportedError('Buffer needs to be bigger than 3');
    }
    final byteData = ByteData.view(buffer);
    final byteWidth = byteData.getUint8(len - 1);
    final packedType = byteData.getUint8(len - 2);
    final offset = len - byteWidth - 2;
    return Reference._(ByteData.view(buffer), offset, BitWidthUtil.fromByteWidth(byteWidth), packedType, "/");
  }

  /// Returns true if the underlying value is null.
  bool get isNull => _valueType == ValueType.Null;
  /// Returns true if the underlying value can be represented as [num].
  bool get isNum => ValueTypeUtils.isNumber(_valueType) || ValueTypeUtils.isIndirectNumber(_valueType);
  /// Returns true if the underlying value was encoded as a float (direct or indirect).
  bool get isDouble => _valueType == ValueType.Float || _valueType == ValueType.IndirectFloat;
  /// Returns true if the underlying value was encoded as an int or uint (direct or indirect).
  bool get isInt => isNum && !isDouble;
  /// Returns true if the underlying value was encoded as a string or a key.
  bool get isString => _valueType == ValueType.String || _valueType == ValueType.Key;
  /// Returns true if the underlying value was encoded as a bool.
  bool get isBool => _valueType == ValueType.Bool;
  /// Returns true if the underlying value was encoded as a blob.
  bool get isBlob => _valueType == ValueType.Blob;
  /// Returns true if the underlying value points to a vector.
  bool get isVector => ValueTypeUtils.isAVector(_valueType);
  /// Returns true if the underlying value points to a map.
  bool get isMap => _valueType == ValueType.Map;

  /// If this [isBool], returns the bool value. Otherwise, returns null.
  bool get boolValue {
    if(_valueType == ValueType.Bool) {
      return _readInt(_offset, _parentWidth) != 0;
    }
    return null;
  }

  /// Returns an [int], if the underlying value can be represented as an int.
  ///
  /// Otherwise returns [null].
  int get intValue {
    if (_valueType == ValueType.Int) {
      return _readInt(_offset, _parentWidth);
    }
    if (_valueType == ValueType.UInt) {
      return _readUInt(_offset, _parentWidth);
    }
    if (_valueType == ValueType.IndirectInt) {
      return _readInt(_indirect, BitWidthUtil.fromByteWidth(_byteWidth));
    }
    if (_valueType == ValueType.IndirectUInt) {
      return _readUInt(_indirect, BitWidthUtil.fromByteWidth(_byteWidth));
    }
    return null;
  }

  /// Returns [double], if the underlying value [isDouble].
  ///
  /// Otherwise returns [null].
  double get doubleValue {
    if (_valueType == ValueType.Float) {
      return _readFloat(_offset, _parentWidth);
    }
    if (_valueType == ValueType.IndirectFloat) {
      return _readFloat(_indirect, BitWidthUtil.fromByteWidth(_byteWidth));
    }
    return null;
  }

  /// Returns [num], if the underlying value is numeric, be it int uint, or float (direct or indirect).
  ///
  /// Otherwise returns [null].
  num get numValue => doubleValue ?? intValue;

  /// Returns [String] value or null otherwise.
  /// 
  /// This method performers a utf8 decoding, as FlexBuffers format stores strings in utf8 encoding. 
  String get stringValue {
    if (_valueType == ValueType.String || _valueType == ValueType.Key) {
      return utf8.decode(_buffer.buffer.asUint8List(_indirect, length));
    }
    return null;
  }

  /// Returns [Uint8List] value or null otherwise.
  Uint8List get blobValue {
    if (_valueType == ValueType.Blob) {
      return _buffer.buffer.asUint8List(_indirect, length);
    }
    return null;
  }

  /// Can be used with an [int] or a [String] value for key.
  /// If the underlying value in FlexBuffer is a vector, then use [int] for access.
  /// If the underlying value in FlexBuffer is a map, then use [String] for access.
  /// Returns [Reference] value. Throws an exception when [key] is not applicable.
  Reference operator [](Object key) {
    if (key is int && ValueTypeUtils.isAVector(_valueType)) {
      final index = key;
      if(index >= length || index < 0) {
        throw ArgumentError('Key: [$key] is not applicable on: $_path of: $_valueType length: $length');
      }
      final elementOffset = _indirect + index * _byteWidth;
      final reference = Reference._(_buffer, elementOffset, BitWidthUtil.fromByteWidth(_byteWidth), 0, "$_path[$index]");
      reference._byteWidth = 1;
      if (ValueTypeUtils.isTypedVector(_valueType)) {
        reference._valueType = ValueTypeUtils.typedVectorElementType(_valueType);
        return reference;
      }
      if(ValueTypeUtils.isFixedTypedVector(_valueType)) {
        reference._valueType = ValueTypeUtils.fixedTypedVectorElementType(_valueType);
        return reference;
      }
      final packedType = _buffer.getUint8(_indirect + length * _byteWidth + index);
      return Reference._(_buffer, elementOffset, BitWidthUtil.fromByteWidth(_byteWidth), packedType, "$_path[$index]");
    }
    if (key is String && _valueType == ValueType.Map) {
      final index = _keyIndex(key);
      if (index != null) {
        return _valueForIndexWithKey(index, key);
      }
    }
    throw ArgumentError('Key: [$key] is not applicable on: $_path of: $_valueType');
  }

  /// Get an iterable if the underlying flexBuffer value is a vector.
  /// Otherwise throws an exception.
  Iterable<Reference> get vectorIterable {
    if(isVector == false) {
      throw UnsupportedError('Value is not a vector. It is: $_valueType');
    }
    return _VectorIterator(this);
  }

  /// Get an iterable for keys if the underlying flexBuffer value is a map.
  /// Otherwise throws an exception.
  Iterable<String> get mapKeyIterable {
    if(isMap == false) {
      throw UnsupportedError('Value is not a map. It is: $_valueType');
    }
    return _MapKeyIterator(this);
  }

  /// Get an iterable for values if the underlying flexBuffer value is a map.
  /// Otherwise throws an exception.
  Iterable<Reference> get mapValueIterable {
    if(isMap == false) {
      throw UnsupportedError('Value is not a map. It is: $_valueType');
    }
    return _MapValueIterator(this);
  }

  /// Returns the length of the the underlying FlexBuffer value.
  /// If the underlying value is [null] the length is 0.
  /// If the underlying value is a number, or a bool, the length is 1.
  /// If the underlying value is a vector, or map, the length reflects number of elements / element pairs.
  /// If the values is a string or a blob, the length reflects a number of bytes the value occupies (strings are encoded in utf8 format).
  int get length {
    if (_length != null) {
      return _length;
    }
    // needs to be checked before more generic isAVector
    if(ValueTypeUtils.isFixedTypedVector(_valueType)) {
      _length = ValueTypeUtils.fixedTypedVectorElementSize(_valueType);
    } else if(_valueType == ValueType.Blob || ValueTypeUtils.isAVector(_valueType) || _valueType == ValueType.Map){
      _length = _readUInt(_indirect - _byteWidth, BitWidthUtil.fromByteWidth(_byteWidth));
    } else if (_valueType == ValueType.Null) {
      _length = 0;
    } else if (_valueType == ValueType.String) {
      final indirect = _indirect;
      var size_byte_width = _byteWidth;
      var size = _readUInt(indirect - size_byte_width, BitWidthUtil.fromByteWidth(size_byte_width));
      while (_buffer.getInt8(indirect + size) != 0) {
        size_byte_width <<= 1;
        size = _readUInt(indirect - size_byte_width, BitWidthUtil.fromByteWidth(size_byte_width));
      }
      _length = size;
    } else if (_valueType == ValueType.Key) {
      final indirect = _indirect;
      var size = 1;
      while (_buffer.getInt8(indirect + size) != 0) {
        size += 1;
      }
      _length = size;
    } else {
      _length = 1;
    }
    return _length;
  }


  /// Returns a minified JSON representation of the underlying FlexBuffer value.
  ///
  /// This method involves materializing the entire object tree, which may be
  /// expensive. It is more efficient to work with [Reference] and access only the needed data.
  /// Blob values are represented as base64 encoded string.
  String get json {
    if(_valueType == ValueType.Bool) {
      return boolValue ? 'true' : 'false';
    }
    if (_valueType == ValueType.Null) {
      return 'null';
    }
    if(ValueTypeUtils.isNumber(_valueType)) {
      return jsonEncode(numValue);
    }
    if (_valueType == ValueType.String) {
      return jsonEncode(stringValue);
    }
    if (_valueType == ValueType.Blob) {
      return jsonEncode(base64Encode(blobValue));
    }
    if (ValueTypeUtils.isAVector(_valueType)) {
      final result = StringBuffer();
      result.write('[');
      for (var i = 0; i < length; i++) {
        result.write(this[i].json);
        if (i < length - 1) {
          result.write(',');
        }
      }
      result.write(']');
      return result.toString();
    }
    if (_valueType == ValueType.Map) {
      final result = StringBuffer();
      result.write('{');
      for (var i = 0; i < length; i++) {
        result.write(jsonEncode(_keyForIndex(i)));
        result.write(':');
        result.write(_valueForIndex(i).json);
        if (i < length - 1) {
          result.write(',');
        }
      }
      result.write('}');
      return result.toString();
    }
    throw UnsupportedError('Type: $_valueType is not supported for JSON conversion');
  }

  /// Computes the indirect offset of the value.
  ///
  /// To optimize for the more common case of being called only once, this
  /// value is not cached. Callers that need to use it more than once should
  /// cache the return value in a local variable.
  int get _indirect {
    final step = _readUInt(_offset, _parentWidth);
    return _offset - step;
  }

  int _readInt(int offset, BitWidth width) {
    _validateOffset(offset, width);
    if (width == BitWidth.width8) {
      return _buffer.getInt8(offset);
    }
    if (width == BitWidth.width16) {
      return _buffer.getInt16(offset, Endian.little);
    }
    if (width == BitWidth.width32) {
      return _buffer.getInt32(offset, Endian.little);
    }
    return _buffer.getInt64(offset, Endian.little);
  }

  int _readUInt(int offset, BitWidth width) {
    _validateOffset(offset, width);
    if (width == BitWidth.width8) {
      return _buffer.getUint8(offset);
    }
    if (width == BitWidth.width16) {
      return _buffer.getUint16(offset, Endian.little);
    }
    if (width == BitWidth.width32) {
      return _buffer.getUint32(offset, Endian.little);
    }
    return _buffer.getUint64(offset, Endian.little);
  }

  double _readFloat(int offset, BitWidth width) {
    _validateOffset(offset, width);
    if (width.index < BitWidth.width32.index) {
      throw StateError('Bad width: $width');
    }

    if (width == BitWidth.width32) {
      return _buffer.getFloat32(offset, Endian.little);
    }

    return _buffer.getFloat64(offset, Endian.little);
  }

  void _validateOffset(int offset, BitWidth width) {
    if (_offset < 0 || _buffer.lengthInBytes <= offset + width.index || offset & (BitWidthUtil.toByteWidth(width) - 1) != 0) {
      throw StateError('Bad offset: $offset, width: $width');
    }
  }

  int _keyIndex(String key) {
    final input = utf8.encode(key);
    final keysVectorOffset = _indirect - _byteWidth * 3;
    final indirectOffset = keysVectorOffset - _readUInt(keysVectorOffset, BitWidthUtil.fromByteWidth(_byteWidth));
    final byteWidth = _readUInt(keysVectorOffset + _byteWidth, BitWidthUtil.fromByteWidth(_byteWidth));
    var low = 0;
    var high = length - 1;
    while (low <= high) {
      final mid = (high + low) >> 1;
      final dif = _diffKeys(input, mid, indirectOffset, byteWidth);
      if (dif == 0) return mid;
      if (dif < 0) {
        high = mid - 1;
      } else {
        low = mid + 1;
      }
    }
    return null;
  }

  int _diffKeys(List<int> input, int index, int indirect_offset, int byteWidth) {
    final keyOffset = indirect_offset + index * byteWidth;
    final keyIndirectOffset = keyOffset - _readUInt(keyOffset, BitWidthUtil.fromByteWidth(byteWidth));
    for (var i = 0; i < input.length; i++) {
      final dif = input[i] - _buffer.getUint8(keyIndirectOffset + i);
      if (dif != 0) {
        return dif;
      }
    }
    return (_buffer.getUint8(keyIndirectOffset + input.length) == 0) ? 0 : -1;
  }

  Reference _valueForIndexWithKey(int index, String key) {
    final indirect = _indirect;
    final elementOffset = indirect + index * _byteWidth;
    final packedType = _buffer.getUint8(indirect + length * _byteWidth + index);
    return Reference._(_buffer, elementOffset, BitWidthUtil.fromByteWidth(_byteWidth), packedType, "$_path/$key");
  }

  Reference _valueForIndex(int index) {
    final indirect = _indirect;
    final elementOffset = indirect + index * _byteWidth;
    final packedType = _buffer.getUint8(indirect + length * _byteWidth + index);
    return Reference._(_buffer, elementOffset, BitWidthUtil.fromByteWidth(_byteWidth), packedType, "$_path/[$index]");
  }

  String _keyForIndex(int index) {
    final keysVectorOffset = _indirect - _byteWidth * 3;
    final indirectOffset = keysVectorOffset - _readUInt(keysVectorOffset, BitWidthUtil.fromByteWidth(_byteWidth));
    final byteWidth = _readUInt(keysVectorOffset + _byteWidth, BitWidthUtil.fromByteWidth(_byteWidth));
    final keyOffset = indirectOffset + index * byteWidth;
    final keyIndirectOffset = keyOffset - _readUInt(keyOffset, BitWidthUtil.fromByteWidth(byteWidth));
    var length = 0;
    while (_buffer.getUint8(keyIndirectOffset + length) != 0) {
      length += 1;
    }
    return utf8.decode(_buffer.buffer.asUint8List(keyIndirectOffset, length));
  }

}

class _VectorIterator with IterableMixin<Reference> implements Iterator<Reference> {
  final Reference _vector;
  int index;

  _VectorIterator(this._vector) {
    index = -1;
  }

  @override
  Reference get current => _vector[index];

  @override
  bool moveNext() {
    index++;
    return index < _vector.length;
  }

  @override
  Iterator<Reference> get iterator => this;
}

class _MapKeyIterator with IterableMixin<String> implements Iterator<String> {
  final Reference _map;
  int index;

  _MapKeyIterator(this._map) {
    index = -1;
  }

  @override
  String get current => _map._keyForIndex(index);

  @override
  bool moveNext() {
    index++;
    return index < _map.length;
  }

  @override
  Iterator<String> get iterator => this;
}

class _MapValueIterator with IterableMixin<Reference> implements Iterator<Reference> {
  final Reference _map;
  int index;

  _MapValueIterator(this._map) {
    index = -1;
  }

  @override
  Reference get current => _map._valueForIndex(index);

  @override
  bool moveNext() {
    index++;
    return index < _map.length;
  }

  @override
  Iterator<Reference> get iterator => this;
}
