/**
 * @packageDocumentation
 * @module Common-SECP256k1KeyChain
 */
import { Buffer } from "buffer/"
import * as elliptic from "elliptic"
import createHash from "create-hash"
import BinTools from "../utils/bintools"
import { StandardKeyPair, StandardKeyChain } from "./keychain"
import { PublicKeyError } from "../utils/errors"
import { BNInput } from "elliptic"
import { Serialization, SerializedType } from "../utils"

/**
 * @ignore
 */
const EC: typeof elliptic.ec = elliptic.ec

/**
 * @ignore
 */
const ec: elliptic.ec = new EC("secp256k1")

/**
 * @ignore
 */
const ecparams: any = ec.curve

/**
 * @ignore
 */
const BN: any = ecparams.n.constructor

/**
 * @ignore
 */
const bintools: BinTools = BinTools.getInstance()
const serialization: Serialization = Serialization.getInstance()

/**
 * Class for representing a private and public keypair on the Authority Chain.
 */
export abstract class SECP256k1KeyPair extends StandardKeyPair {
  protected keypair: elliptic.ec.KeyPair
  protected chainID: string = ""
  protected hrp: string = ""

  /**
   * @ignore
   */
  protected _sigFromSigBuffer(sig: Buffer): elliptic.ec.SignatureOptions {
    const r: BNInput = new BN(bintools.copyFrom(sig, 0, 32))
    const s: BNInput = new BN(bintools.copyFrom(sig, 32, 64))
    const recoveryParam: number = bintools
      .copyFrom(sig, 64, 65)
      .readUIntBE(0, 1)
    const sigOpt = {
      r: r,
      s: s,
      recoveryParam: recoveryParam
    }
    return sigOpt
  }

  /**
   * Generates a new keypair.
   */
  generateKey() {
    this.keypair = ec.genKeyPair()

    // doing hex translation to get Buffer class
    this.privk = Buffer.from(
      this.keypair.getPrivate("hex").padStart(64, "0"),
      "hex"
    )
    this.pubk = Buffer.from(
      this.keypair.getPublic(true, "hex").padStart(66, "0"),
      "hex"
    )
  }

  /**
   * Imports a private key and generates the appropriate public key.
   *
   * @param privk A {@link https://github.com/feross/buffer|Buffer} representing the private key
   *
   * @returns true on success, false on failure
   */
  importKey(privk: Buffer): boolean {
    this.keypair = ec.keyFromPrivate(privk.toString("hex"), "hex")
    // doing hex translation to get Buffer class
    try {
      this.privk = Buffer.from(
        this.keypair.getPrivate("hex").padStart(64, "0"),
        "hex"
      )
      this.pubk = Buffer.from(
        this.keypair.getPublic(true, "hex").padStart(66, "0"),
        "hex"
      )
      return true // silly I know, but the interface requires so it returns true on success, so if Buffer fails validation...
    } catch (error) {
      return false
    }
  }

  /**
   * Returns the address as a {@link https://github.com/feross/buffer|Buffer}.
   *
   * @returns A {@link https://github.com/feross/buffer|Buffer} representation of the address
   */
  getAddress(): Buffer {
    return SECP256k1KeyPair.addressFromPublicKey(this.pubk)
  }

  /**
   * Returns the address's string representation.
   *
   * @returns A string representation of the address
   */
  getAddressString(): string {
    const addr: Buffer = SECP256k1KeyPair.addressFromPublicKey(this.pubk)
    const type: SerializedType = "bech32"
    return serialization.bufferToType(addr, type, this.hrp, this.chainID)
  }

  /**
   * Returns an address given a public key.
   *
   * @param pubk A {@link https://github.com/feross/buffer|Buffer} representing the public key
   *
   * @returns A {@link https://github.com/feross/buffer|Buffer} for the address of the public key.
   */
  static addressFromPublicKey(pubk: Buffer): Buffer {
    if (pubk.length === 65) {
      /* istanbul ignore next */
      pubk = Buffer.from(
        ec.keyFromPublic(pubk).getPublic(true, "hex").padStart(66, "0"),
        "hex"
      ) // make compact, stick back into buffer
    }
    if (pubk.length === 33) {
      const sha256: Buffer = Buffer.from(
        createHash("sha256").update(pubk).digest()
      )
      const ripesha: Buffer = Buffer.from(
        createHash("ripemd160").update(sha256).digest()
      )
      return ripesha
    }
    /* istanbul ignore next */
    throw new PublicKeyError("Unable to make address.")
  }

