import 'dart:convert';
import 'dart:typed_data';

import 'types.dart';

/// The main builder class for creation of a FlexBuffer.
class Builder {
  final ByteData _buffer;
  List<_StackValue> _stack = [];
  List<_StackPointer> _stackPointers = [];
  int _offset = 0;
  bool _finished = false;
  final Map<String, _StackValue> _stringCache = {};
  final Map<String, _StackValue> _keyCache = {};
  final Map<_KeysHash, _StackValue> _keyVectorCache = {};
  final Map<int, _StackValue> _indirectIntCache = {};
  final Map<double, _StackValue> _indirectDoubleCache = {};

  /// Instantiate the builder if you intent to gradually build up the buffer by calling
  /// add... methods and calling [finish] to receive the resulting byte array.
  ///
  /// The default size of internal buffer is set to 2048. Provide a different value in order to avoid buffer copies.
  Builder({int size = 2048}) : _buffer = ByteData(size);

  /// Use this method in order to turn an object into a FlexBuffer directly.
  ///
  /// Use the manual instantiation of the [Builder] and gradual addition of values, if performance is more important than convenience.
  static ByteBuffer buildFromObject(Object? value) {
    final builder = Builder();
    builder._add(value);
    final buffer = builder.finish();
    final byteData = ByteData(buffer.lengthInBytes);
    byteData.buffer.asUint8List().setAll(0, buffer);
    return byteData.buffer;
  }

  void _add(Object? value) {
    if (value == null) {
      addNull();
    } else if (value is bool) {
      addBool(value);
    } else if (value is int) {
      addInt(value);
    } else if (value is double) {
      addDouble(value);
    } else if (value is ByteBuffer) {
      addBlob(value);
    } else if (value is String) {
      addString(value);
    } else if (value is List<dynamic>) {
      startVector();
      for (var i = 0; i < value.length; i++) {
        _add(value[i]);
      }
      end();
    } else if (value is Map<String, dynamic>) {
      startMap();
      value.forEach((key, value) {
        addKey(key);
        _add(value);
      });
      end();
    } else {
      throw UnsupportedError('Value of unexpected type: $value');
    }
  }

  /// Use this method if you want to store a null value.
  ///
  /// Specifically useful when building up a vector where values can be null.
  void addNull() {
    _integrityCheckOnValueAddition();
    _stack.add(_StackValue.withNull());
  }

  /// Adds a string value.
  void addInt(int value) {
    _integrityCheckOnValueAddition();
    _stack.add(_StackValue.withInt(value));
  }

  /// Adds a bool value.
  void addBool(bool value) {
    _integrityCheckOnValueAddition();
    _stack.add(_StackValue.withBool(value));
  }

  /// Adds a double value.
  void addDouble(double value) {
    _integrityCheckOnValueAddition();
    _stack.add(_StackValue.withDouble(value));
  }

  /// Adds a string value.
  void addString(String value) {
    _integrityCheckOnValueAddition();
    if (_stringCache.containsKey(value)) {
      _stack.add(_stringCache[value]!);
      return;
    }
    final utf8String = utf8.encode(value);
    final length = utf8String.length;
    final bitWidth = BitWidthUtil.uwidth(length);
    final byteWidth = _align(bitWidth);
    _writeUInt(length, byteWidth);
    final stringOffset = _offset;
    final newOffset = _newOffset(length + 1);
    _pushBuffer(utf8String);
    _offset = newOffset;
    final stackValue = _StackValue.withOffset(
      stringOffset,
      ValueType.String,
      bitWidth,
    );
    _stack.add(stackValue);
    _stringCache[value] = stackValue;
  }

  /// This methods adds a key to a map and should be followed by an add... value call.
  ///
  /// It also implies that you call this method only after you called [startMap].
  void addKey(String value) {
    _integrityCheckOnKeyAddition();
    if (_keyCache.containsKey(value)) {
      _stack.add(_keyCache[value]!);
      return;
    }
    final utf8String = utf8.encode(value);
    final length = utf8String.length;
    final keyOffset = _offset;
    final newOffset = _newOffset(length + 1);
    _pushBuffer(utf8String);
    _offset = newOffset;
    final stackValue = _StackValue.withOffset(
      keyOffset,
      ValueType.Key,
      BitWidth.width8,
    );
    _stack.add(stackValue);
    _keyCache[value] = stackValue;
  }

