import {Buffer} from 'buffer';
import BN from 'bn.js';
import {Md5Hash, SashaHash, Sha1Hash} from './hash';
import * as ellip from 'elliptic';
import {
  BlockStatusCode,
  CommonDelimeterChar,
  CryptoTransactionBeforeStruct,
  CryptoTransactionResult,
  CryptoTransactionStruct,
  Default_WordListArrayCount,
  KeyPair,
  ListMainNodeIp,
  NetworkLayer,
  NetworkType,
  Prefix_MainNetwork,
  Prefix_TestNetwork,
  Prefix_DevNetwork,
  Prefix_MultiMainNetwork,
  Prefix_MultiTestNetwork,
  Prefix_MultiDevNetwork,
} from '../types';
import {
  GetNetworkPort,
  MakeHttpListenerPath,
  MergeRawDataWithCommonDelimeterChar,
  PostRequest,
  SeedPhraseList,
} from './functions';

export function generateKeyPair(
  wordList: string[] = [],
  network: NetworkType,
): KeyPair {
  if (wordList.length == 0) wordList = SeedPhraseList();
  var privKey = generatePrivateKey(wordList);
  var pubKey = generatePublicKey(privKey);
  var walletAddress = generateWalletKey(pubKey, network);
  return {
    PublicKey: pubKey,
    PrivateKey: privKey,
    WalletKey: walletAddress,
    Words: wordList,
  };
}

export function generateWalletKey(
  publicKey: string,
  network: NetworkType,
): string {
  if (publicKey.substring(0, 2) === '04')
    publicKey = publicKey.substring(2, publicKey.length);

  var pubCorsX = publicKey.substring(0, 64);
  var pubCorsY = publicKey.substring(64, publicKey.length);

  var pubCorsMainCalculate = new BN('0' + pubCorsY, 'hex').mod(new BN(2));
  var pubCorsMain = '';

  if (pubCorsMainCalculate.isZero()) pubCorsMain = '02' + pubCorsX;
  else pubCorsMain = '03' + pubCorsX;

  var networkByteStr = 10;
  var keyPrefix = Prefix_MainNetwork;

  if (network == NetworkType.TestNet) {
    networkByteStr = 20;
    keyPrefix = Prefix_TestNetwork;
  } else if (network == NetworkType.DevNet) {
    networkByteStr = 30;
    keyPrefix = Prefix_DevNetwork;
  }

  var hashPubKeyStr = shrinkHex(SashaHash(SashaHash(pubCorsMain)), 22);

  var checkSumStr = shrinkHex(
    SashaHash(SashaHash(networkByteStr + publicKey)),
    4,
  );

  var walletAddress = bs58encode(
    BigInt(
      new BN(networkByteStr + hashPubKeyStr + checkSumStr, 16).toString(10),
    ),
    36,
  );

  return keyPrefix + walletAddress;
}

export function generatePublicKey(privateKey: string): string {
  var EC = ellip.ec;
  var ec = new EC('p256');
  var key = ec
    .keyFromPrivate(privateKey, 'hex')
    .getPublic()
    .encode('hex', false);
  return key;
}

export function generatePrivateKey(wordList: string[]): string {
  if (wordList.length != 16) return '0';

  var tmpWordList: string[] = [];
  var fullWordLine: string = '';
  for (var i = 0; i < Default_WordListArrayCount; i++) {
    if (i == 0) {
      fullWordLine = wordList[i] + CommonDelimeterChar;
    } else {
      fullWordLine = fullWordLine + wordList[i];
      if (Default_WordListArrayCount - 1 > i)
        fullWordLine = fullWordLine + CommonDelimeterChar;
    }
    tmpWordList[i] = Md5Hash(wordList[i]);
  }
  fullWordLine = SashaHash(
    SashaHash(fullWordLine, false, false) + CommonDelimeterChar + fullWordLine,
    false,
    false,
  );

  var hexResultStr: string = '';
  for (var i = 0; i < Default_WordListArrayCount; i++) {
    tmpWordList[i] = shrinkHex(
      Sha1Hash(fullWordLine + CommonDelimeterChar + tmpWordList[i]),
      2,
    );
    hexResultStr = hexResultStr + tmpWordList[i];
  }

  return hexResultStr;
}

