//  Copyright (C) 2018 Zilliqa
//
//  This file is part of zilliqa-js
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.

import elliptic from "elliptic";
import hashjs from "hash.js";

import { BN, validation } from "@zilliqa-js/util";

import { fromBech32Address, toBech32Address } from "./bech32";

const secp256k1 = new elliptic.ec("secp256k1");

/**
 * getAddressFromPrivateKey
 *
 * takes a hex-encoded string (private key) and returns its corresponding
 * 20-byte hex-encoded address.
 *
 * @param {string} privateKey
 * @returns {string}
 */
export const getAddressFromPrivateKey = (privateKey: string): string => {
  const normalizedPrviateKey = normalizePrivateKey(privateKey);
  const keyPair = secp256k1.keyFromPrivate(normalizedPrviateKey, "hex");
  const pub = keyPair.getPublic(true, "hex");

  return toChecksumAddress(
    hashjs.sha256().update(pub, "hex").digest("hex").slice(24)
  );
};

/**
 * getPubKeyFromPrivateKey
 *
 * takes a hex-encoded string (private key) and returns its corresponding
 * hex-encoded 33-byte public key.
 *
 * @param {string} privateKey
 * @returns {string}
 */
export const getPubKeyFromPrivateKey = (privateKey: string) => {
  const normalizedPrviateKey = normalizePrivateKey(privateKey);
  const keyPair = secp256k1.keyFromPrivate(normalizedPrviateKey, "hex");
  return keyPair.getPublic(true, "hex");
};

/**
 * getAccountFrom0xPrivateKey
 *
 * Utility method for recovering account from 0x private key.
 * See https://github.com/Zilliqa/zilliqa-js/pull/159
 * @param privateKeyWith0x : private key with 0x prefix
 */

export const getAccountFrom0xPrivateKey = (privateKeyWith0x: string) => {
  const privateKeyWithout0x = normalizePrivateKey(privateKeyWith0x);
  const keyPair = secp256k1.keyFromPrivate(privateKeyWith0x, "hex");
  const publicKeyWith0x = keyPair.getPublic(true, "hex");
  const addressWith0x = getAddressFromPublicKey(publicKeyWith0x);
  const bech32With0x = toBech32Address(addressWith0x);
  const with0x = {
    prv: privateKeyWith0x,
    pub: publicKeyWith0x,
    addr: addressWith0x,
    bech32: bech32With0x,
  };

  const keyPair2 = secp256k1.keyFromPrivate(privateKeyWithout0x, "hex");
  const publicKeyWithout0x = keyPair2.getPublic(true, "hex");
  const addressWithout0x = getAddressFromPublicKey(publicKeyWithout0x);
  const bech32Without0x = toBech32Address(addressWithout0x);
  const without0x = {
    prv: privateKeyWithout0x,
    pub: publicKeyWithout0x,
    addr: addressWithout0x,
    bech32: bech32Without0x,
  };

  const privateKeyAfterChange = keyPair.getPrivate("hex");
  const publicKeyAfterChange = keyPair.getPublic(true, "hex");
  const addressAfterChange = getAddressFromPublicKey(publicKeyAfterChange);
  const bech32AfterChange = toBech32Address(addressAfterChange);

  const changed = {
    prv: privateKeyAfterChange,
    pub: publicKeyAfterChange,
    addr: addressAfterChange,
    bech32: bech32AfterChange,
  };

  return {
    with0x,
    without0x,
    changed,
  };
};

/**
 * compressPublicKey
 *
 * @param {string} publicKey - 65-byte public key, a point (x, y)
 *
 * @returns {string}
 */
export const compressPublicKey = (publicKey: string): string => {
  return secp256k1.keyFromPublic(publicKey, "hex").getPublic(true, "hex");
};

/**
 * getAddressFromPublicKey
 *
 * takes hex-encoded string and returns the corresponding address
 *
 * @param {string} pubKey
 * @returns {string}
 */
export const getAddressFromPublicKey = (publicKey: string) => {
  const normalized = publicKey.toLowerCase().replace("0x", "");
  return toChecksumAddress(
    hashjs.sha256().update(normalized, "hex").digest("hex").slice(24)
  );
};

/**
 * toChecksumAddress
 *
 * takes hex-encoded string and returns the corresponding address
 *
 * @param {string} address
 * @returns {string}
 */
