// See file LICENSE for more information.

import 'dart:typed_data';

import 'package:pointycastle/src/ufixnum.dart';
import 'package:pointycastle/src/impl/base_digest.dart';

/// Base implementation of MD4 family style digest
abstract class MD4FamilyDigest extends BaseDigest {
  final _byteCount = Register64(0);

  final _wordBuffer = Uint8List(4);
  late int _wordBufferOffset;

  final Endian _endian;
  final int _packedStateSize;

  final List<int> state;

  final List<int> buffer;
  late int bufferOffset;

  MD4FamilyDigest(this._endian, int stateSize, int bufferSize,
      [int? packedStateSize])
      : _packedStateSize =
            (packedStateSize == null) ? stateSize : packedStateSize,
        state = List<int>.filled(stateSize, 0, growable: false),
        buffer = List<int>.filled(bufferSize, 0, growable: false) {
    reset();
  }

  /// Reset state of digest.
  void resetState();

  /// Process a whole block of data in extender digest.
  void processBlock();

  @override
  void reset() {
    _byteCount.set(0);

    _wordBufferOffset = 0;
    _wordBuffer.fillRange(0, _wordBuffer.length, 0);

    bufferOffset = 0;
    buffer.fillRange(0, buffer.length, 0);

    resetState();
  }

  @override
  void updateByte(int inp) {
    _wordBuffer[_wordBufferOffset++] = clip8(inp);
    _processWordIfBufferFull();
    _byteCount.sum(1);
  }

  @override
  void update(Uint8List inp, int inpOff, int len) {
    int nbytes;

    nbytes = _processUntilNextWord(inp, inpOff, len);
    inpOff += nbytes;
    len -= nbytes;

    nbytes = _processWholeWords(inp, inpOff, len);
    inpOff += nbytes;
    len -= nbytes;

    _processBytes(inp, inpOff, len);
  }

  @override
  int doFinal(Uint8List out, int outOff) {
    var bitLength = Register64(_byteCount)..shiftl(3);

    _processPadding();
    _processLength(bitLength);
    _doProcessBlock();

    _packState(out, outOff);

    reset();

    return digestSize;
  }

  /// Process a word (4 bytes) of data stored in [inp], starting at [inpOff].
  void _processWord(Uint8List inp, int inpOff) {
    buffer[bufferOffset++] = unpack32(inp, inpOff, _endian);

    if (bufferOffset == 16) {
      _doProcessBlock();
    }
  }

  /// Process a block of data and reset the [buffer].
  void _doProcessBlock() {
    processBlock();

    // reset the offset and clean out the word buffer.
    bufferOffset = 0;
    buffer.fillRange(0, 16, 0);
  }

  /// Process [len] bytes from [inp] starting at [inpOff]
  void _processBytes(Uint8List inp, int inpOff, int len) {
    while (len > 0) {
      updateByte(inp[inpOff]);

      inpOff++;
      len--;
    }
  }

  /// Process data word by word until no more words can be extracted from [inp] and return the number of bytes processed.
  int _processWholeWords(Uint8List inp, int inpOff, int len) {
    var processed = 0;
    while (len > _wordBuffer.length) {
      _processWord(inp, inpOff);

      inpOff += _wordBuffer.length;
      len -= _wordBuffer.length;
      _byteCount.sum(_wordBuffer.length);
      processed += 4;
    }
    return processed;
  }

  /// Process bytes from [inp] until the word buffer [_wordBuffer] is full and reset and return the number of bytes processed.
  int _processUntilNextWord(Uint8List inp, int inpOff, int len) {
    var processed = 0;

    while ((_wordBufferOffset != 0) && (len > 0)) {
      updateByte(inp[inpOff]);

      inpOff++;
      len--;
      processed++;
    }

    return processed;
  }

  // ignore: comment_references
  /// Process a word in [_xBuff] if it is already full and then reset it
  void _processWordIfBufferFull() {
    if (_wordBufferOffset == _wordBuffer.length) {
      _processWord(_wordBuffer, 0);
      _wordBufferOffset = 0;
    }
  }

  /// Add final padding to the digest
  void _processPadding() {
    updateByte(128);
    while (_wordBufferOffset != 0) {
      updateByte(0);
    }
  }

  // ignore: comment_references
  /// Called from [finish] so that extender can process the number of bits processed.
  void _processLength(Register64 bitLength) {
    if (bufferOffset > 14) {
      _doProcessBlock();
    }

    switch (_endian) {
      case Endian.little:
        buffer[14] = bitLength.lo32;
        buffer[15] = bitLength.hi32;
        break;

      case Endian.big:
        buffer[14] = bitLength.hi32;
        buffer[15] = bitLength.lo32;
        break;

      default:
        throw StateError('Invalid endianness: $_endian');
    }
  }

  void _packState(Uint8List out, int outOff) {
    for (var i = 0; i < _packedStateSize; i++) {
      pack32(state[i], out, outOff + i * 4, _endian);
    }
  }
}
