import type { IBlockHeader } from "../../common/interfaces";
import BN from "../../crypto/bn.extension";
import BufferReader from "../../encoding/bufferreader";
import BufferUtils from "../../utils/buffer.utils";
import ValidationUtils from "../../utils/validation.utils";
import BufferWriter from "../../encoding/bufferwriter";
import Hash from "../../crypto/hash";

export default class BlockHeader implements IBlockHeader {

  public static readonly MAX_TIME_OFFSET = 2 * 60 * 60;

  public prevHash: Uint8Array;
  public bits: number;
  public ancestorHash: Uint8Array;
  public merkleRoot: Uint8Array;
  public txFilter: Uint8Array;
  public time: number;
  public height: number;
  public chainWork: Uint8Array;
  public size: number;
  public txCount: number;
  public poolFee: number;
  public utxoCommitment: Uint8Array;
  public minerData: Uint8Array;
  public nonce: Uint8Array;

  constructor(data: Uint8Array | IBlockHeader) {
    ValidationUtils.validateArgument(data != null, 'data is required');

    if (BufferUtils.isBuffer(data)) {
      data = BlockHeader._fromBufferReader(new BufferReader(data));
    } else if (typeof data !== 'object') {
      throw new TypeError('Unrecognized argument for BlockHeader');
    }
  
    this.prevHash = typeof data.prevHash === 'string' ? BufferUtils.hexToBuffer(data.prevHash) : data.prevHash;
    this.bits = data.bits;
    this.ancestorHash = typeof data.ancestorHash === 'string' ? BufferUtils.hexToBuffer(data.ancestorHash) : data.ancestorHash;
    this.merkleRoot = typeof data.merkleRoot === 'string' ? BufferUtils.hexToBuffer(data.merkleRoot) : data.merkleRoot;
    this.txFilter = typeof data.txFilter === 'string' ? BufferUtils.hexToBuffer(data.txFilter) : data.txFilter;
    this.time = data.time;
    this.height = data.height;
    this.chainWork = typeof data.chainWork === 'string' ? BufferUtils.hexToBuffer(data.chainWork) : data.chainWork;
    this.size = data.size;
    this.txCount = data.txCount;
    this.poolFee = data.poolFee;
    this.utxoCommitment = typeof data.utxoCommitment === 'string' ? BufferUtils.hexToBuffer(data.utxoCommitment) : data.utxoCommitment;
    this.minerData = typeof data.minerData === 'string' ? BufferUtils.hexToBuffer(data.minerData) : data.minerData;
    this.nonce = typeof data.nonce === 'string' ? BufferUtils.hexToBuffer(data.nonce) : data.nonce;

    if (data.hash) {
      ValidationUtils.validateState(this.hash === data.hash,
        'Argument object hash property does not match block hash.'
      );
    }
  }

  public get hash(): string {
    return BufferUtils.bufferToHex(this._getHash().reverse());
  }

  /**
   * @returns The little endian hash buffer of the header
   */
  private _getHash(): Uint8Array {
    let miniHeader = new BufferWriter();
    miniHeader.writeReverse(this.prevHash);
    miniHeader.writeInt32LE(this.bits);

    let miniHash = Hash.sha256(miniHeader.toBuffer())

    let extHeader = new BufferWriter();
    extHeader.writeReverse(this.ancestorHash);
    extHeader.writeReverse(this.txFilter);
    extHeader.writeReverse(this.merkleRoot);
    extHeader.writeInt32LE(this.time);
    extHeader.writeUInt64LEBN(BN.fromNumber(this.height));
    extHeader.writeReverse(this.chainWork);
    extHeader.writeUInt64LEBN(BN.fromNumber(this.size));
    extHeader.writeUInt64LEBN(BN.fromNumber(this.txCount));
    extHeader.writeUInt64LEBN(BN.fromNumber(this.poolFee));
    extHeader.writeVarLengthBuf(this.utxoCommitment);
    extHeader.writeVarLengthBuf(this.minerData);
    extHeader.writeVarLengthBuf(this.nonce);

    let extHash = Hash.sha256(extHeader.toBuffer());

    let commintment = new BufferWriter();
    commintment.write(miniHash);
    commintment.write(extHash);

    return Hash.sha256(commintment.toBuffer());
  }

  /**
   * @param br A BufferReader of the block header
   * @returns An object representing block header data
   */
  private static _fromBufferReader(br: BufferReader): IBlockHeader {
    return {
      prevHash: br.readReverse(32),
      bits: br.readUInt32LE(),
      ancestorHash: br.readReverse(32),
      merkleRoot: br.readReverse(32),
      txFilter: br.readReverse(32),
      time: br.readUInt32LE(),
      height: br.readCoreVarintNum(),
      chainWork: br.readReverse(32),
      size: br.readUInt64LEBN().toNumber(),
      txCount: br.readCoreVarintNum(),
      poolFee: br.readCoreVarintNum(),
      utxoCommitment: br.readVarLengthBuffer(),
      minerData: br.readVarLengthBuffer(),
      nonce: br.readVarLengthBuffer(),
    };
  }

