// See file LICENSE for more information.

library impl.ecc.ecc_fp;

import 'dart:typed_data';

import 'package:pointycastle/api.dart';
import 'package:pointycastle/ecc/ecc_base.dart'
    hide ECCurveBase, ECFieldElementBase, ECPointBase;
import 'package:pointycastle/ecc/ecc_base.dart' as ecc;
import 'package:pointycastle/src/utils.dart' as utils;

/// return index of lowest 1-bit in x, x < 2^31 */
int _lbit(BigInt x) {
  // Implementation borrowed from bignum.BigIntegerDartvm.
  if (x == BigInt.zero) return -1;
  var r = 0;
  while ((x & BigInt.from(0xffffffff)) == BigInt.zero) {
    x >>= 32;
    r += 32;
  }
  if ((x & BigInt.from(0xffff)) == BigInt.zero) {
    x >>= 16;
    r += 16;
  }
  if ((x & BigInt.from(0xff)) == BigInt.zero) {
    x >>= 8;
    r += 8;
  }
  if ((x & BigInt.from(0xf)) == BigInt.zero) {
    x >>= 4;
    r += 4;
  }
  if ((x & BigInt.from(3)) == BigInt.zero) {
    x >>= 2;
    r += 2;
  }
  if ((x & BigInt.one) == BigInt.zero) ++r;
  return r;
}

bool _testBit(BigInt i, int n) {
  return i & (BigInt.one << n) != BigInt.zero;
}

class ECFieldElement extends ecc.ECFieldElementBase {
  final BigInt? q;
  final BigInt? x;

  ECFieldElement(this.q, this.x) {
    if (x! >= q!) {
      throw ArgumentError('Value x must be smaller than q');
    }
  }

  @override
  String get fieldName => 'Fp';
  @override
  int get fieldSize => q!.bitLength;

  @override
  BigInt? toBigInteger() => x;

  @override
  ECFieldElement operator +(ECFieldElement b) =>
      ECFieldElement(q, (x! + b.toBigInteger()!) % q!);
  @override
  ECFieldElement operator -(ECFieldElement b) =>
      ECFieldElement(q, (x! - b.toBigInteger()!) % q!);
  @override
  ECFieldElement operator *(ECFieldElement b) =>
      ECFieldElement(q, (x! * b.toBigInteger()!) % q!);
  @override
  ECFieldElement operator /(ECFieldElement b) =>
      ECFieldElement(q, (x! * b.toBigInteger()!.modInverse(q!)) % q!);

  @override
  ECFieldElement operator -() => ECFieldElement(q, -x! % q!);

  @override
  ECFieldElement invert() => ECFieldElement(q, x!.modInverse(q!));
  @override
  ECFieldElement square() => ECFieldElement(q, x!.modPow(BigInt.two, q!));

  // D.1.4 91
  /// return a sqrt root - the routine verifies that the calculation
  /// returns the right value - if none exists it returns null.
  @override
  ECFieldElement? sqrt() {
    if (!_testBit(q!, 0)) {
      throw UnimplementedError('Not implemented yet');
    }

    // p % 4 == 3
    if (_testBit(q!, 1)) {
      // z = g^(u+1) + p, p = 4u + 3
      var z = ECFieldElement(q, x!.modPow((q! >> 2) + BigInt.one, q!));
      return z.square() == this ? z : null;
    }

    // p % 4 == 1
    var qMinusOne = q! - BigInt.one;

    var legendreExponent = qMinusOne >> 1;
    if (x!.modPow(legendreExponent, q!) != BigInt.one) {
      return null;
    }

    var u = qMinusOne >> 2;
    var k = (u << 1) + BigInt.one;

    var Q = x!;
    var fourQ = (Q >> 2) % q!;

    BigInt U, V;
    var rand = SecureRandom();
    do {
      BigInt? P;
      do {
        P = rand.nextBigInteger(q!.bitLength);
      } while ((P >= q!) ||
          (((P * P) - fourQ).modPow(legendreExponent, q!) != qMinusOne));

      var result = _lucasSequence(q!, P, Q, k);
      U = result[0];
      V = result[1];

      if (((V * V) % q!) == fourQ) {
        // Integer division by 2, mod q
        if (_testBit(V, 0)) {
          V = V + q!;
        }

        V = V >> 1;

        //assert V.multiply(V).mod(q).equals(x);

        return ECFieldElement(q, V);
      }
    } while ((U == BigInt.one) || (U == qMinusOne));

    return null;
  }

