// Copyright (c) 2016, 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/dart/analysis/features.dart';
import 'package:analyzer/src/binary/binary_reader.dart';
import 'package:analyzer/src/binary/binary_writer.dart';
import 'package:analyzer/src/dart/analysis/experiments.dart';
import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:pub_semver/pub_semver.dart';

/// An instance of [ApiSignature] collects data in the form of primitive types
/// (strings, ints, bools, etc.) from a summary "builder" object, and uses them
/// to generate an MD5 signature of a the non-informative parts of the summary
/// (i.e. those parts representing the API of the code being summarized).
///
/// Note that the data passed to the MD5 signature algorithm is untyped.  So,
/// for instance, an API signature built from a sequence of `false` booleans is
/// likely to match an API signature built from a sequence of zeros.  The caller
/// should take this into account; e.g. if a data structure may be represented
/// either by a boolean or an int, the caller should encode a tag distinguishing
/// the two representations before encoding the data.
class ApiSignature {
  /// Version number of the code in this class.  Any time this class is changed
  /// in a way that affects the data collected in [_data], this version number
  /// should be incremented, so that a summary signed by a newer version of the
  /// signature algorithm won't accidentally have the same signature as a
  /// summary signed by an older version.
  static const int _version = 0;

  /// Data accumulated so far.
  ByteData _data = ByteData(4096);

  /// Offset into [_data] where the next byte should be written.
  int _offset = 0;

  /// Create an [ApiSignature] which is ready to accept data.
  ApiSignature() {
    addInt(_version);
  }

  /// For testing only: create an [ApiSignature] which doesn't include any
  /// version information.  This makes it easier to unit tests, since the data
  /// is stable even if [_version] is changed.
  ApiSignature.unversioned();

  /// Collect a boolean value.
  void addBool(bool b) {
    _makeRoom(1);
    _data.setUint8(_offset, b ? 1 : 0);
    _offset++;
  }

  /// Collect a sequence of arbitrary bytes.  Note that the length is not
  /// collected, so for example `addBytes([1, 2]);` will have the same effect as
  /// `addBytes([1]); addBytes([2]);`.
  void addBytes(List<int> bytes) {
    int length = bytes.length;
    _makeRoom(length);
    for (int i = 0; i < length; i++) {
      _data.setUint8(_offset + i, bytes[i]);
    }
    _offset += length;
  }

  /// Collect a double-precision floating point value.
  void addDouble(double d) {
    _makeRoom(8);
    _data.setFloat64(_offset, d, Endian.little);
    _offset += 8;
  }

  /// Collect a [FeatureSet].
  void addFeatureSet(FeatureSet featureSet) {
    var knownFeatures = ExperimentStatus.knownFeatures;
    addInt(knownFeatures.length);
    for (var feature in knownFeatures.values) {
      addBool(featureSet.isEnabled(feature));
    }
  }

  /// Collect a 32-bit unsigned integer value.
  void addInt(int i) {
    _makeRoom(4);
    _data.setUint32(_offset, i, Endian.little);
    _offset += 4;
  }

  /// Collect a language version.
  void addLanguageVersion(Version version) {
    addInt(version.major);
    addInt(version.minor);
  }

  void addList<T>(List<T> elements, void Function(T) addElement) {
    addInt(elements.length);
    for (var element in elements) {
      addElement(element);
    }
  }

  void addMapEntryList<K, V>(
    List<MapEntry<K, V>> entries,
    void Function(K, V) addEntry,
  ) {
    addInt(entries.length);
    for (var entry in entries) {
      addEntry(entry.key, entry.value);
    }
  }

  /// Collect a string.
  void addString(String s) {
    bool isAscii = true;
    for (int i = 0; i < s.length; i++) {
      if (s.codeUnitAt(i) >= 128) {
        isAscii = false;
        break;
      }
    }
    if (isAscii) {
      int length = s.length;
      _makeRoom(length + 4);
      _data.setUint32(_offset, length, Endian.little);
      _offset += 4;
      // Calling addBytes is slow because addBytes will have to be more general.
      for (int i = 0; i < length; i++) {
        _data.setUint8(_offset + i, s.codeUnitAt(i));
      }
      _offset += length;
    } else {
      var bytes = const Utf8Encoder().convert(s);
      addInt(bytes.length);
      addBytes(bytes);
    }
  }

  /// Collect a string list, with the length.
  void addStringList(List<String> values) {
    addInt(values.length);
    for (var value in values) {
      addString(value);
    }
  }

  /// Collect the given [Uint32List].
  void addUint32List(Uint32List data) {
    addBytes(data.buffer.asUint8List());
  }

  /// For testing only: retrieve the internal representation of the data that
  /// has been collected.
  Uint8List getBytes_forDebug() {
    return Uint8List.view(_data.buffer, 0, _offset);
  }

  /// Return the bytes of the MD5 hash of the data collected so far.
  Uint8List toByteList() {
    var data = _data.buffer.asUint8List(0, _offset);
    var bytes = md5.convert(data).bytes;
    return bytes is Uint8List ? bytes : Uint8List.fromList(bytes);
  }

  /// Returns the MD5 hash of the data collected so far.
  Hash toHash() {
    var bytes = toByteList();
    return Hash._(bytes);
  }

  /// Return a hex-encoded MD5 signature of the data collected so far.
  String toHex() {
    return hex.encode(toByteList());
  }

  /// Return the MD5 hash of the data collected so far as [Uint32List].
  Uint32List toUint32List() {
    var hash = toHash();
    return hash._bytes.buffer.asUint32List();
  }

  /// Ensure that [spaceNeeded] bytes can be added to [_data] at [_offset]
  /// (copying it to a larger object if necessary).
  void _makeRoom(int spaceNeeded) {
    int oldLength = _data.lengthInBytes;
    if (_offset + spaceNeeded > oldLength) {
      int newLength = 2 * (_offset + spaceNeeded);
      ByteData newData = ByteData(newLength);
      Uint8List.view(
        newData.buffer,
      ).setRange(0, oldLength, Uint8List.view(_data.buffer));
      _data = newData;
    }
  }
}

/// Hash value computed by [ApiSignature].
class Hash {
  static final Hash empty = Hash._(Uint8List(0));

  final Uint8List _bytes;
  late final int _hashCode = const ListEquality<int>().hash(_bytes);

  factory Hash.read(BinaryReader reader) {
    return Hash._(reader.readUint8List());
  }

  Hash._(this._bytes);

  @override
  int get hashCode => _hashCode;

  @override
  bool operator ==(Object other) {
    return other is Hash &&
        const ListEquality<int>().equals(_bytes, other._bytes);
  }

  @override
  String toString() {
    return hex.encode(_bytes);
  }

  void write(BinaryWriter writer) {
    writer.writeUint8List(_bytes);
  }
}