  /// Adds a byte array.
  ///
  /// This method can be used to store any generic BLOB.
  void addBlob(ByteBuffer value) {
    _integrityCheckOnValueAddition();
    final length = value.lengthInBytes;
    final bitWidth = BitWidthUtil.uwidth(length);
    final byteWidth = _align(bitWidth);
    _writeUInt(length, byteWidth);
    final blobOffset = _offset;
    final newOffset = _newOffset(length);
    _pushBuffer(value.asUint8List());
    _offset = newOffset;
    final stackValue = _StackValue.withOffset(
      blobOffset,
      ValueType.Blob,
      bitWidth,
    );
    _stack.add(stackValue);
  }

  /// Stores int value indirectly in the buffer.
  ///
  /// Adding large integer values indirectly might be beneficial if those values suppose to be store in a vector together with small integer values.
  /// This is due to the fact that FlexBuffers will add padding to small integer values, if they are stored together with large integer values.
  /// When we add integer indirectly the vector of ints will contain not the value itself, but only the relative offset to the value.
  /// By setting the [cache] parameter to true, you make sure that the builder tracks added int value and performs deduplication.
  void addIntIndirectly(int value, {bool cache = false}) {
    _integrityCheckOnValueAddition();
    if (_indirectIntCache.containsKey(value)) {
      _stack.add(_indirectIntCache[value]!);
      return;
    }
    final stackValue = _StackValue.withInt(value);
    final byteWidth = _align(stackValue.width);
    final newOffset = _newOffset(byteWidth);
    final valueOffset = _offset;
    _pushBuffer(stackValue.asU8List(stackValue.width));
    final stackOffset = _StackValue.withOffset(
      valueOffset,
      ValueType.IndirectInt,
      stackValue.width,
    );
    _stack.add(stackOffset);
    _offset = newOffset;
    if (cache) {
      _indirectIntCache[value] = stackOffset;
    }
  }

  /// Stores double value indirectly in the buffer.
  ///
  /// Double are stored as 8 or 4 byte values in FlexBuffers. If they are stored in a mixed vector, values which are smaller than 4 / 8 bytes will be padded.
  /// When we add double indirectly, the vector will contain not the value itself, but only the relative offset to the value. Which could occupy only 1 or 2 bytes, reducing the odds for unnecessary padding.
  /// By setting the [cache] parameter to true, you make sure that the builder tracks already added double value and performs deduplication.
  void addDoubleIndirectly(double value, {bool cache = false}) {
    _integrityCheckOnValueAddition();
    if (cache && _indirectDoubleCache.containsKey(value)) {
      _stack.add(_indirectDoubleCache[value]!);
      return;
    }
    final stackValue = _StackValue.withDouble(value);
    final byteWidth = _align(stackValue.width);
    final newOffset = _newOffset(byteWidth);
    final valueOffset = _offset;
    _pushBuffer(stackValue.asU8List(stackValue.width));
    final stackOffset = _StackValue.withOffset(
      valueOffset,
      ValueType.IndirectFloat,
      stackValue.width,
    );
    _stack.add(stackOffset);
    _offset = newOffset;
    if (cache) {
      _indirectDoubleCache[value] = stackOffset;
    }
  }

  /// This method starts a vector definition and needs to be followed by 0 to n add... value calls.
  ///
  /// The vector definition needs to be finished with an [end] call.
  /// It is also possible to add nested vector or map by calling [startVector] / [startMap].
  void startVector() {
    _integrityCheckOnValueAddition();
    _stackPointers.add(_StackPointer(_stack.length, true));
  }

  /// This method starts a map definition.
  ///
  /// This method call needs to be followed by 0 to n [addKey] +  add... value calls.
  /// The map definition needs to be finished with an [end] call.
  /// It is also possible to add nested vector or map by calling [startVector] / [startMap] after calling [addKey].
  void startMap() {
    _integrityCheckOnValueAddition();
    _stackPointers.add(_StackPointer(_stack.length, false));
  }

  /// Marks that the addition of values to the last vector, or map have ended.
  void end() {
    final pointer = _stackPointers.removeLast();
    if (pointer.isVector) {
      _endVector(pointer);
    } else {
      _sortKeysAndEndMap(pointer);
    }
  }

  /// Finish building the FlatBuffer and return array of bytes.
  ///
  /// Can be called multiple times, to get the array of bytes.
  /// After the first call, adding values, or starting vectors / maps will result in an exception.
  Uint8List finish() {
    if (_finished == false) {
      _finish();
    }
    return _buffer.buffer.asUint8List(0, _offset);
  }