  /**
   * Returns a string representation of the private key.
   *
   * @returns A cb58 serialized string representation of the private key
   */
  getPrivateKeyString(): string {
    return `PrivateKey-${bintools.cb58Encode(this.privk)}`
  }

  /**
   * Returns the public key.
   *
   * @returns A cb58 serialized string representation of the public key
   */
  getPublicKeyString(): string {
    return bintools.cb58Encode(this.pubk)
  }

  /**
   * Takes a message, signs it, and returns the signature.
   *
   * @param msg The message to sign, be sure to hash first if expected
   *
   * @returns A {@link https://github.com/feross/buffer|Buffer} containing the signature
   */
  sign(msg: Buffer): Buffer {
    const sigObj: elliptic.ec.Signature = this.keypair.sign(msg, undefined, {
      canonical: true
    })
    const recovery: Buffer = Buffer.alloc(1)
    recovery.writeUInt8(sigObj.recoveryParam, 0)
    const r: Buffer = Buffer.from(sigObj.r.toArray("be", 32)) //we have to skip native Buffer class, so this is the way
    const s: Buffer = Buffer.from(sigObj.s.toArray("be", 32)) //we have to skip native Buffer class, so this is the way
    const result: Buffer = Buffer.concat([r, s, recovery], 65)
    return result
  }

  /**
   * Verifies that the private key associated with the provided public key produces the signature associated with the given message.
   *
   * @param msg The message associated with the signature
   * @param sig The signature of the signed message
   *
   * @returns True on success, false on failure
   */
  verify(msg: Buffer, sig: Buffer): boolean {
    const sigObj: elliptic.ec.SignatureOptions = this._sigFromSigBuffer(sig)
    return ec.verify(msg, sigObj, this.keypair)
  }

  /**
   * Recovers the public key of a message signer from a message and its associated signature.
   *
   * @param msg The message that's signed
   * @param sig The signature that's signed on the message
   *
   * @returns A {@link https://github.com/feross/buffer|Buffer} containing the public key of the signer
   */
  recover(msg: Buffer, sig: Buffer): Buffer {
    const sigObj: elliptic.ec.SignatureOptions = this._sigFromSigBuffer(sig)
    const pubk = ec.recoverPubKey(msg, sigObj, sigObj.recoveryParam)
    return Buffer.from(pubk.encodeCompressed())
  }

  /**
   * Returns the chainID associated with this key.
   *
   * @returns The [[KeyPair]]'s chainID
   */
  getChainID(): string {
    return this.chainID
  }

  /**
   * Sets the the chainID associated with this key.
   *
   * @param chainID String for the chainID
   */
  setChainID(chainID: string): void {
    this.chainID = chainID
  }

  /**
   * Returns the Human-Readable-Part of the network associated with this key.
   *
   * @returns The [[KeyPair]]'s Human-Readable-Part of the network's Bech32 addressing scheme
   */
  getHRP(): string {
    return this.hrp
  }

  /**
   * Sets the the Human-Readable-Part of the network associated with this key.
   *
   * @param hrp String for the Human-Readable-Part of Bech32 addresses
   */
  setHRP(hrp: string): void {
    this.hrp = hrp
  }

  constructor(hrp: string, chainID: string) {
    super()
    this.chainID = chainID
    this.hrp = hrp
    this.generateKey()
  }
}

/**
 * Class for representing a key chain in Dcomm.
 *
 * @typeparam SECP256k1KeyPair Class extending [[StandardKeyPair]] which is used as the key in [[SECP256k1KeyChain]]
 */
export abstract class SECP256k1KeyChain<
  SECPKPClass extends SECP256k1KeyPair
> extends StandardKeyChain<SECPKPClass> {
  /**
   * Makes a new key pair, returns the address.
   *
   * @returns Address of the new key pair
   */
  makeKey: () => SECPKPClass

  addKey(newKey: SECPKPClass): void {
    super.addKey(newKey)
  }

  /**
   * Given a private key, makes a new key pair, returns the address.
   *
   * @param privk A {@link https://github.com/feross/buffer|Buffer} or cb58 serialized string representing the private key
   *
   * @returns Address of the new key pair
   */
  importKey: (privk: Buffer | string) => SECPKPClass
}
