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;
  final int _byteWidth;
  final ValueType _valueType;
  int? _length;

  Reference._(
    this._buffer,
    this._offset,
    this._parentWidth,
    int packedType,
    this._path, [
    int? byteWidth,
    ValueType? valueType,
  ]) : _byteWidth = byteWidth ?? 1 << (packedType & 3),
       _valueType = 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;
      int packedType = 0;
      int? byteWidth;
      ValueType? valueType;
      if (ValueTypeUtils.isTypedVector(_valueType)) {
        byteWidth = 1;
        valueType = ValueTypeUtils.typedVectorElementType(_valueType);
      } else if (ValueTypeUtils.isFixedTypedVector(_valueType)) {
        byteWidth = 1;
        valueType = ValueTypeUtils.fixedTypedVectorElementType(_valueType);
      } else {
        packedType = _buffer.getUint8(_indirect + length * _byteWidth + index);
      }
      return Reference._(
        _buffer,
        elementOffset,
        BitWidthUtil.fromByteWidth(_byteWidth),
        packedType,
        "$_path[$index]",
        byteWidth,
        valueType,
      );
    }
    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 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) {
      // 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 sizeByteWidth = _byteWidth;
        var size = _readUInt(
          indirect - sizeByteWidth,
          BitWidthUtil.fromByteWidth(sizeByteWidth),
        );
        while (_buffer.getInt8(indirect + size) != 0) {
          sizeByteWidth <<= 1;
          size = _readUInt(
            indirect - sizeByteWidth,
            BitWidthUtil.fromByteWidth(sizeByteWidth),
          );
        }
        _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 indirectOffset, int byteWidth) {
    final keyOffset = indirectOffset + 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 = -1;

  _VectorIterator(this._vector);

  @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 = -1;

  _MapKeyIterator(this._map);

  @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 = -1;

  _MapValueIterator(this._map);

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

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

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