import { ApiPromise } from "@polkadot/api";
import { SubmittableExtrinsic } from "@polkadot/api/types";
import { compactFromU8a, hexStripPrefix, hexToU8a, u8aConcat, u8aToHex, u8aToU8a } from "@polkadot/util";
import { encodeAddress, decodeAddress, blake2AsU8a } from "@polkadot/util-crypto";
import { SUBSTRATE_NETWORK_LIST } from "../constants/networkSpect";
import gov from "../service/gov";
import { QRSigner, QRSubmittable } from "../types/scannerTypes";

const MULTIPART = new Uint8Array([0]);

let signer: QRSigner = {} as QRSigner;
let submittable: QRSubmittable;

/*
  Example Full Raw Data
  ---
  4 // indicates binary
  37 // indicates data length
  --- UOS Specific Data
  00 + // is it multipart?
  0001 + // how many parts in total?
  0000 +  // which frame are we on?
  53 // indicates payload is for Substrate
  01 // crypto: sr25519
  00 // indicates action: signData
  f4cd755672a8f9542ca9da4fbf2182e79135d94304002e6a09ffc96fef6e6c4c // public key
  544849532049532053504152544121 // actual payload to sign (should be SCALE or utf8)
  91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3 // genesis hash
  0 // terminator
  --- SQRC Filler Bytes
  ec11ec11ec11ec // SQRC filler bytes
  */
function _rawDataToU8A(rawData: string) {
  if (!rawData) {
    return null;
  }

  // Strip filler bytes padding at the end
  if (rawData.substr(-2) === "ec") {
    rawData = rawData.substr(0, rawData.length - 2);
  }

  while (rawData.substr(-4) === "ec11") {
    rawData = rawData.substr(0, rawData.length - 4);
  }

  // Verify that the QR encoding is binary and it's ending with a proper terminator
  if (rawData.substr(0, 1) !== "4" || rawData.substr(-1) !== "0") {
    return null;
  }

  // Strip the encoding indicator and terminator for ease of reading
  rawData = rawData.substr(1, rawData.length - 2);

  const length8 = parseInt(rawData.substr(0, 2), 16) || 0;
  const length16 = parseInt(rawData.substr(0, 4), 16) || 0;
  let length = 0;

  // Strip length prefix
  if (length8 * 2 + 2 === rawData.length) {
    rawData = rawData.substr(2);
    length = length8;
  } else if (length16 * 2 + 4 === rawData.length) {
    rawData = rawData.substr(4);
    length = length16;
  } else {
    return null;
  }

  const bytes = new Uint8Array(length);

  for (let i = 0; i < length; i++) {
    bytes[i] = parseInt(rawData.substr(i * 2, 2), 16);
  }

  return bytes;
}

