// 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:convert/convert.dart';
import 'package:crypto/crypto.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 = new 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, Endianness.LITTLE_ENDIAN);
    _offset += 8;
  }

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

  /**
   * Collect a string.
   */
  void addString(String s) {
    List<int> bytes = UTF8.encode(s);
    addInt(bytes.length);
    addBytes(bytes);
  }

  /**
   * 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.
   */
  List<int> getBytes_forDebug() {
    return new Uint8List.view(_data.buffer, 0, _offset).toList();
  }

  /**
   * Return the bytes of the MD5 hash of the data collected so far.
   */
  List<int> toByteList() {
    return md5.convert(new Uint8List.view(_data.buffer, 0, _offset)).bytes;
  }

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

  /**
   * 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 = new ByteData(newLength);
      new Uint8List.view(newData.buffer)
          .setRange(0, oldLength, new Uint8List.view(_data.buffer));
      _data = newData;
    }
  }
}
