import Schnorr from "../../crypto/schnorr";
import type Signature from "../../crypto/signature";
import type PrivateKey from "../../keys/privatekey";
import ValidationUtils from "../../utils/validation.utils";
import type Script from "../script/script";
import type SighashType from "./sighashtype";
import { InputSighashType, OutputSighashType } from "./sighashtype";
import type Transaction from "./transaction";
import type PublicKey from "../../keys/publickey";
import BufferWriter from "../../encoding/bufferwriter";
import Hash from "../../crypto/hash";
import BN from "../../crypto/bn.extension";
import TxSignature from "./txsignature";

interface SighashComponents {
  hashPrevouts: Uint8Array;
  hashSequence: Uint8Array;
  hashInputAmounts: Uint8Array;
  hashOutputs: Uint8Array;
}

export default class TxSigner {

  /**
   * Build a transaction signature object (`TxSignature`) for a given input.
   *
   * This method extracts the subscript for the specified input, signs the
   * transaction using the provided private key and sighash type, and returns
   * a `TxSignature` containing all the components needed for inclusion in a scriptSig.
   *
   * @param transaction The transaction to sign.
   * @param inputNumber The zero-based index of the input within the transaction.
   * @param sighashType The sighash type that determines which parts of the transaction are signed.
   * @param privateKey The private key used to generate the signature.
   * 
   * @returns A `TxSignature` object.
   */
  public static buildTxSignature(transaction: Transaction, inputNumber: number, sighashType: SighashType, privateKey: PrivateKey): TxSignature {
    let subscript = transaction.inputs[inputNumber].getSubscript();
    return new TxSignature({
      inputIndex: inputNumber,
      publicKey: privateKey.publicKey,
      subscript: subscript,
      signature: this.sign(transaction, inputNumber, sighashType, subscript, privateKey),
      sigType: sighashType
    });
  }

  /**
   * Create a signature
   * 
   * @param transaction the transaction to sign
   * @param inputNumber the input index for the signature
   * @param sighashType the sighash type
   * @param subscript the script that will be signed
   * @param privateKey the privkey to sign with
   * @returns The signature
   */
  public static sign(transaction: Transaction, inputNumber: number, sighashType: SighashType, subscript: Script, privateKey: PrivateKey): Signature {
    let hashbuf = this.buildSighash(transaction, inputNumber, sighashType, subscript);
    return Schnorr.sign(hashbuf, privateKey, 'little') as Signature;
  }

  /**
   * Verify a signature
   * 
   * @param transaction the transaction to verify
   * @param inputNumber the input index for the signature
   * @param signature the signature to verify
   * @param sighashType the sighash type
   * @param subscript the script that will be verified
   * @param publicKey the pubkey that correspond to the signing privkey
   * @returns true if signature is valid
   */
  public static verify(transaction: Transaction, inputNumber: number, signature: Signature, sighashType: SighashType, subscript: Script, publicKey: PublicKey): boolean {
    ValidationUtils.validateArgument(transaction != null, 'transaction');
    ValidationUtils.validateArgument(signature != null, 'signature');
    ValidationUtils.validateArgument(sighashType != null, 'sighashType');

    let hashbuf = this.buildSighash(transaction, inputNumber, sighashType, subscript);
    return Schnorr.verify(hashbuf, signature, publicKey, 'little');
  }

