import type { IPrivateKey, IPublicKey, ISignature } from "../common/interfaces";
import type { EndianType } from "../common/types";
import BufferUtils from "../utils/buffer.utils";
import ValidationUtils from "../utils/validation.utils";
import BN from "./bn.extension";
import DigitalSignature from "./digital-signature";
import Point from "./point";
import Hash from "./hash";

export default class Schnorr extends DigitalSignature {

  public override sigError(): boolean | string {
    if (!BufferUtils.isBuffer(this.hashbuf) || this.hashbuf.length !== 32) {
      return 'hashbuf must be a 32 byte buffer';
    }

    let sigLength = Schnorr.getProperSizeBuffer(this.sig!.r).length + Schnorr.getProperSizeBuffer(this.sig!.s).length;
    
    if(!(sigLength === 64 || sigLength === 65)) {
      return 'signature must be a 64 byte or 65 byte array';
    } 

    let hashbuf = this.endian === 'little' ? BufferUtils.reverse(this.hashbuf) : this.hashbuf
    
    let P = this.pubkey.point;
    let G = Point.getG();

    if(P.ecPoint.isInfinity()) return true; /* c8 ignore next */
    
    let r = this.sig!.r;
    let s = this.sig!.s;

    let p = new BN('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 'hex');
    let n = Point.getN();

    if(r.gte(p) || s.gte(n)) {
      // ("Failed >= condition") 
      return true;
    }
    
    let Br = Schnorr.getProperSizeBuffer(this.sig!.r);
    let Bp = Point.pointToCompressed(P);
    
    let hash = Hash.sha256(BufferUtils.concat([Br, Bp, hashbuf]));
    let e = BN.fromBuffer(hash, { endian: 'big' }).umod(n);
    
    let sG = G.mul(s);
    let eP = P.mul(n.sub(e));
    let R = sG.add(eP);
    
    if(R.ecPoint.isInfinity() || !R.hasSquare() || !R.getX().eq(r)) {
      return true;
    } 
    return false;
  }

  /**
   * RFC6979 deterministic nonce generation used from https://reviews.bitcoinabc.org/D2501
   * 
   * @param privkeybuf 
   * @param msgbuf 
   * @return BN nonce
   */
  public nonceFunctionRFC6979(privkeybuf: Uint8Array, msgbuf: Uint8Array): BN {
    let V: Uint8Array = BufferUtils.hexToBuffer("0101010101010101010101010101010101010101010101010101010101010101");
    let K: Uint8Array = BufferUtils.hexToBuffer("0000000000000000000000000000000000000000000000000000000000000000");

    let blob = BufferUtils.concat([privkeybuf, msgbuf, new Uint8Array(0), Uint8Array.from("Schnorr+SHA256  ", c => c.charCodeAt(0))]);

    K = Hash.sha256hmac(BufferUtils.concat([V, BufferUtils.hexToBuffer('00'), blob]), K);
    V = Hash.sha256hmac(V,K); 

    K = Hash.sha256hmac(BufferUtils.concat([V, BufferUtils.hexToBuffer('01'), blob]), K);
    V = Hash.sha256hmac(V,K);

    let k = new BN(0);
    let T;
    while (true) {
      V = Hash.sha256hmac(V,K);
      T = BN.fromBuffer(V);
      k = T;

      ValidationUtils.validateState(V.length >= 32, "V length should be >= 32");

      if (k.gt(new BN(0)) && k.lt(Point.getN())) {
        break;
      }/* c8 ignore start */

      K = Hash.sha256hmac(BufferUtils.concat([V, BufferUtils.hexToBuffer('00')]), K);
      V = Hash.sha256hmac(V, K);
    }/* c8 ignore stop */

    return k;
  }

  /**
   * @remarks
   * Important references for schnorr implementation {@link https://spec.nexa.org/forks/2019-05-15-schnorr/}
   * 
   * @param d the private key
   * @param e the message to be signed
   */
  protected override _findSignature(d: BN, e: BN): Partial<ISignature> {
    let n = Point.getN();
    let G = Point.getG();

    ValidationUtils.validateState(!d.lte(new BN(0)), 'privkey out of field of curve');
    ValidationUtils.validateState(!d.gte(n), 'privkey out of field of curve');

    
    let k = this.nonceFunctionRFC6979(d.toByteArray({ size: 32 }), e.toByteArray({ size: 32 }));

    let P = G.mul(d);
    let R = G.mul(k);

    // Find deterministic k
    if(R.hasSquare()) {
      k = k;
    } else {
      k = n.sub(k);
    }
    
    let r = R.getX();
    let e0 = BN.fromBuffer(Hash.sha256(BufferUtils.concat([Schnorr.getProperSizeBuffer(r), Point.pointToCompressed(P), e.toByteArray({ size: 32 })])));
    
    let s = ((e0.mul(d)).add(k)).mod(n);

    return { r: r, s: s };
  }

  /**
   * Function written to ensure s or r parts of signature is at least 32 bytes, when converting 
   * from a BN to type Uint8Array.
   * The BN type naturally cuts off leading zeros, e.g.
   * <BN: 4f92d8094f710bc11b93935ac157730dda26c5c2a856650dbd8ebcd730d2d4> 31 bytes
   * Buffer <00 4f 92 d8 09 4f 71 0b c1 1b 93 93 5a c1 57 73 0d da 26 c5 c2 a8 56 65 0d bd 8e bc d7 30 d2 d4> 32 bytes
   * Both types are equal, however Schnorr signatures must be a minimum of 64 bytes.
   * In a previous implementation of this schnorr module, was resulting in 63 byte signatures. 
   * (Although it would have been verified, it's proper to ensure the min requirement)
   * 
   * @param bn the r or s signature part
   */
  private static getProperSizeBuffer(bn: BN): Uint8Array {
    if (bn.toByteArray().length < 32) {
      return bn.toByteArray({size: 32});
    }
    return bn.toByteArray();
  }

  public static sign(hashbuf: Uint8Array, privkey: IPrivateKey, endian: EndianType): ISignature {
    return new Schnorr({
      hashbuf: hashbuf,
      endian: endian,
      privkey: privkey
    }).sign().sig!;
  }
  
  public static verify(hashbuf: Uint8Array, sig: ISignature, pubkey: IPublicKey, endian: EndianType): boolean {
    return new Schnorr({
      hashbuf: hashbuf,
      endian: endian,
      sig: sig,
      pubkey: pubkey
    }).verify().verified!;
  }
}