export function signMessage(
  message: string,
  privateKey: string,
): {hash: string; signature: string} {
  var EC = ellip.ec;
  var ec = new EC('p256');

  var t = SashaHash(message, false, false).substring(0, 64);
  var key = ec.keyFromPrivate(privateKey);
  var signature = key.sign(new BN(t, 16).toString(16));
  return {hash: t, signature: bytesToHex(Buffer.from(signature.toDER()))};
}

export function validateMessage(
  message: string,
  signature: string,
  publicKey: string,
): boolean {
  var EC = ellip.ec;
  var ec = new EC('p256');
  var key = ec.keyFromPublic(publicKey, 'hex');
  return key.verify(
    SashaHash(message, false, false).substring(0, 64),
    signature,
  );
}

export function CheckAddress(
  walletAddress: string,
  whichNetworkFor: NetworkType = NetworkType.MainNet,
): boolean {
  if (walletAddress != null) {
    if (walletAddress.length == 39) {
      if (whichNetworkFor == NetworkType.TestNet) {
        if (
          walletAddress.substring(0, Prefix_TestNetwork.length) ==
          Prefix_TestNetwork
        ) {
          return true;
        }
      } else if (whichNetworkFor == NetworkType.DevNet) {
        if (
          walletAddress.substring(0, Prefix_DevNetwork.length) ==
          Prefix_DevNetwork
        ) {
          return true;
        }
      } else {
        if (
          walletAddress.substring(0, Prefix_MainNetwork.length) ==
          Prefix_MainNetwork
        ) {
          return true;
        }
      }
    }
  }

  return false;
}

export function CheckMultiAddress(
  walletAddress: string,
  whichNetworkFor: NetworkType = NetworkType.MainNet,
): boolean {
  if (walletAddress != null) {
    if (walletAddress.length == 39) {
      if (whichNetworkFor == NetworkType.TestNet) {
        if (
          walletAddress.substring(0, Prefix_MultiTestNetwork.length) ==
          Prefix_MultiTestNetwork
        ) {
          return true;
        }
      } else if (whichNetworkFor == NetworkType.DevNet) {
        if (
          walletAddress.substring(0, Prefix_MultiDevNetwork.length) ==
          Prefix_MultiDevNetwork
        ) {
          return true;
        }
      } else {
        if (
          walletAddress.substring(0, Prefix_MultiMainNetwork.length) ==
          Prefix_MultiMainNetwork
        ) {
          return true;
        }
      }
    }
  }

  return false;
}

export function isMultiWalletAddress(walletAddress: string): boolean {
  if (walletAddress != null) {
    if (walletAddress.length == 39) {
      if (
        walletAddress.substring(0, Prefix_MultiTestNetwork.length) ==
          Prefix_MultiTestNetwork ||
        walletAddress.substring(0, Prefix_MultiDevNetwork.length) ==
          Prefix_MultiDevNetwork ||
        walletAddress.substring(0, Prefix_MultiMainNetwork.length) ==
          Prefix_MultiMainNetwork
      ) {
        return true;
      }
    }
  }

  return false;
}

