package com.digitalvotingpass.blockchain;

import android.content.Context;
import android.os.Environment;

import com.digitalvotingpass.digitalvotingpass.R;
import com.digitalvotingpass.electionchoice.Election;
import com.digitalvotingpass.passportconnection.PassportConnection;
import com.digitalvotingpass.passportconnection.PassportTransactionFormatter;
import com.digitalvotingpass.transactionhistory.TransactionHistoryItem;
import com.digitalvotingpass.utilities.MultiChainAddressGenerator;
import com.digitalvotingpass.utilities.Util;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.Asset;
import org.bitcoinj.core.AssetBalance;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.MultiChainParams;
import org.bitcoinj.utils.BriefLogFormatter;
import org.bitcoinj.wallet.Wallet;

import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class BlockChain {
    public static final String PEER_IP = "188.226.149.56";
    private static BlockChain instance;
    private WalletAppKit kit;
    private Context context;
    private ProgressTracker progressTracker;

    private InetAddress peeraddr;
    private long addressChecksum = 0xcc350cafL;
    private String[] version = {"00", "62", "8f", "ed"};
    final NetworkParameters params = MultiChainParams.get(
            "00d7fa1a62c5f1eadd434b9f7a8a657a42bd895f160511af6de2d2cd690319b8",
            "01000000000000000000000000000000000000000000000000000000000000000000000059c075b5dd26a328e185333ce1464b7279d476fbe901c38a003e694906e01c073b633559ffff0020ae0000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1704ffff002001040f4d756c7469436861696e20766f7465ffffffff0200000000000000002f76a91474f585ec0e5f452a80af1e059b9d5079ec501d5588ac1473706b703731000000000000ffffffff3b633559750000000000000000131073706b6e0200040101000104726f6f74756a00000000",
            6799,
            Integer.parseInt(Arrays.toString(version).replaceAll(", |\\[|\\]", ""), 16),
            addressChecksum,
            0xf5dec1feL
    );
    private Address masterAddress = Address.fromBase58(params, "1GoqgbPZUV2yuPZXohtAvB2NZbjcew8Rk93mMn");

    private BlockChain(Context ctx) {
        this.context = ctx;
        try {
            peeraddr = InetAddress.getByName(PEER_IP);
            progressTracker = new ProgressTracker();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    public static synchronized BlockChain getInstance(Context ctx) throws Exception {
        if (instance == null) {
            if (ctx == null) throw new Exception("Context cannot be null on first call");
            instance = new BlockChain(ctx);
        }
        return instance;
    }

    /**
     * Add a listener.
     * @param listener The listener.
     */
    public void addListener(BlockchainCallBackListener listener) {
        progressTracker.addListener(listener);
    }

    /**
     * Remove a listener.
     * @param listener a listener.
     */
    public void removeListener(BlockchainCallBackListener listener) {
        progressTracker.removeListener(listener);
    }

    public void startDownload() {
        BriefLogFormatter.init();
        String filePrefix = "voting-wallet";
        File walletFile = new File(Environment.getExternalStorageDirectory() + "/" + Util.FOLDER_DIGITAL_VOTING_PASS);
        if (!walletFile.exists()) {
            walletFile.mkdirs();
        }
        kit = new WalletAppKit(params, walletFile, filePrefix);

        //set the observer
        kit.setDownloadListener(progressTracker);

        kit.setBlockingStartup(false);

        PeerAddress peer = new PeerAddress(params, peeraddr);
        kit.setPeerNodes(peer);
        kit.startAsync();
    }

    public void disconnect() {
        kit.stopAsync();
    }

    /**
     * Gets the amount of voting tokens associated with the given public key.
     * @param pubKey - The Public Key read from the ID of the voter
     * @param mcAsset - The asset (election) that is chosen at app start-up.
     * @return - The amount of voting tokens available
     */
    public int getVotingPassAmount(PublicKey pubKey, Asset mcAsset) {
        if(pubKey != null && mcAsset != null) {
            Address mcAddress = Address.fromBase58(params, MultiChainAddressGenerator.getPublicAddress(version, Long.toString(addressChecksum), pubKey));
            return (int) kit.wallet().getAssetBalance(mcAsset, mcAddress).getBalance();
        } else {
            return 0;
        }
    }

    public ArrayList<Asset> getAssets() {
        return kit.wallet().getAvailableAssets();
    }

    public boolean assetExists(Asset asset) {
        if(asset != null) {
            for (Asset a : getAssets()) {
                if (a.getName().equals(asset.getName())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Get the balance of a public key based on the information on the blockchain.
     * @param pubKey
     * @return
     */
    public AssetBalance getVotingPassBalance(PublicKey pubKey, Asset asset) {
        Address address = Address.fromBase58(params, MultiChainAddressGenerator.getPublicAddress(version, Long.toString(addressChecksum), pubKey));
        return kit.wallet().getAssetBalance(asset, address);
    }

    /**
     * Spends all outputs in this balance to the master address.
     * @param balance
     * @param pcon
     */

    public ArrayList<byte[]> getSpendUtxoTransactions(PublicKey pubKey, AssetBalance balance, PassportConnection pcon) throws Exception {
        ArrayList<byte[]> transactions = new ArrayList<>();
        for (TransactionOutput utxo : balance) {
            transactions.add(utxoToSignedTransaction(pubKey, utxo, masterAddress, pcon));
        }
        return transactions;
    }

    /**
     * Create a new transaction, signes it with the travel document.
     * @param utxo
     * @param destination
     * @param pcon
     */
    public byte[] utxoToSignedTransaction(PublicKey pubKey, TransactionOutput utxo, Address destination, PassportConnection pcon) throws Exception {
        return new PassportTransactionFormatter(utxo, destination)
                .buildAndSign(pubKey, pcon);
    }


    /**
     * Broadcasts the list of signed transactions.
     * @param transactionsRaw transactions in raw byte[] format
     */
    public ArrayList<Transaction> broadcastTransactions(ArrayList<byte[]> transactionsRaw) {
        ArrayList<Transaction> transactions = new ArrayList<>();
        for (byte[] transactionRaw : transactionsRaw) {
            final Wallet.SendResult result = new Wallet.SendResult();
            result.tx = new Transaction(params, transactionRaw);

            result.broadcast = kit.peerGroup().broadcastTransaction(result.tx);
            result.broadcastComplete = result.broadcast.future();

            result.broadcastComplete.addListener(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Asset spent! txid: " + result.tx.getHashAsString());
                }
            }, MoreExecutors.directExecutor());

            transactions.add(result.tx);
        }
        return transactions;
    }


    /**
     * Returns the address corresponding to the pubkey.
     * @param pubKey
     * @return Address
     */
    public Address getAddress(PublicKey pubKey) {
        return Address.fromBase58(params, MultiChainAddressGenerator.getPublicAddress(version, Long.toString(addressChecksum), pubKey));
    }

    /**
     * Load transactions that involve the given public key, either incomming or outgoing.
     * @param pubKey PublicKey comming from epassport
     * @param assetFilter Asset for which transactions needs to be checked.
     * @return List containing interesting transactions.
     */
    public List<TransactionHistoryItem> getMyTransactions(PublicKey pubKey, Asset assetFilter) {
        List<TransactionHistoryItem> result = new ArrayList<>();
        Address address = Address.fromBase58(params, MultiChainAddressGenerator.getPublicAddress(version, Long.toString(addressChecksum), pubKey));
        List<Transaction> ts = kit.wallet().getAssetTransactions(address, assetFilter);

        for (Transaction transaction : ts) {
            for (TransactionOutput o : transaction.getOutputs()) {
                boolean sentToAddr = o.getScriptPubKey().isSentToAddress();
                boolean isReturn = o.getScriptPubKey().isOpReturn();
                if (sentToAddr && !isReturn) {
                    byte[] metaData = o.getScriptPubKey().getChunks().get(5).data;
                    assert metaData != null;
                    byte[] quantity = Arrays.copyOfRange(metaData, 20, 28);
                    int amount = ByteBuffer.wrap(quantity).order(ByteOrder.LITTLE_ENDIAN).getInt();
                    Address toAddress = o.getScriptPubKey().getToAddress(this.params);
                    Address fromAddress = transaction.getInput(0).getFromAddress();
                    Date date = transaction.getUpdateTime();
                    if (!toAddress.equals(fromAddress)) {
                        TransactionHistoryItem item = createTransactionHistoryItem(address, fromAddress, toAddress, date, assetFilter, amount);
                        result.add(item);
                    }
                }
            }
        }
        return result;
    }

    private TransactionHistoryItem createTransactionHistoryItem(Address myAddress, Address fromAddress, Address toAddress, Date date, Asset assetFilter, int amount) {
        String titleFormat = "";
        String detailString = "";
        if (myAddress.equals(fromAddress)) {
            titleFormat = context.getString(R.string.transaction_sent_item_format_title);
            String detailFormat = context.getString(R.string.transaction_sent_item_format_detail);
            detailString = String.format(detailFormat, translateAddress(toAddress.toString()));
        } else if (myAddress.equals(toAddress)) {
            titleFormat = context.getString(R.string.transaction_received_item_format_title);
            String detailFormat = context.getString(R.string.transaction_received_item_format_detail);
            detailString  = String.format(detailFormat, translateAddress(fromAddress.toString()));
        }
        return new TransactionHistoryItem(
                String.format(titleFormat,
                        amount,
                        Election.parseElection(assetFilter, context).getKind(),
                        Election.parseElection(assetFilter, context).getPlace()),
                date, detailString);
    }

    /**
     * Translate a MultiChain address to a meaningful String value if such a value is defined for
     * that address in strings.xml
     * @param address String value of MultiChain address
     * @return String containing defined mapped value or {@code address} if no mapping was found.
     */
    public String translateAddress(String address) {
        Map<String, String> addresses = Util.getKeyValueFromStringArray(context);
        if (addresses.containsKey(address))
            return addresses.get(address);
        else
            return address;
    }
}