  /// Builds a FlatBuffer with current state without finishing the builder.
  ///
  /// Creates an internal temporary copy of current builder and finishes the copy.
  /// Use this method, when the state of a long lasting builder need to be persisted periodically.
  ByteBuffer snapshot() {
    final tmp = Builder(size: _offset + 200);
    tmp._offset = _offset;
    tmp._stack = List.from(_stack);
    tmp._stackPointers = List.from(_stackPointers);
    tmp._buffer.buffer.asUint8List().setAll(
      0,
      _buffer.buffer.asUint8List(0, _offset),
    );
    for (var i = 0; i < tmp._stackPointers.length; i++) {
      tmp.end();
    }
    final buffer = tmp.finish();
    final bd = ByteData(buffer.lengthInBytes);
    bd.buffer.asUint8List().setAll(0, buffer);
    return bd.buffer;
  }

  void _integrityCheckOnValueAddition() {
    if (_finished) {
      throw StateError('Adding values after finish is prohibited');
    }
    if (_stackPointers.isNotEmpty && _stackPointers.last.isVector == false) {
      if (_stack.last.type != ValueType.Key) {
        throw StateError(
          'Adding value to a map before adding a key is prohibited',
        );
      }
    }
  }

  void _integrityCheckOnKeyAddition() {
    if (_finished) {
      throw StateError('Adding values after finish is prohibited');
    }
    if (_stackPointers.isEmpty || _stackPointers.last.isVector) {
      throw StateError('Adding key before staring a map is prohibited');
    }
  }

  void _finish() {
    if (_stack.length != 1) {
      throw StateError(
        'Stack has to be exactly 1, but is ${_stack.length}. You have to end all started vectors and maps, before calling [finish]',
      );
    }
    final value = _stack[0];
    final byteWidth = _align(value.elementWidth(_offset, 0));
    _writeStackValue(value, byteWidth);
    _writeUInt(value.storedPackedType(), 1);
    _writeUInt(byteWidth, 1);
    _finished = true;
  }

  _StackValue _createVector(
    int start,
    int vecLength,
    int step, [
    _StackValue? keys,
  ]) {
    var bitWidth = BitWidthUtil.uwidth(vecLength);
    var prefixElements = 1;
    if (keys != null) {
      var elemWidth = keys.elementWidth(_offset, 0);
      if (elemWidth.index > bitWidth.index) {
        bitWidth = elemWidth;
      }
      prefixElements += 2;
    }
    var vectorType = ValueType.Key;
    var typed = keys == null;
    for (var i = start; i < _stack.length; i += step) {
      final elemWidth = _stack[i].elementWidth(_offset, i + prefixElements);
      if (elemWidth.index > bitWidth.index) {
        bitWidth = elemWidth;
      }
      if (i == start) {
        vectorType = _stack[i].type;
        typed &= ValueTypeUtils.isTypedVectorElement(vectorType);
      } else {
        if (vectorType != _stack[i].type) {
          typed = false;
        }
      }
    }
    final byteWidth = _align(bitWidth);
    final fix =
        typed & ValueTypeUtils.isNumber(vectorType) &&
        vecLength >= 2 &&
        vecLength <= 4;
    if (keys != null) {
      _writeStackValue(keys, byteWidth);
      _writeUInt(1 << keys.width.index, byteWidth);
    }
    if (fix == false) {
      _writeUInt(vecLength, byteWidth);
    }
    final vecOffset = _offset;
    for (var i = start; i < _stack.length; i += step) {
      _writeStackValue(_stack[i], byteWidth);
    }
    if (typed == false) {
      for (var i = start; i < _stack.length; i += step) {
        _writeUInt(_stack[i].storedPackedType(), 1);
      }
    }
    if (keys != null) {
      return _StackValue.withOffset(vecOffset, ValueType.Map, bitWidth);
    }
    if (typed) {
      final vType = ValueTypeUtils.toTypedVector(
        vectorType,
        fix ? vecLength : 0,
      );
      return _StackValue.withOffset(vecOffset, vType, bitWidth);
    }
    return _StackValue.withOffset(vecOffset, ValueType.Vector, bitWidth);
  }

  void _endVector(_StackPointer pointer) {
    final vecLength = _stack.length - pointer.stackPosition;
    final vec = _createVector(pointer.stackPosition, vecLength, 1);
    _stack.removeRange(pointer.stackPosition, _stack.length);
    _stack.add(vec);
  }

