import BN from "bn.js";
import ValidationUtils from "../utils/validation.utils";
import BufferUtils from "../utils/buffer.utils";
import type { BNOptions } from "../common/interfaces";

export default class BNExtended extends BN {

  public static Zero = new BNExtended(0);
  public static One = new BNExtended(1);
  public static Minus1 = new BNExtended(-1);

  public static fromNumber(num: number): BNExtended {
    ValidationUtils.validateArgument(typeof num === 'number', "num");
    return new BNExtended(num);
  }

  public static fromBigInt(num: bigint): BNExtended {
    ValidationUtils.validateArgument(typeof num === 'bigint', "num");
    return new BNExtended(num.toString());
  }
  
  public static fromString(str: string, base?: number | "hex"): BNExtended {
    ValidationUtils.validateArgument(typeof str === 'string', "str");
    return new BNExtended(str, base);
  }
  
  public static fromBuffer(buf: Uint8Array, opts?: BNOptions): BNExtended {
    ValidationUtils.validateArgument(BufferUtils.isBuffer(buf), "buf");
    if (opts?.endian === 'little') {
      buf = BufferUtils.reverse(buf);
    }
    return new BNExtended(BufferUtils.bufferToHex(buf), 16);
  }

  /**
   * Create a BN from a "ScriptNum":
   * This is analogous to the constructor for CScriptNum in nexad. Many ops in
   * nexad's script interpreter use CScriptNum, which is not really a proper
   * bignum. Instead, an error is thrown if trying to input a number bigger than
   * 4 bytes. We copy that behavior here. A third argument, `size`, is provided to
   * extend the hard limit of 4 bytes, as some usages require more than 4 bytes.
   */
  public static fromScriptNumBuffer(buf: Uint8Array, fRequireMinimal?: boolean, size?: number): BNExtended {
    let nMaxNumSize = size || 4;
    ValidationUtils.validateArgument(buf.length <= nMaxNumSize, 'script number overflow');
    if (fRequireMinimal && buf.length > 0) {
      // Check that the number is encoded with the minimum possible
      // number of bytes.
      //
      // If the most-significant-byte - excluding the sign bit - is zero
      // then we're not minimal. Note how this test also rejects the
      // negative-zero encoding, 0x80.
      if ((buf[buf.length - 1] & 0x7f) === 0) {
        // One exception: if there's more than one byte and the most
        // significant bit of the second-most-significant-byte is set
        // it would conflict with the sign bit. An example of this case
        // is +-255, which encode to 0xff00 and 0xff80 respectively.
        // (big-endian).
        if (buf.length <= 1 || (buf[buf.length - 2] & 0x80) === 0) {
          throw new Error('non-minimally encoded script number');
        }
      }
    }
    return BNExtended.fromSM(buf, { endian: 'little' });
  }

  // Override arithmetic methods to ensure they return BNExtended instances
  public override add(b: BN): BNExtended {
    const result = super.add(b).toString();
    return new BNExtended(result);
  }

  public override sub(b: BN): BNExtended {
    const result = super.sub(b).toString();
    return new BNExtended(result);
  }

  public override mul(b: BN): BNExtended {
    const result = super.mul(b).toString();
    return new BNExtended(result);
  }

  public override mod(b: BN): BNExtended {
    const result = super.mod(b).toString();
    return new BNExtended(result);
  }

  public override umod(b: BN): BNExtended {
    const result = super.umod(b).toString();
    return new BNExtended(result);
  }

  public override invm(b: BN): BNExtended {
    const result = super.invm(b).toString();
    return new BNExtended(result);
  }

  public override neg(): BNExtended {
    const result = super.neg().toString();
    return new BNExtended(result);
  }

  public override toNumber(): number {
    return parseInt(this.toString(10), 10);
  }
  
  public toBigInt(): bigint {
    return BigInt(this.toString(10))
  }

  public toByteArray(opts?: BN.Endianness | BNOptions, length?: number): Uint8Array {
    if (typeof opts === 'string') {
      return Uint8Array.from(super.toArray(opts, length));
    }

    let hex = this.toString(16, 2);
    let buf = BufferUtils.hexToBuffer(hex);

    if (opts?.size) {
      let natlen = hex.length / 2;
      if (natlen > opts.size) {
        buf = BNExtended.trim(buf, natlen);
      } else if (natlen < opts.size) {
        buf = BNExtended.pad(buf, natlen, opts.size);
      }
    }
  
    if (opts?.endian === 'little') {
      buf = BufferUtils.reverse(buf);
    }
  
    return buf;
  }