  List<BigInt> _lucasSequence(BigInt p, BigInt P, BigInt Q, BigInt k) {
    var n = k.bitLength;
    var s = _lbit(k);

    var uh = BigInt.one;
    var vl = BigInt.two;
    var vh = P;
    var ql = BigInt.one;
    var qh = BigInt.one;

    for (var j = n - 1; j >= (s + 1); j--) {
      ql = (ql * qh) % p;

      if (_testBit(k, j)) {
        qh = (ql * Q) % p;
        uh = (uh * vh) % p;
        vl = ((vh * vl) - (P * ql)) % p;
        vh = ((vh * vh) - (qh << 1)) % p;
      } else {
        qh = ql;
        uh = ((uh * vl) - ql) % p;
        vh = ((vh * vl) - (P * ql)) % p;
        vl = ((vl * vl) - (ql << 1)) % p;
      }
    }

    ql = (ql * qh) % p;
    qh = (ql * Q) % p;
    uh = ((uh * vl) - ql) % p;
    vl = ((vh * vl) - (P * ql)) % p;
    ql = (ql * qh) % p;

    for (var j = 1; j <= s; j++) {
      uh = (uh * vl) % p;
      vl = ((vl * vl) - (ql << 1)) % p;
      ql = (ql * ql) % p;
    }

    return [uh, vl];
  }

  @override
  bool operator ==(Object other) {
    if (other is ECFieldElement) {
      return (q == other.q) && (x == other.x);
    }
    return false;
  }

  @override
  int get hashCode => q.hashCode ^ x.hashCode;
}

/// Elliptic curve points over Fp
class ECPoint extends ecc.ECPointBase {
  /// Create a point that encodes with or without point compression.
  ///
  /// @param curve the curve to use
  /// @param x affine x co-ordinate
  /// @param y affine y co-ordinate
  /// @param withCompression if true encode with point compression
  ECPoint(ECCurve curve, ECFieldElement? x, ECFieldElement? y,
      [bool withCompression = false])
      : super(curve, x, y, withCompression, _wNafMultiplier) {
    if ((x != null && y == null) || (x == null && y != null)) {
      throw ArgumentError('Exactly one of the field elements is null');
    }
  }

  /// return the field element encoded with point compression. (S 4.3.6)
  @override
  Uint8List getEncoded([bool compressed = true]) {
    if (isInfinity) {
      return Uint8List.fromList([1]);
    }

    var qLength = x!.byteLength;
    if (compressed) {
      int pc;

      if (_testBit(y!.toBigInteger()!, 0)) {
        pc = 0x03;
      } else {
        pc = 0x02;
      }

      var X = _x9IntegerToBytes(x!.toBigInteger(), qLength);
      var po = Uint8List(X.length + 1);

      po[0] = pc.toInt();
      po.setAll(1, X);

      return po;
    } else {
      var X = _x9IntegerToBytes(x!.toBigInteger(), qLength);
      var Y = _x9IntegerToBytes(y!.toBigInteger(), qLength);
      var po = Uint8List(X.length + Y.length + 1);

      po[0] = 0x04;
      po.setAll(1, X);
      po.setAll(X.length + 1, Y);

      return po;
    }
  }

  // B.3 pg 62
  @override
  ECPoint? operator +(ECPoint? b) {
    if (isInfinity) {
      return b;
    }

    if (b!.isInfinity) {
      return this;
    }

    // Check if b = this or b = -this
    if (x == b.x) {
      if (y == b.y) {
        // this = b, i.e. this must be doubled
        return twice();
      }

      // this = -b, i.e. the result is the point at infinity
      return curve.infinity as ECPoint?;
    }

    var gamma = (b.y! - y!) / (b.x! - x!);

    var x3 = (gamma.square() - x!) - b.x!;
    var y3 = (gamma * (x! - x3)) - y!;

    return ECPoint(curve as ECCurve, x3 as ECFieldElement?,
        y3 as ECFieldElement?, isCompressed);
  }

  // B.3 pg 62
  @override
  ECPoint? twice() {
    if (isInfinity) {
      // Twice identity element (point at infinity) is identity
      return this;
    }

    if (y!.toBigInteger() == BigInt.zero) {
      // if y1 == 0, then (x1, y1) == (x1, -y1)
      // and hence this = -this and thus 2(x1, y1) == infinity
      return curve.infinity as ECPoint?;
    }

    var two = curve.fromBigInteger(BigInt.two);
    var three = curve.fromBigInteger(BigInt.from(3));
    var gamma = ((x!.square() * three) + curve.a!) / (y! * two);

    var x3 = gamma.square() - (x! * two);
    var y3 = (gamma * (x! - x3)) - y!;

    return ECPoint(curve as ECCurve, x3 as ECFieldElement?,
        y3 as ECFieldElement?, isCompressed);
  }