async function _constructDataFromBytes(bytes: Uint8Array, multipartComplete = false) {
  const frameInfo = hexStripPrefix(u8aToHex(bytes.slice(0, 5)));
  const frameCount = parseInt(frameInfo.substr(2, 4), 16);
  const isMultipart = frameCount > 1; // for simplicity, even single frame payloads are marked as multipart.
  const currentFrame = parseInt(frameInfo.substr(6, 4), 16);
  const uosAfterFrames = hexStripPrefix(u8aToHex(bytes.slice(5)));

  // UOS after frames can be metadata json
  if (isMultipart && !multipartComplete) {
    const partData = {
      currentFrame,
      frameCount,
      isMultipart,
      partData: uosAfterFrames,
    };
    return partData;
  }

  const zerothByte = uosAfterFrames.substr(0, 2);
  const firstByte = uosAfterFrames.substr(2, 2);
  const secondByte = uosAfterFrames.substr(4, 2);

  let action: string;

  try {
    // decode payload appropriately via UOS
    switch (zerothByte) {
      case "45": {
        // Ethereum UOS payload
        const data = {
          data: {}, // for consistency with legacy data format.
        };
        action = firstByte === "00" || firstByte === "01" ? "signData" : firstByte === "01" ? "signTransaction" : null;
        const address = uosAfterFrames.substr(4, 44);

        data["action"] = action;
        data.data["account"] = address;
        if (action === "signData") {
          data.data["rlp"] = uosAfterFrames[13];
        } else if (action === "signTransaction") {
          data.data["data"] = uosAfterFrames[13];
        } else {
          throw new Error("Could not determine action type.");
        }
        return data;
      }
      case "53": {
        // Substrate UOS payload
        const data = {
          data: {}, // for consistency with legacy data format.
        };
        try {
          data.data["crypto"] = firstByte === "00" ? "ed25519" : firstByte === "01" ? "sr25519" : null;
          const pubKeyHex = uosAfterFrames.substr(6, 64);
          const publicKeyAsBytes = hexToU8a("0x" + pubKeyHex);
          const hexEncodedData = "0x" + uosAfterFrames.slice(70);
          const hexPayload = hexEncodedData.slice(0, -64);
          const genesisHash = `0x${hexEncodedData.substr(-64)}`;
          const rawPayload = hexToU8a(hexPayload);
          data.data["genesisHash"] = genesisHash;
          const isOversized = rawPayload.length > 256;
          const network = SUBSTRATE_NETWORK_LIST[genesisHash];
          if (!network) {
            throw new Error(`Signer does not currently support a chain with genesis hash: ${genesisHash}`);
          }

          switch (secondByte) {
            case "00": // sign mortal extrinsic
            case "02": // sign immortal extrinsic
              data["action"] = isOversized ? "signData" : "signTransaction";
              data["oversized"] = isOversized;
              data["isHash"] = isOversized;
              const [offset] = compactFromU8a(rawPayload);
              const payload = rawPayload.subarray(offset);
              // data.data.data = isOversized
              // 	? await blake2b(u8aToHex(payload, -1, false))
              // 	: rawPayload;
              data.data["data"] = rawPayload; // ignore oversized data for now
              data.data["account"] = encodeAddress(publicKeyAsBytes, network.prefix); // encode to the prefix;

              break;
            case "01": // data is a hash
              data["action"] = "signData";
              data["oversized"] = false;
              data["isHash"] = true;
              data.data["data"] = hexPayload;
              data.data["account"] = encodeAddress(publicKeyAsBytes, network.prefix); // default to Kusama
              break;
          }
        } catch (e) {
          throw new Error("Error: something went wrong decoding the Substrate UOS payload: " + uosAfterFrames);
        }
        return data;
      }
      default:
        throw new Error("Error: Payload is not formatted correctly: " + bytes);
    }
  } catch (e) {
    throw new Error("we cannot handle the payload: " + bytes);
  }
}

function _isMultipartData(parsedData: any) {
  const hasMultiFrames = parsedData.frameCount !== undefined && parsedData.frameCount > 1;
  return parsedData.isMultipart || hasMultiFrames;
}

async function _setPartData(currentFrame: number, frameCount: number, partData: string) {
  // set it once only
  if (!signer.totalFrameCount) {
    const newArray = new Array(frameCount).fill(null);
    signer.multipartData = newArray;
    signer.totalFrameCount = frameCount;
  }
  const { completedFramesCount, multipartComplete, multipartData, totalFrameCount } = signer;
  const partDataAsBytes = new Uint8Array(partData.length / 2);

  for (let i = 0; i < partDataAsBytes.length; i++) {
    partDataAsBytes[i] = parseInt(partData.substr(i * 2, 2), 16);
  }

  if (currentFrame === 0 && (partDataAsBytes[0] === new Uint8Array([0x00])[0] || partDataAsBytes[0] === new Uint8Array([0x7b])[0])) {
    // part_data for frame 0 MUST NOT begin with byte 00 or byte 7B.
    throw new Error("Error decoding invalid part data.");
  }
  if (completedFramesCount < totalFrameCount) {
    // we haven't filled all the frames yet
    const nextDataState = multipartData;
    nextDataState[currentFrame] = partDataAsBytes;

    const nextMissedFrames = nextDataState.reduce((acc, current, index) => {
      if (current === null) acc.push(index + 1);
      return acc;
    }, []);
    const nextCompletedFramesCount = totalFrameCount - nextMissedFrames.length;
    signer.completedFramesCount = nextCompletedFramesCount;
    signer.latestFrame = currentFrame;
    signer.missedFrames = nextMissedFrames;
    signer.multipartData = nextDataState;

    if (totalFrameCount > 0 && nextCompletedFramesCount === totalFrameCount && !multipartComplete) {
      // all the frames are filled
      await _integrateMultiPartData();
    }
  }
}

async function _integrateMultiPartData() {
  const { multipartData, totalFrameCount } = signer;

  // concatenate all the parts into one binary blob
  let concatMultipartData = multipartData.reduce((acc, part) => {
    if (part === null) throw new Error("part data is not completed");
    const c = new Uint8Array(acc.length + part.length);
    c.set(acc);
    c.set(part, acc.length);
    return c;
  }, new Uint8Array(0));

  // unshift the frame info
  const frameInfo = u8aConcat(MULTIPART, _encodeNumber(totalFrameCount), _encodeNumber(0));
  concatMultipartData = u8aConcat(frameInfo, concatMultipartData);

  signer.multipartComplete = true;
  // handle the binary blob as a single UOS payload
  await _setParsedData(concatMultipartData, true);
}

