package com.digitalvotingpass.passportconnection;

import com.digitalvotingpass.utilities.Util;
import com.google.common.primitives.Bytes;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.TransactionOutput;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.PublicKey;
import java.util.Arrays;

public class PassportTransactionFormatter {

    private Address destination;
    private TransactionOutput utxo;
    private byte[] data;

    /**
     * Class builds the bytes needed for a transaction
     */
    public PassportTransactionFormatter(TransactionOutput utxo, Address destination) {
        setDestinationAddress(destination);
        setUTXO(utxo);
    }

    /**
     * Set the address of the public key which can spend this output.
     * @param destination
     */
    public void setDestinationAddress(Address destination) {
        this.destination = destination;
    }

    /**
     * Set the UTXO we would like to spend.
     * @param utxo
     */
    public void setUTXO(TransactionOutput utxo) {
        this.utxo = utxo;
    }

    /**
     * Builds the raw transaction and signes using the passport.
     * @param pcon
     */
    public byte[] buildAndSign(PublicKey pubKey, PassportConnection pcon) throws Exception {
        byte[][] parts = buildRawTransaction();
        this.data = signRawTransaction(pubKey, parts, pcon);
        return this.data;
    }

    /**
     * Creates a byte array which contains all the element for a valid transaction.
     * Follows the steps in this answer: https://bitcoin.stackexchange.com/a/5241
     * @return byte[] rawTransaction
     */
    public byte[][] buildRawTransaction() {

        // Version
        byte[] step1 = new BigInteger("01000000", 16).toByteArray();

        // Number of outputs
        byte[] step2 = new byte[]{0x01};

        if (utxo.getParentTransactionHash() == null)
            return null;

        // Transaction hash
        byte[] step3 = utxo.getParentTransactionHash().getReversedBytes();

        // Output index
        byte[] step4 = ByteBuffer.allocate(4).putInt(Integer.reverseBytes(utxo.getIndex())).array();

        // Length of scriptsig (scriptpubkey)
        byte[] step5 = new byte[] {(byte) (utxo.getScriptBytes().length & 0xFF)};

        // Scriptpubkey of output we want to redeem
        byte[] step6 = utxo.getScriptBytes();

        // Unused sequence
        byte[] step7 = Util.hexStringToByteArray("FFFFFFFF");

        // Number of outputs in transaction
        byte[] step8 = Util.hexStringToByteArray("01");

        // Spend amount
        byte[] step9 = Util.hexStringToByteArray("0000000000000000");

        // Size of redeem script
        byte[] step10 = new byte[]{(byte) (step6.length & 0xFF)};

        // Redeem script (copies output and replaces address)
        byte[] step11 = step6.clone();
        System.arraycopy(this.destination.getHash160(), 0, step11, 3, 20);

        // Lock time
        byte[] step12 = Util.hexStringToByteArray("00000000");

        // Hash code type
        byte[] step13 = Util.hexStringToByteArray("01000000");

        return new byte[][]{step1, step2, step3, step4, step5, step6, step7, step8, step9,
                step10, step11, step12, step13};
    }

    /**
     * Signs the raw bytes using a travel document.
     * Follows the steps in this answer: https://bitcoin.stackexchange.com/a/5241
     * @return signedRawTransaction
     */
    public byte[] signRawTransaction(PublicKey pubkey, byte[][] parts, PassportConnection pcon) throws Exception {
        byte[] rawTransaction = Bytes.concat(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5],
                parts[6], parts[7], parts[8], parts[9], parts[10], parts[11], parts[12]);

        // Double hash transaction
        byte[] step14 = Sha256Hash.hash(Sha256Hash.hash(rawTransaction));

        // Generate signature and get publickey
        byte[] multiSignature = new byte[320];
        byte[] hashPart;

        for (int i = 0; i < 4; i++) {
            hashPart = Arrays.copyOfRange(step14, i * 8, i * 8 + 8);
            System.arraycopy(pcon.signData(hashPart), 0, multiSignature, i * 80, 80);
        }

        byte[] signatureLength = Util.hexStringToByteArray("fd97014d4101");
        byte[] hashCodeType = Util.hexStringToByteArray("01");
        byte[] publicKeyASN = pubkey.getEncoded();

        byte[] publicKey = new byte[81];
        System.arraycopy(publicKeyASN, publicKeyASN.length-81, publicKey, 0, 81);

        byte[] publickeyLength = Util.hexStringToByteArray("4c51");

        // Set signature and pubkey in format
        byte[] step16 = Bytes.concat(signatureLength, multiSignature, hashCodeType, publickeyLength, publicKey);

        // Update transaction with signature and remove hash code type
        byte[] step19 = Bytes.concat(parts[0], parts[1], parts[2], parts[3], step16, parts[6],
                parts[7], parts[8], parts[9], parts[10], parts[11], parts[12]);

        return step19;
    }
}