export const toChecksumAddress = (address: string): string => {
  if (!validation.isAddress(address)) {
    throw new Error(`${address} is not a valid base 16 address`);
  }

  address = address.toLowerCase().replace("0x", "");
  const hash = hashjs.sha256().update(address, "hex").digest("hex");
  const v = new BN(hash, "hex", "be");
  let ret = "0x";

  for (let i = 0; i < address.length; i++) {
    if ("0123456789".indexOf(address[i]) !== -1) {
      ret += address[i];
    } else {
      ret += v.and(new BN(2).pow(new BN(255 - 6 * i))).gte(new BN(1))
        ? address[i].toUpperCase()
        : address[i].toLowerCase();
    }
  }

  return ret;
};

/**
 * isValidChecksumAddress
 *
 * takes hex-encoded string and returns boolean if address is checksumed
 *
 * @param {string} address
 * @returns {boolean}
 */
export const isValidChecksumAddress = (address: string): boolean => {
  return (
    validation.isAddress(address.replace("0x", "")) &&
    toChecksumAddress(address) === address
  );
};

/**
 * normaliseAddress
 *
 * takes in a base16 address or a zilliqa bech32 encoded address
 * and returns a checksum base16 address. If the address is neither a base16
 * nor bech32 address, the code will return an error
 * @param {string)} address
 * @returns {string}
 */
export const normaliseAddress = (address: string): string => {
  if (validation.isBech32(address)) {
    return fromBech32Address(address);
  }

  if (!isValidChecksumAddress(address)) {
    throw Error(
      "Wrong address format, should be either bech32 or checksummed address"
    );
  }

  return address;
};

/**
 * encodeBase58 - may be required for DID public key
 * undeprecating this function after version 2.0.0
 *
 * @param {string} hex - base 16 encoded string
 * @returns {string} - big endian base 58 encoded string
 */
export const encodeBase58 = (hex: string): string => {
  const clean = hex.toLowerCase().replace("0x", "");
  const tbl = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
  const base = new BN(58);
  const zero = new BN(0);
  let x = new BN(clean, 16);
  let res = "";

  while (x.gt(zero)) {
    const rem = x.mod(base).toNumber(); // safe, always < 58
    // big endian
    res = tbl[rem] + res;
    // quotient, remainders thrown away in integer division
    x = x.div(base);
  }

  // convert to big endian in case the input hex is little endian
  const hexBE = x.toString("hex", clean.length);
  for (let i = 0; i < hexBE.length; i += 2) {
    if (hex[i] === "0" && hex[i + 1] === "0") {
      res = tbl[0] + res;
    } else {
      break;
    }
  }

  return res;
};

/**
 * decodeBase58 - may be required for DID public key
 * undeprecating this function after version 2.0.0
 *
 * @param {string} raw - base 58 string
 * @returns {string} - big endian base 16 string
 */
export const decodeBase58 = (raw: string): string => {
  const tbl = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
  const base = new BN(58);
  const zero = new BN(0);
  let isBreak = false;
  let n = new BN(0);
  let leader = "";

  for (let i = 0; i < raw.length; i++) {
    const char = raw.charAt(i);
    const weight = new BN(tbl.indexOf(char));
    n = n.mul(base).add(weight);

    // check if padding required
    if (!isBreak) {
      if (i - 1 > 0 && raw[i - 1] !== "1") {
        isBreak = true;
        continue;
      }
      if (char === "1") {
        leader += "00";
      }
    }
  }
  if (n.eq(zero)) {
    return leader;
  }

  let res = leader + n.toString("hex");
  if (res.length % 2 !== 0) {
    res = "0" + res;
  }

  return res;
};

/**
 * verifyPrivateKey
 *
 * @param {string|Buffer} privateKey
 * @returns {boolean}
 */
export const verifyPrivateKey = (privateKey: string): boolean => {
  const keyPair = secp256k1.keyFromPrivate(privateKey, "hex");
  const { result } = keyPair.validate();
  return result;
};

/**
 * normalizePrivateKey : normalise private key from 0x or without 0x prefix
 *
 * @param {string} privateKey
 * @returns {string}
 */

export const normalizePrivateKey = (privateKey: string): string => {
  try {
    if (!validation.isPrivateKey(privateKey)) {
      throw new Error("Private key is not correct");
    }
    const normalized = privateKey.toLowerCase().replace("0x", "");
    if (!verifyPrivateKey(normalized)) {
      throw new Error("Private key is not correct");
    }
    return normalized;
  } catch (error) {
    throw error;
  }
};