  /**
   * Returns a buffer of length 32 bytes with the hash that needs to be signed for OP_CHECKSIG(VERIFY).
   *
   * @param transaction the transaction to sign
   * @param inputNumber the input index for the signature
   * @param sighashType the sighash type
   * @param subscript the script that will be signed
   */
  public static buildSighash(transaction: Transaction, inputNumber: number, sighashType: SighashType, subscript: Script): Uint8Array {
    let components = this._getSighashComponents(transaction, inputNumber, sighashType);
    let writer = new BufferWriter();

    // Version
    writer.writeUInt8(transaction.version);

    // Input prevouts/nSequence (none/all, depending on flags)
    writer.write(components.hashPrevouts);
    writer.write(components.hashInputAmounts);
    writer.write(components.hashSequence);

    // scriptCode of the input (serialized as scripts inside CTxOuts)
    writer.writeVarLengthBuf(subscript.toBuffer());

    // Outputs (none/one/all, depending on flags)
    writer.write(components.hashOutputs);

    // Locktime
    writer.writeUInt32LE(transaction.nLockTime);

    // sighashType 
    writer.writeVarLengthBuf(sighashType.toBuffer());

    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf).reverse();
  }

  private static _getSighashComponents(transaction: Transaction, inputNumber: number, sighashType: SighashType): SighashComponents {
    ValidationUtils.validateArgument(!sighashType.isInvalid(), 'sighashType');
    let hashPrevouts: Uint8Array, hashSequence: Uint8Array, hashInputAmounts: Uint8Array, hashOutputs: Uint8Array;

    switch (sighashType.inType) {
      case InputSighashType.FIRSTN:
        let firstN = sighashType.inData[0];
        ValidationUtils.validateArgument(firstN <= transaction.inputs.length, 'firstN out of range');
        hashPrevouts = this._getPrevoutHash(transaction, firstN);
        hashSequence = this._getSequenceHash(transaction, firstN);
        hashInputAmounts = this._getInputAmountHash(transaction, firstN);
        break;
      case InputSighashType.THISIN:
        ValidationUtils.validateArgument(inputNumber < transaction.inputs.length, 'inputNumber out of range');
        hashPrevouts = this._getPrevoutHashOf(transaction, inputNumber);
        hashSequence = this._getSequenceHashOf(transaction, inputNumber);
        hashInputAmounts = this._getInputAmountHashOf(transaction, inputNumber);
        break;
      default: // ALL
        hashPrevouts = this._getPrevoutHash(transaction, transaction.inputs.length);
        hashSequence = this._getSequenceHash(transaction, transaction.inputs.length);
        hashInputAmounts = this._getInputAmountHash(transaction, transaction.inputs.length);
        break;
    }

    switch (sighashType.outType) {
      case OutputSighashType.FIRSTN:
        let firstN = sighashType.outData[0];
        ValidationUtils.validateArgument(firstN <= transaction.outputs.length, 'firstN out of range');
        hashOutputs = this._getOutputsHash(transaction, firstN);
        break;
      case OutputSighashType.TWO:
        let [out1, out2] = sighashType.outData;
        ValidationUtils.validateArgument(out1 < transaction.outputs.length, 'out1 out of range');
        ValidationUtils.validateArgument(out2 < transaction.outputs.length, 'out2 out of range');
        hashOutputs = this._getOutputsHashOf(transaction, out1, out2);
        break;
      default: // ALL
        hashOutputs = this._getOutputsHash(transaction, transaction.outputs.length);
        break;
    }

    return { hashPrevouts, hashSequence, hashInputAmounts, hashOutputs };
  }

  private static _getPrevoutHash(tx: Transaction, firstN: number): Uint8Array {
    let writer = new BufferWriter();
    for (let i = 0; i < firstN; i++) { 
      writer.writeUInt8(tx.inputs[i].type);
      writer.writeReverse(tx.inputs[i].outpoint);
    }
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }

  private static _getPrevoutHashOf(tx: Transaction, inputNumber: number): Uint8Array {
    let writer = new BufferWriter();
    writer.writeUInt8(tx.inputs[inputNumber].type);
    writer.writeReverse(tx.inputs[inputNumber].outpoint);
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }

  private static _getSequenceHash(tx: Transaction, firstN: number): Uint8Array {
    let writer = new BufferWriter();
    for (let i = 0; i < firstN; i++) { 
      writer.writeUInt32LE(tx.inputs[i].sequenceNumber);
    }
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }

  private static _getSequenceHashOf(tx: Transaction, inputNumber: number): Uint8Array {
    let writer = new BufferWriter();
    writer.writeUInt32LE(tx.inputs[inputNumber].sequenceNumber);
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }

  private static _getInputAmountHash(tx: Transaction, firstN: number): Uint8Array {
    let writer = new BufferWriter();
    for (let i = 0; i < firstN; i++) { 
      writer.writeUInt64LEBN(BN.fromBigInt(tx.inputs[i].amount));
    }
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }

  private static _getInputAmountHashOf(tx: Transaction, inputNumber: number): Uint8Array {
    let writer = new BufferWriter();
    writer.writeUInt64LEBN(BN.fromBigInt(tx.inputs[inputNumber].amount));
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }

  private static _getOutputsHash(tx: Transaction, firstN: number): Uint8Array {
    let writer = new BufferWriter();
    for (let i = 0; i < firstN; i++) { 
      tx.outputs[i].toBufferWriter(writer);
    }
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }

  private static _getOutputsHashOf(tx: Transaction, out1: number, out2: number): Uint8Array {
    let writer = new BufferWriter();
    tx.outputs[out1].toBufferWriter(writer);
    tx.outputs[out2].toBufferWriter(writer);
    let buf = writer.toBuffer();
    return Hash.sha256sha256(buf);
  }
}