export async function sendTransaction(
  preTransfer: CryptoTransactionStruct,
  currentNetwork: NetworkType,
  nodeIp: string = '',
  isSsl: boolean = true,
): Promise<CryptoTransactionResult> {
  try {
    const tmpTransactVerify = verify(preTransfer);
    if (!tmpTransactVerify) {
      const result: CryptoTransactionResult = {
        ID: '',
        Result: BlockStatusCode.WrongSignature,
      };
      return result;
    }

    let exitInnerLoop = false;
    while (!exitInnerLoop) {
      for (let a = 0; a < ListMainNodeIp.length && !exitInnerLoop; a++) {
        let nodeIpAddress = '';
        if (nodeIp == '') nodeIpAddress = ListMainNodeIp[a];
        else nodeIpAddress = nodeIp;

        try {
          let fullUrlAddress = '';
          if (isSsl) {
            fullUrlAddress = 'https://devnet.notus.network/send';
          } else {
            fullUrlAddress = MakeHttpListenerPath(
              nodeIpAddress,
              GetNetworkPort(NetworkLayer.Layer1, currentNetwork).toString() +
                '/send',
            );
          }

          const ResultStr = await PostRequest(fullUrlAddress, preTransfer);

          const tmpResultStr: CryptoTransactionResult = JSON.parse(ResultStr);
          exitInnerLoop = true;
          return tmpResultStr;
        } catch (err) {
          console.log(err);
          exitInnerLoop = true;
        }

        if (nodeIp != '') {
          return {
            ID: '',
            Result: BlockStatusCode.AnErrorOccurred,
          };
        }
      }
    }
  } catch (err) {
    console.log(err);
  }

  return {
    ID: '',
    Result: BlockStatusCode.AnErrorOccurred,
  };
}

export function signTransaction(
  preTransfer: CryptoTransactionBeforeStruct,
): CryptoTransactionStruct | boolean {
  if (isMultiWalletAddress(preTransfer.Sender)) {
    if (!CheckMultiAddress(preTransfer.Sender, preTransfer.Network))
      return false;
  } else if (!CheckAddress(preTransfer.Sender, preTransfer.Network))
    return false;

  if (isMultiWalletAddress(preTransfer.Receiver)) {
    if (!CheckMultiAddress(preTransfer.Receiver, preTransfer.Network))
      return false;
  } else if (!CheckAddress(preTransfer.Receiver, preTransfer.Network))
    return false;

  return {
    ErrorNo: 0,
    CurrentTime: preTransfer.CurrentTime,
    UnlockTime: preTransfer.UnlockTime,
    Currency: preTransfer.Currency,
    Sender: preTransfer.Sender,
    Receiver: preTransfer.Receiver,
    Volume: preTransfer.Volume,
    PublicKey: generatePublicKey(preTransfer.PrivateKey),
    Sign: signMessage(
      MergeRawDataWithCommonDelimeterChar(
        preTransfer.Sender,
        preTransfer.Receiver,
        preTransfer.Volume,
        preTransfer.CurrentTime,
        preTransfer.UnlockTime,
        preTransfer.Currency,
      ),
      preTransfer.PrivateKey,
    ).signature,
    CurveName: 'prime256v1',
    Network: preTransfer.Network,
  };
}

const zero = new BN(0);
const negative1 = new BN(-1);

export function fromLNotus(
  data: string | number,
  isFull: boolean = false,
  baseNumber: string = '1000000000',
  baseLength: number = 9,
): string {
  var lNotus = new BN(data);
  var negative = lNotus.lt(zero);
  const base = new BN(baseNumber);

  if (negative) lNotus = lNotus.mul(negative1);

  var fraction = lNotus.mod(base).toString(10);

  while (fraction.length < baseLength) {
    fraction = `0${fraction}`;
  }

  var whole = lNotus.div(base).toString(10);
  whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  var value = '';

  if (isFull) {
    // 0.000500000 = 0.0005
    fraction = fraction.replace(/0+$/, '');

    value = `${whole}${fraction === '' ? '' : `.${fraction}`}`;
  } else
    value = `${whole}${
      fraction == '000000000' ? '.0000' : `.${fraction.substring(0, 4)}`
    }`;

  if (negative) {
    value = `-${value}`;
  }

  return value;
}