  /**
   * The corollary to the above, with the notable exception that we do not throw
   * an error if the output is larger than four bytes. (Which can happen if
   * performing a numerical operation that results in an overflow to more than 4
   * bytes).
   */
  public toScriptNumBuffer(): Uint8Array {
    return this.toSM({  endian: 'little' });
  }

  public toScriptBigNumBuffer(): Uint8Array {
    return this.toSM({ endian: 'little', bignum: true });
  }

  public getSize(): number {
    const bin = this.toString(2).replace('-', '');
    const numBits = bin.length + 1;
    return numBits / 8;
  }
  
  public safeAdd(bigNumToAdd: BNExtended, maxSize: number): BNExtended {
    const sum = this.add(bigNumToAdd);
    this.checkOperationForOverflow(bigNumToAdd, sum, maxSize);
    return sum;
  }
  
  public safeSub(bigNumToSubtract: BNExtended, maxSize: number): BNExtended {
    const difference = this.sub(bigNumToSubtract);
    this.checkOperationForOverflow(bigNumToSubtract, difference, maxSize);
    return difference;
  }
  
  public safeMul(bigNumToMultiply: BNExtended, maxSize: number): BNExtended {
    const product = this.mul(bigNumToMultiply);
    this.checkOperationForOverflow(bigNumToMultiply, product, maxSize);
    return product;
  }

  private checkOperationForOverflow(operand: BNExtended, result: BNExtended, maxSize: number): void {
    if (this.getSize() > maxSize || operand.getSize() > maxSize || result.getSize() > 8) {
      throw new Error('overflow');
    }
  }
  
  private toSMBigEndian(): Uint8Array {
    let buf;
    if (this.cmp(BNExtended.Zero) === -1) {
      buf = this.neg().toByteArray();
      if (buf[0] & 0x80) {
        buf = BufferUtils.concat([Uint8Array.from([0x80]), buf]);
      } else {
        buf[0] = buf[0] | 0x80;
      }
    } else {
      buf = this.toByteArray();
      if (buf[0] & 0x80) {
        buf = BufferUtils.concat([Uint8Array.from([0x00]), buf]);
      }
    }
  
    if (buf.length === 1 && buf[0] === 0) {
      buf = Uint8Array.from([]);
    }
    return buf;
  }
  
  private toBigNumSMBigEndian(): Uint8Array {
    let buf;
    if (this.cmp(BNExtended.Zero) === -1) {
      buf = this.neg().toByteArray();
      buf = BufferUtils.concat([Uint8Array.from([0x80]), buf]);
    } else {
      buf = this.toByteArray();
      buf = BufferUtils.concat([Uint8Array.from([0x00]), buf]);
    }

    return buf;
  }
  
  private toSM(opts: BNOptions): Uint8Array {
    let buf = opts?.bignum ? this.toBigNumSMBigEndian() : this.toSMBigEndian();
  
    if (opts?.endian === 'little') {
      buf = BufferUtils.reverse(buf);
    }

    return buf;
  }

  /**
 * Instantiate a BigNumber from a "signed magnitude buffer"
 * (a buffer where the most significant bit represents the sign (0 = positive, -1 = negative))
 */
  private static fromSM(buf: Uint8Array, opts?: BNOptions): BNExtended {
    if (buf.length === 0) {
      return this.fromBuffer(Uint8Array.from([0]));
    }

    if (opts?.endian === 'little') {
      buf = BufferUtils.reverse(buf);
    }

    let ret;
    if (buf[0] & 0x80) {
      buf[0] = buf[0] & 0x7f;
      ret = this.fromBuffer(buf);
      ret.neg().copy(ret);
    } else {
      ret = this.fromBuffer(buf);
    }
    return ret;
  }

  private static trim(buf: Uint8Array, natlen: number): Uint8Array {
    return buf.subarray(natlen - buf.length, natlen);
  }
  
  private static pad(buf: Uint8Array, natlen: number, size: number): Uint8Array {
    let rbuf = new Uint8Array(size);
    for (let i = 0; i < buf.length; i++) {
      rbuf[rbuf.length - 1 - i] = buf[buf.length - 1 - i];
    }
    for (let i = 0; i < size - natlen; i++) {
      rbuf[i] = 0;
    }
    return rbuf;
  }
}