// See file LICENSE for more information.

library impl.asymmetric_block_cipher.rsa;

import 'dart:typed_data';

import 'package:pointycastle/api.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:pointycastle/src/impl/base_asymmetric_block_cipher.dart';
import 'package:pointycastle/src/registry/registry.dart';
import 'package:pointycastle/src/utils.dart' as utils;

class RSAEngine extends BaseAsymmetricBlockCipher {
  static final FactoryConfig factoryConfig =
      StaticFactoryConfig(AsymmetricBlockCipher, 'RSA', () => RSAEngine());

  late bool _forEncryption;
  RSAAsymmetricKey? _key;
  late BigInt _dP;
  late BigInt _dQ;
  late BigInt _qInv;

  @override
  String get algorithmName => 'RSA';

  @override
  int get inputBlockSize {
    if (_key == null) {
      throw StateError(
          'Input block size cannot be calculated until init() called');
    }

    var bitSize = _key!.modulus!.bitLength;
    if (_forEncryption) {
      return ((bitSize + 7) ~/ 8) - 1;
    } else {
      return (bitSize + 7) ~/ 8;
    }
  }

  @override
  int get outputBlockSize {
    if (_key == null) {
      throw StateError(
          'Output block size cannot be calculated until init() called');
    }

    var bitSize = _key!.modulus!.bitLength;
    if (_forEncryption) {
      return (bitSize + 7) ~/ 8;
    } else {
      return ((bitSize + 7) ~/ 8) - 1;
    }
  }

  @override
  void reset() {}

  @override
  void init(bool forEncryption,
      covariant AsymmetricKeyParameter<RSAAsymmetricKey> params) {
    _forEncryption = forEncryption;
    _key = params.key;

    if (_key is RSAPrivateKey) {
      var privKey = _key as RSAPrivateKey;
      var pSub1 = privKey.p! - BigInt.one;
      var qSub1 = privKey.q! - BigInt.one;
      _dP = privKey.privateExponent!.remainder(pSub1);
      _dQ = privKey.privateExponent!.remainder(qSub1);
      _qInv = privKey.q!.modInverse(privKey.p!);
    }
  }

  @override
  Uint8List process(Uint8List data) {
    // Expand the output block size by an extra byte to handle cases where
    // the output is larger than expected.
    var out = Uint8List(outputBlockSize + 1);
    var len = processBlock(data, 0, data.length, out, 0);
    return out.sublist(0, len);
  }

  @override
  int processBlock(
      Uint8List inp, int inpOff, int len, Uint8List out, int outOff) {
    var input = _convertInput(inp, inpOff, len);
    var output = _processBigInteger(input);
    return _convertOutput(output, out, outOff);
  }

  BigInt _convertInput(Uint8List inp, int inpOff, int len) {
    var inpLen = inp.length;

    if (inpLen < inpOff + len) {
      throw ArgumentError.value(inpOff, 'inpOff',
          'Not enough data for RSA cipher (length=$len, available=$inpLen)');
    }

    if (inputBlockSize + 1 < len) {
      throw ArgumentError.value(len, 'len',
          'Too large for maximum RSA cipher input block size ($inputBlockSize)');
    }

    var res = utils.decodeBigIntWithSign(1, inp.sublist(inpOff, inpOff + len));
    if (res >= _key!.modulus!) {
      throw ArgumentError('Input block too large for RSA key size');
    }

    return res;
  }

  int _convertOutput(BigInt result, Uint8List out, int outOff) {
    final output = utils.encodeBigInt(result);

    if (_forEncryption) {
      if ((output[0] == 0) && (output.length > outputBlockSize)) {
        // have ended up with an extra zero byte, copy down.
        var len = output.length - 1;
        out.setRange(outOff, outOff + len, output.sublist(1));
        return len;
      }
      if (output.length < outputBlockSize) {
        // have ended up with less bytes than normal, lengthen
        var len = outputBlockSize;
        out.setRange(outOff + len - output.length, outOff + len, output);
        return len;
      }
    } else {
      if (output[0] == 0) {
        // have ended up with an extra zero byte, copy down.
        var len = output.length - 1;
        out.setRange(outOff, outOff + len, output.sublist(1));
        return len;
      }
    }

    out.setAll(outOff, output);
    return output.length;
  }

  BigInt _processBigInteger(BigInt input) {
    if (_key is RSAPrivateKey) {
      var privKey = _key as RSAPrivateKey;
      BigInt mP, mQ, h, m;

      mP = input.remainder(privKey.p!).modPow(_dP, privKey.p!);

      mQ = input.remainder(privKey.q!).modPow(_dQ, privKey.q!);

      h = mP - mQ;
      h = h * _qInv;
      h = h % privKey.p!;

      m = h * privKey.q!;
      m = m + mQ;

      return m;
    } else {
      return input.modPow(_key!.exponent!, _key!.modulus!);
    }
  }
}