  void _sortKeysAndEndMap(_StackPointer pointer) {
    if (((_stack.length - pointer.stackPosition) & 1) == 1) {
      throw StateError(
        'The stack needs to hold key value pairs (even number of elements). Check if you combined [addKey] with add... method calls properly.',
      );
    }

    var sorted = true;
    for (var i = pointer.stackPosition; i < _stack.length - 2; i += 2) {
      if (_shouldFlip(_stack[i], _stack[i + 2])) {
        sorted = false;
        break;
      }
    }

    if (sorted == false) {
      for (var i = pointer.stackPosition; i < _stack.length; i += 2) {
        var flipIndex = i;
        for (var j = i + 2; j < _stack.length; j += 2) {
          if (_shouldFlip(_stack[flipIndex], _stack[j])) {
            flipIndex = j;
          }
        }
        if (flipIndex != i) {
          var k = _stack[flipIndex];
          var v = _stack[flipIndex + 1];
          _stack[flipIndex] = _stack[i];
          _stack[flipIndex + 1] = _stack[i + 1];
          _stack[i] = k;
          _stack[i + 1] = v;
        }
      }
    }
    _endMap(pointer);
  }

  void _endMap(_StackPointer pointer) {
    final vecLength = (_stack.length - pointer.stackPosition) >> 1;
    final offsets = <int>[];
    for (var i = pointer.stackPosition; i < _stack.length; i += 2) {
      offsets.add(_stack[i].offset!);
    }
    final keysHash = _KeysHash(offsets);
    _StackValue? keysStackValue;
    if (_keyVectorCache.containsKey(keysHash)) {
      keysStackValue = _keyVectorCache[keysHash];
    } else {
      keysStackValue = _createVector(pointer.stackPosition, vecLength, 2);
      _keyVectorCache[keysHash] = keysStackValue;
    }
    final vec = _createVector(
      pointer.stackPosition + 1,
      vecLength,
      2,
      keysStackValue,
    );
    _stack.removeRange(pointer.stackPosition, _stack.length);
    _stack.add(vec);
  }

  bool _shouldFlip(_StackValue v1, _StackValue v2) {
    if (v1.type != ValueType.Key || v2.type != ValueType.Key) {
      throw StateError(
        'Stack values are not keys $v1 | $v2. Check if you combined [addKey] with add... method calls properly.',
      );
    }

    late int c1, c2;
    var index = 0;
    do {
      c1 = _buffer.getUint8(v1.offset! + index);
      c2 = _buffer.getUint8(v2.offset! + index);
      if (c2 < c1) return true;
      if (c1 < c2) return false;
      index += 1;
    } while (c1 != 0 && c2 != 0);
    return false;
  }

  int _align(BitWidth width) {
    final byteWidth = BitWidthUtil.toByteWidth(width);
    _offset += BitWidthUtil.paddingSize(_offset, byteWidth);
    return byteWidth;
  }

  void _writeStackValue(_StackValue value, int byteWidth) {
    final newOffset = _newOffset(byteWidth);
    if (value.isOffset) {
      final relativeOffset = _offset - value.offset!;
      if (byteWidth == 8 || relativeOffset < (1 << (byteWidth * 8))) {
        _writeUInt(relativeOffset, byteWidth);
      } else {
        throw StateError(
          'Unexpected size $byteWidth. This might be a bug. Please create an issue https://github.com/google/flatbuffers/issues/new',
        );
      }
    } else {
      _pushBuffer(value.asU8List(BitWidthUtil.fromByteWidth(byteWidth)));
    }
    _offset = newOffset;
  }

  void _writeUInt(int value, int byteWidth) {
    final newOffset = _newOffset(byteWidth);
    _pushUInt(value, BitWidthUtil.fromByteWidth(byteWidth));
    _offset = newOffset;
  }

  int _newOffset(int newValueSize) {
    final newOffset = _offset + newValueSize;
    var size = _buffer.lengthInBytes;
    final prevSize = size;
    while (size < newOffset) {
      size <<= 1;
    }
    if (prevSize < size) {
      final newBuf = ByteData(size);
      newBuf.buffer.asUint8List().setAll(0, _buffer.buffer.asUint8List());
    }
    return newOffset;
  }

  void _pushInt(int value, BitWidth width) {
    switch (width) {
      case BitWidth.width8:
        _buffer.setInt8(_offset, value);
        break;
      case BitWidth.width16:
        _buffer.setInt16(_offset, value, Endian.little);
        break;
      case BitWidth.width32:
        _buffer.setInt32(_offset, value, Endian.little);
        break;
      case BitWidth.width64:
        _buffer.setInt64(_offset, value, Endian.little);
        break;
    }
  }

  void _pushUInt(int value, BitWidth width) {
    switch (width) {
      case BitWidth.width8:
        _buffer.setUint8(_offset, value);
        break;
      case BitWidth.width16:
        _buffer.setUint16(_offset, value, Endian.little);
        break;
      case BitWidth.width32:
        _buffer.setUint32(_offset, value, Endian.little);
        break;
      case BitWidth.width64:
        _buffer.setUint64(_offset, value, Endian.little);
        break;
    }
  }

