// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

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

import 'package:analyzer/src/binary/binary_reader.dart';
import 'package:analyzer/src/binary/string_table.dart';
import 'package:analyzer/src/fine/manifest_id.dart';

/// Buffered writer for binary formats.
class BinaryWriter {
  static const int _SIZE = 5 * 1024;
  static const int _SAFE_LENGTH = _SIZE - 5;

  final BytesBuilder _builder = BytesBuilder(copy: false);

  Uint8List _buffer = Uint8List(_SIZE);
  int _length = 0;

  final Int64List _int64Buffer = Int64List(1);
  late final Uint8List _int64BufferUint8 = _int64Buffer.buffer.asUint8List();

  final Float64List _doubleBuffer = Float64List(1);
  late final Uint8List _doubleBufferUint8 = _doubleBuffer.buffer.asUint8List();

  final ManifestIdTableBuilder _manifestIdTableBuilder;
  final StringTableBuilder _stringTableBuilder;

  BinaryWriter({
    StringTableBuilder? stringTableBuilder,
    ManifestIdTableBuilder? manifestIdTableBuilder,
  }) : _stringTableBuilder = stringTableBuilder ?? StringTableBuilder(),
       _manifestIdTableBuilder =
           manifestIdTableBuilder ?? ManifestIdTableBuilder();

  int get offset => _builder.length + _length;

  BinaryWriter clone() {
    return BinaryWriter(
      stringTableBuilder: _stringTableBuilder,
      manifestIdTableBuilder: _manifestIdTableBuilder,
    );
  }

  Uint8List takeBytes() {
    _builder.add(_buffer.sublist(0, _length));
    return _builder.takeBytes();
  }

  @pragma("vm:prefer-inline")
  void writeBool(bool value) {
    writeByte(value ? 1 : 0);
  }

  @pragma("vm:prefer-inline")
  void writeByte(int byte) {
    assert((byte & 0xFF) == byte);
    _addByte(byte);
  }

  void writeBytes(Uint8List bytes) {
    if (bytes.isEmpty) {
      return;
    }

    // Usually the bytes is short, and fits the current buffer.
    if (_length + bytes.length < _SIZE) {
      _buffer.setRange(_length, _length + bytes.length, bytes);
      _length += bytes.length;
      return;
    }

    // If the bytes is too long, add separate buffers.
    if (bytes.length >= _SIZE) {
      _builder.add(_buffer.sublist(0, _length));
      _builder.add(bytes);
      // Start a new buffer.
      _buffer = Uint8List(_SIZE);
      _length = 0;
      return;
    }

    // Copy as much as we can into the current buffer.
    _buffer.setRange(_length, _SIZE, bytes);
    _builder.add(_buffer);

    // Copy the remainder into a new buffer.
    var alreadyCopied = _SIZE - _length;
    var remainder = bytes.length - alreadyCopied;
    _buffer = Uint8List(_SIZE);
    _buffer.setRange(0, remainder, bytes, alreadyCopied);
    _length = remainder;
  }

  void writeDouble(double value) {
    _doubleBuffer[0] = value;
    _addByte4(
      _doubleBufferUint8[0],
      _doubleBufferUint8[1],
      _doubleBufferUint8[2],
      _doubleBufferUint8[3],
    );
    _addByte4(
      _doubleBufferUint8[4],
      _doubleBufferUint8[5],
      _doubleBufferUint8[6],
      _doubleBufferUint8[7],
    );
  }

  void writeEnum(Enum e) {
    writeByte(e.index);
  }

  void writeIf<T extends Object>(bool condition, void Function() ifTrue) {
    if (condition) {
      writeBool(true);
      ifTrue();
    } else {
      writeBool(false);
    }
  }

  void writeInt64(int value) {
    _int64Buffer[0] = value;
    _addByte4(
      _int64BufferUint8[0],
      _int64BufferUint8[1],
      _int64BufferUint8[2],
      _int64BufferUint8[3],
    );
    _addByte4(
      _int64BufferUint8[4],
      _int64BufferUint8[5],
      _int64BufferUint8[6],
      _int64BufferUint8[7],
    );
  }

  /// Writes [items], converts to [List] first.
  void writeIterable<T>(Iterable<T> items, void Function(T x) writeItem) {
    writeList(items.toList(), writeItem);
  }

  void writeList<T>(List<T> items, void Function(T x) writeItem) {
    writeUint30(items.length);
    for (var i = 0; i < items.length; i++) {
      writeItem(items[i]);
    }
  }

  void writeManifestItemId(ManifestItemId id) {
    var index = _manifestIdTableBuilder[id];
    writeUint30(index);
  }

  void writeMap<K, V>(
    Map<K, V> map, {
    required void Function(K key) writeKey,
    required void Function(V value) writeValue,
  }) {
    writeUint30(map.length);
    for (var entry in map.entries) {
      writeKey(entry.key);
      writeValue(entry.value);
    }
  }

