/**
 * @packageDocumentation
 * @module API-DVM-BaseTx
 */
import { Buffer } from "buffer/"
import BinTools from "../../utils/bintools"
import { DVMConstants } from "./constants"
import { TransferableOutput } from "./outputs"
import { TransferableInput } from "./inputs"
import { SelectCredentialClass } from "./credentials"
import { KeyChain, KeyPair } from "./keychain"
import { StandardBaseTx } from "../../common/tx"
import { Signature, SigIdx, Credential } from "../../common/credentials"
import { DefaultNetworkID } from "../../utils/constants"
import { SelectTxClass } from "./tx"
import {
  Serialization,
  SerializedEncoding,
  SerializedType
} from "../../utils/serialization"
import { CodecIdError } from "../../utils/errors"

/**
 * @ignore
 */
const bintools: BinTools = BinTools.getInstance()
const serialization: Serialization = Serialization.getInstance()
const decimalString: SerializedType = "decimalString"
const buffer: SerializedType = "Buffer"
const display: SerializedEncoding = "display"

/**
 * Class representing a base for all transactions.
 */
export class BaseTx extends StandardBaseTx<KeyPair, KeyChain> {
  protected _typeName = "BaseTx"
  protected _codecID = DVMConstants.LATESTCODEC
  protected _typeID =
    this._codecID === 0 ? DVMConstants.BASETX : DVMConstants.BASETX_CODECONE

  //serialize is inherited

  deserialize(fields: object, encoding: SerializedEncoding = "hex") {
    super.deserialize(fields, encoding)
    this.outs = fields["outs"].map((o: TransferableOutput) => {
      let newOut: TransferableOutput = new TransferableOutput()
      newOut.deserialize(o, encoding)
      return newOut
    })
    this.ins = fields["ins"].map((i: TransferableInput) => {
      let newIn: TransferableInput = new TransferableInput()
      newIn.deserialize(i, encoding)
      return newIn
    })
    this.numouts = serialization.decoder(
      this.outs.length.toString(),
      display,
      decimalString,
      buffer,
      4
    )
    this.numins = serialization.decoder(
      this.ins.length.toString(),
      display,
      decimalString,
      buffer,
      4
    )
  }

  getOuts(): TransferableOutput[] {
    return this.outs as TransferableOutput[]
  }

  getIns(): TransferableInput[] {
    return this.ins as TransferableInput[]
  }

  getTotalOuts(): TransferableOutput[] {
    return this.getOuts() as TransferableOutput[]
  }

  /**
   * Set the codecID
   *
   * @param codecID The codecID to set
   */
  setCodecID(codecID: number): void {
    if (codecID !== 0 && codecID !== 1) {
      /* istanbul ignore next */
      throw new CodecIdError(
        "Error - BaseTx.setCodecID: invalid codecID. Valid codecIDs are 0 and 1."
      )
    }
    this._codecID = codecID
    this._typeID =
      this._codecID === 0 ? DVMConstants.BASETX : DVMConstants.BASETX_CODECONE
  }

  /**
   * Returns the id of the [[BaseTx]]
   */
  getTxType(): number {
    return this._typeID
  }

  /**
   * Takes a {@link https://github.com/feross/buffer|Buffer} containing an [[BaseTx]], parses it, populates the class, and returns the length of the BaseTx in bytes.
   *
   * @param bytes A {@link https://github.com/feross/buffer|Buffer} containing a raw [[BaseTx]]
   *
   * @returns The length of the raw [[BaseTx]]
   *
   * @remarks assume not-checksummed
   */
  fromBuffer(bytes: Buffer, offset: number = 0): number {
    this.networkID = bintools.copyFrom(bytes, offset, offset + 4)
    offset += 4
    this.blockchainID = bintools.copyFrom(bytes, offset, offset + 32)
    offset += 32
    this.numouts = bintools.copyFrom(bytes, offset, offset + 4)
    offset += 4
    const outcount: number = this.numouts.readUInt32BE(0)
    this.outs = []
    for (let i: number = 0; i < outcount; i++) {
      const xferout: TransferableOutput = new TransferableOutput()
      offset = xferout.fromBuffer(bytes, offset)
      this.outs.push(xferout)
    }

    this.numins = bintools.copyFrom(bytes, offset, offset + 4)
    offset += 4
    const incount: number = this.numins.readUInt32BE(0)
    this.ins = []
    for (let i: number = 0; i < incount; i++) {
      const xferin: TransferableInput = new TransferableInput()
      offset = xferin.fromBuffer(bytes, offset)
      this.ins.push(xferin)
    }
    let memolen: number = bintools
      .copyFrom(bytes, offset, offset + 4)
      .readUInt32BE(0)
    offset += 4
    this.memo = bintools.copyFrom(bytes, offset, offset + memolen)
    offset += memolen
    return offset
  }

  /**
   * Takes the bytes of an [[UnsignedTx]] and returns an array of [[Credential]]s
   *
   * @param msg A Buffer for the [[UnsignedTx]]
   * @param kc An [[KeyChain]] used in signing
   *
   * @returns An array of [[Credential]]s
   */
  sign(msg: Buffer, kc: KeyChain): Credential[] {
    const creds: Credential[] = []
    for (let i: number = 0; i < this.ins.length; i++) {
      const cred: Credential = SelectCredentialClass(
        this.ins[`${i}`].getInput().getCredentialID()
      )
      const sigidxs: SigIdx[] = this.ins[`${i}`].getInput().getSigIdxs()
      for (let j: number = 0; j < sigidxs.length; j++) {
        const keypair: KeyPair = kc.getKey(sigidxs[`${j}`].getSource())
        const signval: Buffer = keypair.sign(msg)
        const sig: Signature = new Signature()
        sig.fromBuffer(signval)
        cred.addSignature(sig)
      }
      creds.push(cred)
    }
    return creds
  }

  clone(): this {
    let newbase: BaseTx = new BaseTx()
    newbase.fromBuffer(this.toBuffer())
    return newbase as this
  }

  create(...args: any[]): this {
    return new BaseTx(...args) as this
  }

  select(id: number, ...args: any[]): this {
    let newbasetx: BaseTx = SelectTxClass(id, ...args)
    return newbasetx as this
  }

  /**
   * Class representing a BaseTx which is the foundation for all transactions.
   *
   * @param networkID Optional networkID, [[DefaultNetworkID]]
   * @param blockchainID Optional blockchainID, default Buffer.alloc(32, 16)
   * @param outs Optional array of the [[TransferableOutput]]s
   * @param ins Optional array of the [[TransferableInput]]s
   * @param memo Optional {@link https://github.com/feross/buffer|Buffer} for the memo field
   */
  constructor(
    networkID: number = DefaultNetworkID,
    blockchainID: Buffer = Buffer.alloc(32, 16),
    outs: TransferableOutput[] = undefined,
    ins: TransferableInput[] = undefined,
    memo: Buffer = undefined
  ) {
    super(networkID, blockchainID, outs, ins, memo)
  }
}