  // D.3.2 pg 102 (see Note:)
  @override
  ECPoint? operator -(ECPoint b) {
    if (b.isInfinity) {
      return this;
    }

    // Add -b
    return this + (-b);
  }

  @override
  ECPoint operator -() {
    return ECPoint(curve as ECCurve, x as ECFieldElement?,
        -y! as ECFieldElement?, isCompressed);
  }
}

/// Elliptic curve over Fp
class ECCurve extends ecc.ECCurveBase {
  final BigInt? q;
  ECPoint? _infinity;

  ECCurve(this.q, BigInt? a, BigInt? b) : super(a, b) {
    _infinity = ECPoint(this, null, null);
  }

  @override
  int get fieldSize => q!.bitLength;
  @override
  ECPoint? get infinity => _infinity;

  @override
  ECFieldElement fromBigInteger(BigInt? x) => ECFieldElement(q, x);
  @override
  ECPoint createPoint(BigInt x, BigInt y, [bool withCompression = false]) =>
      ECPoint(this, fromBigInteger(x), fromBigInteger(y), withCompression);

  @override
  ECPoint decompressPoint(int yTilde, BigInt x1) {
    var x = fromBigInteger(x1);
    var alpha = (x * ((x * x) + (a as ECFieldElement))) + (b as ECFieldElement);
    var beta = alpha.sqrt();

    //
    // if we can't find a sqrt we haven't got a point on the
    // curve - run!
    //
    if (beta == null) {
      throw ArgumentError('Invalid point compression');
    }

    var betaValue = beta.toBigInteger()!;
    var bit0 = _testBit(betaValue, 0) ? 1 : 0;

    if (bit0 != yTilde) {
      // Use the other root
      beta = fromBigInteger(q! - betaValue);
    }

    return ECPoint(this, x, beta, true);
  }

  @override
  bool operator ==(Object other) {
    if (other is ECCurve) {
      return q == other.q && a == other.a && b == other.b;
    }
    return false;
  }

  @override
  int get hashCode => a.hashCode ^ b.hashCode ^ q.hashCode;
}

/// Class holding precomputation data for the WNAF (Window Non-Adjacent Form)
/// algorithm.
class _WNafPreCompInfo implements PreCompInfo {
  /// Array holding the precomputed [ECPoint]s used for the Window NAF multiplication.
  List<ECPoint>? preComp;

  /// Holds an [ECPoint] representing twice(this). Used for the Window NAF multiplication.
  ECPoint? twiceP;
}

/// Function implementing the WNAF (Window Non-Adjacent Form) multiplication algorithm. Multiplies [p]] by an integer [k] using
/// the Window NAF method.
ecc.ECPointBase? _wNafMultiplier(
    ecc.ECPointBase p, BigInt? k, PreCompInfo? preCompInfo) {
  // Ignore empty PreCompInfo or PreCompInfo of incorrect type
  _WNafPreCompInfo wnafPreCompInfo;
  if (preCompInfo is! _WNafPreCompInfo) {
    wnafPreCompInfo = _WNafPreCompInfo();
  } else {
    wnafPreCompInfo = preCompInfo;
  }

  // floor(log2(k))
  var m = k!.bitLength;

  // width of the Window NAF
  int width;

  // Required length of precomputation array
  int reqPreCompLen;

  // Determine optimal width and corresponding length of precomputation
  // array based on literature values
  if (m < 13) {
    width = 2;
    reqPreCompLen = 1;
  } else {
    if (m < 41) {
      width = 3;
      reqPreCompLen = 2;
    } else {
      if (m < 121) {
        width = 4;
        reqPreCompLen = 4;
      } else {
        if (m < 337) {
          width = 5;
          reqPreCompLen = 8;
        } else {
          if (m < 897) {
            width = 6;
            reqPreCompLen = 16;
          } else {
            if (m < 2305) {
              width = 7;
              reqPreCompLen = 32;
            } else {
              width = 8;
              reqPreCompLen = 127;
            }
          }
        }
      }
    }
  }

  // The length of the precomputation array
  var preCompLen = 1;

  List<ECPoint?>? preComp = wnafPreCompInfo.preComp;
  var twiceP = wnafPreCompInfo.twiceP;

  // Check if the precomputed ECPoints already exist
  if (preComp == null) {
    // Precomputation must be performed from scratch, create an empty
    // precomputation array of desired length
    preComp = List<ECPoint>.filled(1, p as ECPoint);
  } else {
    // Take the already precomputed ECPoints to start with
    preCompLen = preComp.length;
  }

  twiceP ??= p.twice() as ECPoint?;

  if (preCompLen < reqPreCompLen) {
    // Precomputation array must be made bigger, copy existing preComp
    // array into the larger preComp array
    var oldPreComp = preComp as List<ECPoint>;
    preComp = List<ECPoint?>.filled(reqPreCompLen, null, growable: false);
    preComp.setAll(0, oldPreComp);

    for (var i = preCompLen; i < reqPreCompLen; i++) {
      // Compute the ECPoints for the precomputation array.
      // The values 1, 3, 5, ..., 2^(width-1)-1 times p are
      // computed
      preComp[i] = twiceP! + (preComp[i - 1]);
    }
  }

  // Compute the Window NAF of the desired width
  var wnaf = _windowNaf(width, k);
  var l = wnaf.length;

  // Apply the Window NAF to p using the precomputed ECPoint values.
  var q = p.curve.infinity;
  for (var i = l - 1; i >= 0; i--) {
    q = q!.twice();

    if (wnaf[i] != 0) {
      if (wnaf[i]! > 0) {
        q = q! + preComp[(wnaf[i]! - 1) ~/ 2];
      } else {
        // wnaf[i] < 0
        q = q! - preComp[(-wnaf[i]! - 1) ~/ 2]!;
      }
    }
  }

  // Set PreCompInfo in ECPoint, such that it is available for next
  // multiplication.
  wnafPreCompInfo.preComp = preComp.map((e) => e as ECPoint).toList();
  wnafPreCompInfo.twiceP = twiceP;
  p.preCompInfo = wnafPreCompInfo;
  return q;
}