  void writeOptionalInt64(int? value) {
    if (value != null) {
      writeBool(true);
      writeInt64(value);
    } else {
      writeBool(false);
    }
  }

  void writeOptionalObject<T>(T? object, void Function(T x) write) {
    if (object != null) {
      writeBool(true);
      write(object);
    } else {
      writeBool(false);
    }
  }

  void writeOptionalStringReference(String? value) {
    if (value != null) {
      writeBool(true);
      writeStringReference(value);
    } else {
      writeBool(false);
    }
  }

  void writeOptionalStringUtf8(String? value) {
    if (value != null) {
      writeBool(true);
      writeStringUtf8(value);
    } else {
      writeBool(false);
    }
  }

  void writeOptionalUint30(int? value) {
    if (value != null) {
      writeBool(true);
      writeUint30(value);
    } else {
      writeBool(false);
    }
  }

  void writeOptionalUint8List(Uint8List? value) {
    if (value != null) {
      writeBool(true);
      writeUint8List(value);
    } else {
      writeBool(false);
    }
  }

  void writeOptionalUriList(List<Uri>? value) {
    if (value != null) {
      writeBool(true);
      writeUriList(value);
    } else {
      writeBool(false);
    }
  }

  void writeStringList(List<String> values) {
    writeList(values, writeStringReference);
  }

  void writeStringReference(String value) {
    var index = _stringTableBuilder[value];
    writeUint30(index);
  }

  /// Write the [value] as UTF8 encoded byte array.
  void writeStringUtf8(String value) {
    var bytes = const Utf8Encoder().convert(value);
    writeUint8List(bytes);
  }

  void writeStringUtf8Iterable(Iterable<String> items) {
    writeUint30(items.length);
    for (var item in items) {
      writeStringUtf8(item);
    }
  }

  /// Writes the table trailer, recording the start offsets of the manifest-ID
  /// table and the string table as two `uint32` values.
  ///
  /// Layout (BOF -> EOF):
  ///
  /// ```text
  /// <payload>
  /// <manifest_id_table>
  /// <string_table>
  /// <manifestIdTableOffset:u32>
  /// <stringTableOffset:u32>
  /// ```
  ///
  /// Call this after writing all other data, immediately before [takeBytes].
  ///
  /// This is the counterpart to [BinaryReader.initFromTableTrailer].
  void writeTableTrailer() {
    var manifestIdTableOffset = _manifestIdTableBuilder.write(this);
    var stringTableOffset = _stringTableBuilder.write(this);
    writeUint32(manifestIdTableOffset);
    writeUint32(stringTableOffset);
  }

  @pragma("vm:prefer-inline")
  void writeUint30(int value) {
    assert(value >= 0 && value >> 30 == 0);
    if (value < 0x80) {
      _addByte(value);
    } else if (value < 0x4000) {
      _addByte2((value >> 8) | 0x80, value & 0xFF);
    } else {
      _addByte4(
        (value >> 24) | 0xC0,
        (value >> 16) & 0xFF,
        (value >> 8) & 0xFF,
        value & 0xFF,
      );
    }
  }

  void writeUint30List(List<int> values) {
    var length = values.length;
    writeUint30(length);
    for (var i = 0; i < length; i++) {
      writeUint30(values[i]);
    }
  }

  void writeUint32(int value) {
    _addByte4(
      (value >> 24) & 0xFF,
      (value >> 16) & 0xFF,
      (value >> 8) & 0xFF,
      value & 0xFF,
    );
  }

  void writeUint8List(Uint8List bytes) {
    writeUint30(bytes.length);
    writeBytes(bytes);
  }

  void writeUri(Uri uri) {
    var uriStr = uri.toString();
    writeStringReference(uriStr);
  }

  void writeUriList(List<Uri> uriList) {
    writeList(uriList, writeUri);
  }

  @pragma("vm:prefer-inline")
  void _addByte(int byte) {
    _buffer[_length++] = byte;
    if (_length == _SIZE) {
      _builder.add(_buffer);
      _buffer = Uint8List(_SIZE);
      _length = 0;
    }
  }

  @pragma("vm:prefer-inline")
  void _addByte2(int byte1, int byte2) {
    if (_length < _SAFE_LENGTH) {
      _buffer[_length++] = byte1;
      _buffer[_length++] = byte2;
    } else {
      _addByte(byte1);
      _addByte(byte2);
    }
  }

  @pragma("vm:prefer-inline")
  void _addByte4(int byte1, int byte2, int byte3, int byte4) {
    if (_length < _SAFE_LENGTH) {
      _buffer[_length++] = byte1;
      _buffer[_length++] = byte2;
      _buffer[_length++] = byte3;
      _buffer[_length++] = byte4;
    } else {
      _addByte(byte1);
      _addByte(byte2);
      _addByte(byte3);
      _addByte(byte4);
    }
  }
}