export function toLNotus(
  data: string | number,
  baseNumber: string = '1000000000',
  baseLength: number = 9,
): string {
  var lNotus = new String(data);
  const base = new BN(baseNumber);

  var negative = lNotus.substring(0, 1) === '-';
  if (negative) {
    lNotus = lNotus.substring(1);
  }

  if (lNotus === '.') {
    throw new Error(`Data is invalid.`);
  }
  var comps = lNotus.split('.');
  if (comps.length > 2) {
    throw new Error(`Too many decimal points.`);
  }
  var whole = comps[0],
    fraction = comps[1];

  if (!whole) {
    whole = '0';
  }
  if (!fraction) {
    fraction = '0';
  }
  if (fraction.length > baseLength) {
    throw new Error(`Too many decimal places`);
  }

  while (fraction.length < baseLength) {
    fraction += '0';
  }

  var whole2 = new BN(whole);
  var fraction2 = new BN(fraction);
  var lNotus2 = whole2.mul(base).add(fraction2);

  if (negative) {
    lNotus2 = lNotus2.mul(negative1);
  }

  return new BN(lNotus2.toString(10), 10).toString(10);
}

export function dateToString(Date: Date): Number {
  return Number(
    Date.getFullYear() +
      ('0' + (Date.getMonth() + 1)).slice(-2) +
      ('0' + Date.getDate()).slice(-2) +
      ('0' + Date.getHours()).slice(-2) +
      ('0' + Date.getMinutes()).slice(-2) +
      ('0' + Date.getSeconds()).slice(-2),
  );
}

function verify(preTransfer: CryptoTransactionStruct): boolean {
  if (isMultiWalletAddress(preTransfer.Sender)) {
    if (!CheckMultiAddress(preTransfer.Sender, preTransfer.Network))
      return false;
  } else if (!CheckAddress(preTransfer.Sender, preTransfer.Network))
    return false;

  if (isMultiWalletAddress(preTransfer.Receiver)) {
    if (!CheckMultiAddress(preTransfer.Receiver, preTransfer.Network))
      return false;
  } else if (!CheckAddress(preTransfer.Receiver, preTransfer.Network))
    return false;

  return validateMessage(
    MergeRawDataWithCommonDelimeterChar(
      preTransfer.Sender,
      preTransfer.Receiver,
      preTransfer.Volume,
      preTransfer.CurrentTime,
      preTransfer.UnlockTime,
      preTransfer.Currency,
    ),
    preTransfer.Sign,
    preTransfer.PublicKey,
  );
}

function shrinkHex(inputHexData: string, howManyByte: number): string {
  if (2 * howManyByte >= inputHexData.length) return inputHexData;

  var hexArray = hexToBytes(inputHexData);
  var xorValue = 0;
  for (var a = howManyByte; a < hexArray.length; a++) {
    xorValue = Uint8Array.from([xorValue, hexArray[a]]).reduce((a, b) => a ^ b);
  }

  var resultArray: Uint8Array = new Uint8Array(howManyByte);
  for (var x = 0; x < howManyByte; x++) {
    resultArray[x] = Uint8Array.from([hexArray[x], xorValue]).reduce(
      (a, b) => a ^ b,
    );
  }

  return bytesToHex(resultArray);
}

function hexToBytes(hex: string) {
  for (var bytes = [], c = 0; c < hex.length; c += 2)
    bytes.push(parseInt(hex.substr(c, 2), 16));
  return bytes;
}

function bytesToHex(bytes: Uint8Array) {
  for (var hex = [], i = 0; i < bytes.length; i++) {
    var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
    hex.push((current >>> 4).toString(16));
    hex.push((current & 0xf).toString(16));
  }
  return hex.join('');
}

const DefaultBase58AlphabetString =
  '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

function bs58encode(numberToShorten: bigint, resultSize: number = 32): string {
  var result: string[] = [];
  result.length = resultSize;

  var iAlphabetLength = DefaultBase58AlphabetString.length;
  var iAlphabetLength2 = BigInt(iAlphabetLength);

  var i = 0;
  while (numberToShorten >= 0 && result.length > i) {
    var lNumberRemainder = numberToShorten % iAlphabetLength2;
    numberToShorten /= iAlphabetLength2;
    result[result.length - 1 - i] =
      DefaultBase58AlphabetString[Number(lNumberRemainder)];
    i++;
  }

  return result.join('');
}