  void _pushBuffer(List<int> value) {
    _buffer.buffer.asUint8List().setAll(_offset, value);
  }
}

class _StackValue {
  late Object _value;
  int? _offset;
  final ValueType _type;
  final BitWidth _width;

  _StackValue.withNull() : _type = ValueType.Null, _width = BitWidth.width8;

  _StackValue.withInt(int value)
    : _type = ValueType.Int,
      _width = BitWidthUtil.width(value),
      _value = value;

  _StackValue.withBool(bool value)
    : _type = ValueType.Bool,
      _width = BitWidth.width8,
      _value = value;

  _StackValue.withDouble(double value)
    : _type = ValueType.Float,
      _width = BitWidthUtil.width(value),
      _value = value;

  _StackValue.withOffset(int value, ValueType type, BitWidth width)
    : _offset = value,
      _type = type,
      _width = width;

  BitWidth storedWidth({BitWidth width = BitWidth.width8}) {
    return ValueTypeUtils.isInline(_type)
        ? BitWidthUtil.max(_width, width)
        : _width;
  }

  int storedPackedType({BitWidth width = BitWidth.width8}) {
    return ValueTypeUtils.packedType(_type, storedWidth(width: width));
  }

  BitWidth elementWidth(int size, int index) {
    if (ValueTypeUtils.isInline(_type)) return _width;
    final offset = _offset!;
    for (var i = 0; i < 4; i++) {
      final width = 1 << i;
      final bitWidth = BitWidthUtil.uwidth(
        size + BitWidthUtil.paddingSize(size, width) + index * width - offset,
      );
      if (1 << bitWidth.index == width) {
        return bitWidth;
      }
    }
    throw StateError(
      'Element is of unknown. Size: $size at index: $index. This might be a bug. Please create an issue https://github.com/google/flatbuffers/issues/new',
    );
  }

  List<int> asU8List(BitWidth width) {
    if (ValueTypeUtils.isNumber(_type)) {
      if (_type == ValueType.Float) {
        if (width == BitWidth.width32) {
          final result = ByteData(4);
          result.setFloat32(0, _value as double, Endian.little);
          return result.buffer.asUint8List();
        } else {
          final result = ByteData(8);
          result.setFloat64(0, _value as double, Endian.little);
          return result.buffer.asUint8List();
        }
      } else {
        switch (width) {
          case BitWidth.width8:
            final result = ByteData(1);
            result.setInt8(0, _value as int);
            return result.buffer.asUint8List();
          case BitWidth.width16:
            final result = ByteData(2);
            result.setInt16(0, _value as int, Endian.little);
            return result.buffer.asUint8List();
          case BitWidth.width32:
            final result = ByteData(4);
            result.setInt32(0, _value as int, Endian.little);
            return result.buffer.asUint8List();
          case BitWidth.width64:
            final result = ByteData(8);
            result.setInt64(0, _value as int, Endian.little);
            return result.buffer.asUint8List();
        }
      }
    }
    if (_type == ValueType.Null) {
      final result = ByteData(1);
      result.setInt8(0, 0);
      return result.buffer.asUint8List();
    }
    if (_type == ValueType.Bool) {
      final result = ByteData(1);
      result.setInt8(0, _value as bool ? 1 : 0);
      return result.buffer.asUint8List();
    }

    throw StateError(
      'Unexpected type: $_type. This might be a bug. Please create an issue https://github.com/google/flatbuffers/issues/new',
    );
  }

  ValueType get type {
    return _type;
  }

  BitWidth get width {
    return _width;
  }

  bool get isOffset {
    return !ValueTypeUtils.isInline(_type);
  }

  int? get offset => _offset;

  bool get isFloat32 {
    return _type == ValueType.Float && _width == BitWidth.width32;
  }
}

class _StackPointer {
  int stackPosition;
  bool isVector;

  _StackPointer(this.stackPosition, this.isVector);
}

class _KeysHash {
  final List<int> keys;

  const _KeysHash(this.keys);

  @override
  bool operator ==(Object other) {
    if (other is _KeysHash) {
      if (keys.length != other.keys.length) return false;
      for (var i = 0; i < keys.length; i++) {
        if (keys[i] != other.keys[i]) return false;
      }
      return true;
    }
    return false;
  }

  @override
  int get hashCode {
    var result = 17;
    for (var i = 0; i < keys.length; i++) {
      result = result * 23 + keys[i];
    }
    return result;
  }
}