/// Computes the Window NAF (non-adjacent Form) of an integer.
/// @param width The width <code>w</code> of the Window NAF. The width is
/// defined as the minimal number <code>w</code>, such that for any
/// <code>w</code> consecutive digits in the resulting representation, at
/// most one is non-zero.
/// @param k The integer of which the Window NAF is computed.
/// @return The Window NAF of the given width, such that the following holds:
/// <code>k = &sum;<sub>i=0</sub><sup>l-1</sup> k<sub>i</sub>2<sup>i</sup>
/// </code>, where the <code>k<sub>i</sub></code> denote the elements of the
/// returned <code>byte[]</code>.
List<int?> _windowNaf(int width, BigInt k) {
  // The window NAF is at most 1 element longer than the binary
  // representation of the integer k. byte can be used instead of short or
  // int unless the window width is larger than 8. For larger width use
  // short or int. However, a width of more than 8 is not efficient for
  // m = log2(q) smaller than 2305 Bits. Note: Values for m larger than
  // 1000 Bits are currently not used in practice.
  var wnaf = List<int?>.filled(k.bitLength + 1, null, growable: false);

  // 2^width as short and BigInt
  var pow2wB = 1 << width;
  var pow2wBI = BigInt.from(pow2wB);

  var i = 0;

  // The actual length of the WNAF
  var length = 0;

  // while k >= 1
  while (k.sign > 0) {
    // if k is odd
    if (_testBit(k, 0)) {
      // k mod 2^width
      var remainder = k % pow2wBI;

      // if remainder > 2^(width - 1) - 1
      if (_testBit(remainder, width - 1)) {
        wnaf[i] = remainder.toInt() - pow2wB;
      } else {
        wnaf[i] = remainder.toInt();
      }

      // convert to 'Java byte'
      wnaf[i] = wnaf[i]! % 0x100;
      if ((wnaf[i]! & 0x80) != 0) {
        wnaf[i] = wnaf[i]! - 256;
      }

      // wnaf[i] is now in [-2^(width-1), 2^(width-1)-1]

      k = k - BigInt.from(wnaf[i]!);
      length = i;
    } else {
      wnaf[i] = 0;
    }

    // k = k/2
    k = k >> 1;
    i++;
  }

  length++;

  // Reduce the WNAF array to its actual length
  var wnafShort = List<int?>.filled(length, null, growable: false);
  wnafShort.setAll(0, wnaf.sublist(0, length));
  return wnafShort;
}

Uint8List _x9IntegerToBytes(BigInt? s, int qLength) {
  var bytes = Uint8List.fromList(utils.encodeBigInt(s));

  if (qLength < bytes.length) {
    return bytes.sublist(bytes.length - qLength);
  } else if (qLength > bytes.length) {
    return Uint8List(qLength)..setAll(qLength - bytes.length, bytes);
  }

  return bytes;
}