  /**
   * This method is useful for hex that represent concatination of multiple headers
   * so it able to serve in a loop.
   * 
   * @param br A BufferReader of the block header
   * @returns An instance of block header
   */
  public static fromBufferReader(br: BufferReader): BlockHeader {
    let info = this._fromBufferReader(br);
    return new BlockHeader(info);
  }

  /**
   * @param header A plain JavaScript block header object
   * @returns An instance of block header
   */
  public static fromObject(header: IBlockHeader): BlockHeader {
    return new BlockHeader(header);
  }

  /**
   * @param buf A buffer of the block header
   * @returns An instance of block header
   */
  public static fromBuffer(buf: Uint8Array): BlockHeader {
    return this.fromBufferReader(new BufferReader(buf));
  }

  /**
   * @param hex A hex encoded buffer of the block header
   * @returns An instance of block header
   */
  public static fromString(hex: string): BlockHeader {
    let buf = BufferUtils.hexToBuffer(hex);
    return this.fromBuffer(buf);
  }

  public toJSON = this.toObject;

  /**
   * @returns A plain object of the BlockHeader
   */
  public toObject(): IBlockHeader {
    return {
      hash: this.hash,
      prevHash: BufferUtils.bufferToHex(this.prevHash),
      bits: this.bits,
      ancestorHash: BufferUtils.bufferToHex(this.ancestorHash),
      merkleRoot: BufferUtils.bufferToHex(this.merkleRoot),
      txFilter: BufferUtils.bufferToHex(this.txFilter),
      time: this.time,
      height: this.height,
      chainWork: BufferUtils.bufferToHex(this.chainWork),
      size: this.size,
      txCount: this.txCount,
      poolFee: this.poolFee,
      utxoCommitment: BufferUtils.bufferToHex(this.utxoCommitment),
      minerData: BufferUtils.bufferToHex(this.minerData),
      nonce: BufferUtils.bufferToHex(this.nonce),
    };
  }

  /**
   * @param bw - An existing instance BufferWriter
   * @returns An instance of BufferWriter representation of the BlockHeader
   */
  public toBufferWriter(bw?: BufferWriter): BufferWriter {
    if (!bw) {
      bw = new BufferWriter();
    }
    bw.writeReverse(this.prevHash);
    bw.writeInt32LE(this.bits);
    bw.writeReverse(this.ancestorHash);
    bw.writeReverse(this.merkleRoot);
    bw.writeReverse(this.txFilter);
    bw.writeInt32LE(this.time);
    bw.writeCoreVarintNum(this.height);
    bw.writeReverse(this.chainWork);
    bw.writeUInt64LEBN(BN.fromNumber(this.size));
    bw.writeCoreVarintNum(this.txCount);
    bw.writeCoreVarintNum(this.poolFee);
    bw.writeVarLengthBuf(this.utxoCommitment);
    bw.writeVarLengthBuf(this.minerData);
    bw.writeVarLengthBuf(this.nonce);
    return bw;
  }

  /**
   * @returns A Uint8Array of the BlockHeader
   */
  public toBuffer(): Uint8Array {
    return this.toBufferWriter().toBuffer();
  }

  /**
   * @returns A hex encoded string of the BlockHeader
   */
  public toString(): string {
    return BufferUtils.bufferToHex(this.toBuffer());
  }

  /**
   * @returns A string formatted for the console
   */
  public inspect(): string {
    return `<BlockHeader ${this.hash}>`;
  }

  /**
   * Returns the target difficulty for this block
   * 
   * @param bits the bits number
   * @returns An instance of BN with the decoded difficulty bits
   */
  public getTargetDifficulty(bits?: number): BN {
    let bitsBuf = BN.fromNumber(bits || this.bits).toByteArray({ size: 4, endian: 'big' });

    let exponent = BN.fromBuffer(Uint8Array.from([bitsBuf[0]])).toBigInt();

    let significandBytes = bitsBuf.subarray(1);
    significandBytes[0] = significandBytes[0] & 0x7f;

    let significand = BN.fromBuffer(significandBytes);

    let target = 0n;

    if (exponent <= 3n) {
      target = significand.toBigInt() / (2n ** (8n * (3n - exponent)));
    } else {
      target = significand.toBigInt() * (2n ** (8n * (exponent - 3n)));
    }
    target = (2n**256n) / (target+1n)

    return new BN(target.toString());
  }

  /**
   * @returns the target difficulty for this block
   */
  public getDifficulty(): number {
    let nShift = (this.bits >> 24) & 0xff;

    let dDiff = 0x0000ffff / (this.bits & 0x00ffffff);

    while (nShift < 29)
    {
        dDiff *= 256.0;
        nShift++;
    }
    while (nShift > 29)
    {
        dDiff /= 256.0;
        nShift--;
    }

    return dDiff;
  }

  /**
   * @returns true If timestamp is not too far in the future
   */
  public validTimestamp(): boolean {
    let currentTime = Math.round(new Date().getTime() / 1000);
    return this.time <= currentTime + BlockHeader.MAX_TIME_OFFSET
  }
}