function _encodeNumber(value: number) {
  return new Uint8Array([value >> 8, value & 0xff]);
}

async function _setParsedData(strippedData, multipartComplete = false) {
  const parsedData = await _constructDataFromBytes(strippedData, multipartComplete);
  if (_isMultipartData(parsedData)) {
    await _setPartData(parsedData["currentFrame"], parsedData["frameCount"], parsedData["partData"]);
    return;
  }

  signer.unsignedData = parsedData;
}

export async function parseQrCode(rawData: string) {
  signer = {} as QRSigner;
  try {
    const strippedData = _rawDataToU8A(rawData);
    await _setParsedData(strippedData, false);
    return { signer: signer.unsignedData.data.account };
  } catch (err) {
    return { error: err.message };
  }
}

export function getSigner() {
  return signer;
}

const CMD_HASH = 1;
const CMD_MORTAL = 2;

export function makeTx(api: ApiPromise, txInfo: any, paramList: any[], ss58: number) {
  return new Promise((resolve) => {
    const signer = txInfo.proxy ? encodeAddress(hexToU8a(txInfo.proxy.pubKey), ss58) : txInfo.sender.address;
    api.derive.tx
      .signingInfo(signer)
      .then(async ({ header, mortalLength, nonce }) => {
        let tx: SubmittableExtrinsic<"promise">;
        // wrap tx with council.propose for treasury propose
        if (txInfo.txName == "treasury.approveProposal") {
          tx = await gov.makeTreasuryProposalSubmission(api, paramList[0], false);
        } else if (txInfo.txName == "treasury.rejectProposal") {
          tx = await gov.makeTreasuryProposalSubmission(api, paramList[0], true);
        } else {
          tx = api.tx[txInfo.module][txInfo.call](...paramList);
        }
        // wrap tx with recovery.asRecovered for proxy tx
        if (txInfo.proxy) {
          tx = api.tx.recovery.asRecovered(txInfo.sender.address, tx);
        }
        const signerPayload = api.registry.createType("SignerPayload", {
          address: signer,
          blockHash: header.hash,
          blockNumber: header ? header.number : 0,
          era: api.registry.createType("ExtrinsicEra", {
            current: header.number,
            period: mortalLength,
          }),
          genesisHash: api.genesisHash,
          method: tx.method,
          nonce,
          signedExtensions: ["CheckNonce"],
          tip: txInfo.tip,
          runtimeVersion: {
            specVersion: api.runtimeVersion.specVersion,
            transactionVersion: api.runtimeVersion.transactionVersion,
          },
          version: api.extrinsicVersion,
        });
        const payload = signerPayload.toPayload();

        // limit size of the transaction
        const qrIsHashed = payload.method.length > 5000;
        const wrapper = api.registry.createType("ExtrinsicPayload", payload, {
          version: payload.version,
        });
        const qrPayload = qrIsHashed ? blake2AsU8a(wrapper.toU8a(true)) : wrapper.toU8a();
        // cache this submittableExtrinsic
        submittable = {
          tx,
          payload: signerPayload.toPayload(),
        };
        // submittable.tx = tx;
        // submittable.payload = signerPayload.toPayload();
        resolve({
          qrAddress: payload.address,
          qrIsHashed,
          qrPayload: _createFrames(_createSignPayload(payload.address, qrIsHashed ? CMD_HASH : CMD_MORTAL, qrPayload, api.genesisHash))[0],
        });
      })
      .catch((err) => resolve({ error: err.message }));
  });
}

const SUBSTRATE_ID = new Uint8Array([0x53]);
const CRYPTO_SR25519 = new Uint8Array([0x01]);
const FRAME_SIZE = 1024;

function _createSignPayload(address: string, cmd: number, payload: Uint8Array | any, genesisHash: Uint8Array | any) {
  return u8aConcat(SUBSTRATE_ID, CRYPTO_SR25519, new Uint8Array([cmd]), decodeAddress(address), u8aToU8a(payload), u8aToU8a(genesisHash));
}

function _createFrames(input: Uint8Array) {
  const frames = [];
  let idx = 0;
  while (idx < input.length) {
    frames.push(input.subarray(idx, idx + FRAME_SIZE));
    idx += FRAME_SIZE;
  }
  return frames.map((frame, index) => u8aConcat(MULTIPART, _encodeNumber(frames.length), _encodeNumber(index), frame));
}

export function getSubmittable() {
  return submittable;
}
