package org.iota.jota;

import org.apache.commons.lang3.StringUtils;
import org.iota.jota.builder.AddressRequest;
import org.iota.jota.builder.ApiBuilder;
import org.iota.jota.dto.response.BroadcastTransactionsResponse;
import org.iota.jota.dto.response.FindTransactionResponse;
import org.iota.jota.dto.response.GetAccountDataResponse;
import org.iota.jota.dto.response.GetAttachToTangleResponse;
import org.iota.jota.dto.response.GetBalancesAndFormatResponse;
import org.iota.jota.dto.response.GetBalancesResponse;
import org.iota.jota.dto.response.GetBundleResponse;
import org.iota.jota.dto.response.GetInclusionStateResponse;
import org.iota.jota.dto.response.GetNewAddressResponse;
import org.iota.jota.dto.response.GetTransactionsToApproveResponse;
import org.iota.jota.dto.response.GetTransferResponse;
import org.iota.jota.dto.response.GetTrytesResponse;
import org.iota.jota.dto.response.ReplayBundleResponse;
import org.iota.jota.dto.response.SendTransferResponse;
import org.iota.jota.dto.response.WereAddressesSpentFromResponse;
import org.iota.jota.error.ArgumentException;
import org.iota.jota.error.BaseException;
import org.iota.jota.error.NotPromotableException;
import org.iota.jota.model.Bundle;
import org.iota.jota.model.Input;
import org.iota.jota.model.Transaction;
import org.iota.jota.model.Transfer;
import org.iota.jota.pow.SpongeFactory;
import org.iota.jota.utils.BundleValidator;
import org.iota.jota.utils.Checksum;
import org.iota.jota.utils.Constants;
import org.iota.jota.utils.InputValidator;
import org.iota.jota.utils.IotaAPIUtils;
import org.iota.jota.utils.Parallel;
import org.iota.jota.utils.StopWatch;
import org.iota.mddoclet.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.toList;

/**
 * IotaAPI Builder. Usage:
 * <p>
 * {@code IotaAPI api = new IotaAPI.Builder()}
 * {@code .protocol("http")}
 * {@code .nodeAddress("localhost")}
 * {@code .port(12345)}
 * {@code .build();}
 * <p>
 * {@code GetNodeInfoResponse response = api.getNodeInfo();}
 */
public class IotaAPI extends IotaAPICore {

    private static final Logger log = LoggerFactory.getLogger(IotaAPI.class);

    protected IotaAPI(ApiOptions options) {
        super(options);
    }

    protected IotaAPI(Builder builder) {
        super(new ApiOptions(builder));
    }

    /**
     * Generates new addresses, meaning addresses which were not spend from, according to the connected node.
     * Stops when {@link AddressRequest#getAmount()} of unspent addresses are found, starting from {@link AddressRequest#getIndex()}
     * <p>
     * If {@link AddressRequest#getAmount()} is set to 0, we will generate until the first unspent address is found, and stop.
     *
     * @param addressRequest {@link AddressRequest}
     * @return {@link GetNewAddressResponse}
     */
    @Document
    public GetNewAddressResponse generateNewAddresses(AddressRequest addressRequest) {
        StopWatch stopWatch = new StopWatch();
        List<String> addresses = new ArrayList<>();
        String seed = addressRequest.getSeed();
        int securityLevel = addressRequest.getSecurityLevel();
        int index = addressRequest.getIndex();
        int amount = addressRequest.getAmount();
        boolean checksum = addressRequest.isChecksum();
        boolean addSpendAddresses = addressRequest.isAddSpendAddresses();

        if (amount == 0) {
            String unusedAddress = getFirstUnusedAddress(seed, securityLevel, index, checksum);
            addresses.add(unusedAddress);
        } else {
            List<String> mAddresses = getAddresses(seed, securityLevel, index, checksum, amount, addSpendAddresses);
            addresses.addAll(mAddresses);
        }

        return GetNewAddressResponse.create(addresses, stopWatch.getElapsedTimeMili());
    }

    private boolean isAddressSpent(String newAddress, boolean checksum) {
        final String address = checksum ? newAddress : Checksum.addChecksum(newAddress);
        final FindTransactionResponse response = findTransactionsByAddresses(address);

        if (response.getHashes().length == 0) {
            Boolean state = checkWereAddressSpentFrom(address);
            return state;
        }

        return true;
    }

    private String getFirstUnusedAddress(String seed, int securityLevel, int index, boolean checksum) {
        int mIndex = index;
        do {
            final String newAddress = IotaAPIUtils.newAddress(seed, securityLevel, mIndex, checksum, getCurl());
            if (!isAddressSpent(newAddress, checksum)) {
                return newAddress;
            }

            mIndex++;
        } while (true);
    }

    private List<String> getAddresses(String seed, int securityLevel, int index, boolean checksum, int amount,
                                      boolean addSpendAddresses) {
        List<String> addresses = new ArrayList<>();

        for (int i = index, numUnspentFound = 0; numUnspentFound < amount; i++) {

            final String newAddress = IotaAPIUtils.newAddress(seed, securityLevel, i, checksum, getCurl());
            if (!isAddressSpent(newAddress, checksum)) {
                addresses.add(newAddress);
                numUnspentFound++;
            } else if (addSpendAddresses) {
                addresses.add(newAddress);
            }
        }

        return addresses;
    }

    /**
     * Generates <code>amount</code> of addresses, starting from <code>index</code>
     * This does not mean that these addresses are safe to use (unspent)
     *
     * @param addressRequest {@link AddressRequest}
     * @return {@link GetNewAddressResponse}
     * @throws ArgumentException is thrown when the specified input is not valid.
     */
    @Document
    public GetNewAddressResponse getAddressesUnchecked(AddressRequest addressRequest) throws ArgumentException {
        StopWatch stopWatch = new StopWatch();

        List<String> allAddresses = IntStream
                .range(addressRequest.getIndex(), addressRequest.getIndex() + addressRequest.getAmount())
                .mapToObj(mIndex -> IotaAPIUtils.newAddress(addressRequest, mIndex, getCurl()))
                .collect(toList());

        return GetNewAddressResponse.create(allAddresses, stopWatch.getElapsedTimeMili());
    }

    /**
     * Finds all the bundles for all the addresses based on this seed and security.
     *
     * @param seed            Tryte-encoded seed. It should be noted that this seed is not transferred.
     * @param security        Security level to be used for the private key / address. Can be 1, 2 or 3.
     * @param start           Starting key index, must be at least 0.
     * @param end             Ending key index, must be bigger then <tt>start</tt>
     * @param inclusionStates If <code>true</code>, it also gets the inclusion states of the transfers.
     * @return {@link GetTransferResponse}
     * @throws ArgumentException     when <tt>start</tt> and <tt>end</tt> are more then 500 apart
     * @throws ArgumentException     Invalid security index
     * @throws IllegalStateException When the seed is invalid
     */
    @Document
    public GetTransferResponse getTransfers(String seed, int security, Integer start, Integer end, Boolean inclusionStates) throws ArgumentException {

        // validate seed
        if (!InputValidator.isValidSeed(seed)) {
            throw new IllegalStateException(Constants.INVALID_SEED_INPUT_ERROR);
        }

        if (start < 0 || start > end || end > (start + 500)) {
            throw new ArgumentException(Constants.INVALID_INPUT_ERROR);
        }

        if (!InputValidator.isValidSecurityLevel(security)) {
            throw new ArgumentException(Constants.INVALID_SECURITY_LEVEL_INPUT_ERROR);
        }

        StopWatch stopWatch = new StopWatch();

        AddressRequest addressRequest = new AddressRequest.Builder(seed, security)
                .index(start)
                .checksum(true)
                .amount(end)
                .addSpendAddresses(true)
                .build();

        GetNewAddressResponse gnr = generateNewAddresses(addressRequest);
        if (gnr != null && gnr.getAddresses() != null) {
            Bundle[] bundles = bundlesFromAddresses(inclusionStates, gnr.getAddresses().toArray(new String[0]));
            return GetTransferResponse.create(bundles, stopWatch.getElapsedTimeMili());
        }
        return GetTransferResponse.create(new Bundle[]{}, stopWatch.getElapsedTimeMili());
    }

    /**
     * Internal function to get the formatted bundles of a list of addresses.
     *
     * @param addresses       If <code>true</code>, it also gets the inclusion state of each bundle.
     * @param inclusionStates Array of addresses.
     * @return All the transaction bundles for the addresses, or <code>null</code> when our thread gets interrupted
     * @throws ArgumentException     When the addresses are invalid
     * @throws IllegalStateException When inclusion state/confirmed could not be determined (<tt>null</tt> returned)
     */
    @Document
    public Bundle[] bundlesFromAddresses(Boolean inclusionStates, String... addresses) throws ArgumentException {

        List<Transaction> trxs = findTransactionObjectsByAddresses(addresses);
        // set of tail transactions
        List<String> tailTransactions = new ArrayList<>();
        List<String> nonTailBundleHashes = new ArrayList<>();

        for (Transaction trx : trxs) {
            // Sort tail and nonTails
            if (trx.getCurrentIndex() == 0) {
                tailTransactions.add(trx.getHash());
            } else {
                if (nonTailBundleHashes.indexOf(trx.getBundle()) == -1) {
                    nonTailBundleHashes.add(trx.getBundle());
                }
            }
        }

        List<Transaction> bundleObjects = findTransactionObjectsByBundle(nonTailBundleHashes.toArray(new String[0]));
        for (Transaction trx : bundleObjects) {
            // Sort tail and nonTails
            if (trx.getCurrentIndex() == 0) {
                if (tailTransactions.indexOf(trx.getHash()) == -1) {
                    tailTransactions.add(trx.getHash());
                }
            }
        }

        final List<Bundle> finalBundles = new ArrayList<>();
        final String[] tailTxArray = tailTransactions.toArray(new String[0]);

        // If inclusionStates, get the confirmation status
        // of the tail transactions, and thus the bundles
        GetInclusionStateResponse gisr = null;
        if (tailTxArray.length != 0 && inclusionStates) {
            gisr = getLatestInclusion(tailTxArray);
            if (gisr == null || gisr.getStates() == null || gisr.getStates().length == 0) {
                throw new IllegalStateException(Constants.GET_INCLUSION_STATE_RESPONSE_ERROR);
            }
        }
        final GetInclusionStateResponse finalInclusionStates = gisr;
        try {
            Parallel.of(Arrays.asList(tailTxArray), tailTx -> {
                try {
                    GetBundleResponse bundleResponse = getBundle(tailTx);
                    Bundle gbr = new Bundle(bundleResponse.getTransactions(), bundleResponse.getTransactions().size());
                    if (gbr.getTransactions() != null) {
                        if (inclusionStates) {
                            boolean thisInclusion = false;
                            if (finalInclusionStates != null) {
                                thisInclusion = finalInclusionStates.getStates()[Arrays.asList(tailTxArray).indexOf(tailTx)];
                            }
                            for (Transaction t : gbr.getTransactions()) {
                                t.setPersistence(thisInclusion);
                            }
                        }
                        finalBundles.add(gbr);
                    }
                    // If error returned from getBundle, simply ignore it because the bundle was most likely incorrect
                } catch (ArgumentException e) {
                    if (!Thread.interrupted()) {
                        log.warn(Constants.GET_BUNDLE_RESPONSE_ERROR);
                    }
                }
            });
        } catch (InterruptedException e) {
            return null;
        }

        Collections.sort(finalBundles);
        Bundle[] returnValue = new Bundle[finalBundles.size()];
        for (int i = 0; i < finalBundles.size(); i++) {
            returnValue[i] = new Bundle(finalBundles.get(i).getTransactions(), finalBundles.get(i).getTransactions().size());
        }
        return returnValue;
    }

    /**
     * Wrapper method: stores and broadcasts the specified trytes.
     *
     * @param trytes The trytes.
     * @return {@link BroadcastTransactionsResponse}
     * @throws ArgumentException is thrown when the specified <tt>trytes</tt> is not valid.
     * @throws ArgumentException If {@link #storeTransactions(String...)} fails
     * @see #storeTransactions(String...)
     * @see #broadcastTransactions(String...)
     */
    @Document
    public BroadcastTransactionsResponse storeAndBroadcast(String... trytes) throws ArgumentException {

        if (!InputValidator.isArrayOfAttachedTrytes(trytes)) {
            throw new ArgumentException(Constants.INVALID_TRYTES_INPUT_ERROR);
        }

        try {
            storeTransactions(trytes);
        } catch (Exception e) {
            throw new ArgumentException(e.getMessage(), e);
        }
        return broadcastTransactions(trytes);
    }

    /**
     * Wrapper method: Gets transactions to approve, attaches to Tangle, broadcasts and stores.
     *
     * @param trytes             The transaction trytes
     * @param depth              The depth for getting transactions to approve
     * @param minWeightMagnitude The minimum weight magnitude for doing proof of work
     * @param reference          Hash of transaction to start random-walk from
     *                           This is used to make sure the tips returned reference a given transaction in their past.
     *                           This can be <tt>null</tt>, in that case the latest milestone is used as a reference.
     * @return Sent {@link Transaction} objects.
     * @throws ArgumentException is thrown when invalid trytes is provided.
     * @see #broadcastTransactions(String...)
     * @see #attachToTangle(String, String, Integer, String...)
     * @see #storeAndBroadcast(String...)
     */
    @Document
    public List<Transaction> sendTrytes(String[] trytes, int depth, int minWeightMagnitude, String reference) throws ArgumentException {
        GetTransactionsToApproveResponse txs = getTransactionsToApprove(depth, reference);
        // attach to tangle - do pow
        GetAttachToTangleResponse res = attachToTangle(txs.getTrunkTransaction(), txs.getBranchTransaction(), minWeightMagnitude, trytes);
        try {
            storeAndBroadcast(res.getTrytes());
        } catch (ArgumentException e) {
            return new ArrayList<>();
        }

        final List<Transaction> trx = new ArrayList<>();

        for (String tryte : res.getTrytes()) {
            trx.add(new Transaction(tryte, SpongeFactory.create(SpongeFactory.Mode.CURL_P81)));
        }

        return trx;
    }

    /**
     * Wrapper function: get trytes and turns into {@link Transaction} objects.
     * Gets the trytes and transaction object from a list of transaction hashes.
     *
     * @param hashes The hashes of the transactions we want to get the transactions from
     * @return {@link Transaction} objects.
     * @throws ArgumentException if hashes is not a valid array of hashes
     * @see #getTrytes(String...)
     **/
    @Document
    public List<Transaction> findTransactionsObjectsByHashes(String... hashes) throws ArgumentException {

        if (!InputValidator.isArrayOfHashes(hashes)) {
            throw new IllegalStateException(Constants.INVALID_HASHES_INPUT_ERROR);
        }

        final GetTrytesResponse trytesResponse = getTrytes(hashes);

        final List<Transaction> trxs = new ArrayList<>();

        for (final String tryte : trytesResponse.getTrytes()) {
            trxs.add(new Transaction(tryte, SpongeFactory.create(SpongeFactory.Mode.CURL_P81)));
        }
        return trxs;
    }

    /**
     * Wrapper function: Finds transactions, gets trytes and turns it into {@link Transaction} objects.
     *
     * @param addresses The addresses we should get the transactions for, must contain checksums
     * @return a list of {@link Transaction} objects.
     * @throws ArgumentException if addresses is not a valid array of hashes
     * @see #findTransactionsByAddresses(String...)
     * @see #findTransactionsObjectsByHashes
     **/
    @Document
    public List<Transaction> findTransactionObjectsByAddresses(String[] addresses) throws ArgumentException {
        FindTransactionResponse ftr = findTransactionsByAddresses(addresses);
        if (ftr == null || ftr.getHashes() == null) {
            return new ArrayList<>();
        }

        // get the transaction objects of the transactions
        return findTransactionsObjectsByHashes(ftr.getHashes());
    }

    /**
     * Wrapper function: Finds transactions, gets trytes and turns it into {@link Transaction} objects.
     *
     * @param tags The tags the transactions we search for have
     * @return a list of {@link Transaction} objects.
     * @throws ArgumentException if any of the tags are not a valid
     * @see #findTransactionsByTags(String...)
     * @see #findTransactionsObjectsByHashes
     **/
    @Document
    public List<Transaction> findTransactionObjectsByTag(String... tags) throws ArgumentException {
        FindTransactionResponse ftr = findTransactionsByTags(tags);
        if (ftr == null || ftr.getHashes() == null) {
            return new ArrayList<>();
        }

        // get the transaction objects of the transactions
        return findTransactionsObjectsByHashes(ftr.getHashes());
    }

    /**
     * Wrapper function: Finds transactions, gets trytes and turns it into {@link Transaction} objects.
     *
     * @param approvees The transaction hashes of which we want to approvers
     * @return {@link Transaction} objects.
     * @throws ArgumentException if addresses is not a valid array of hashes
     * @see #findTransactionsByApprovees
     **/
    @Document
    public List<Transaction> findTransactionObjectsByApprovees(String... approvees) throws ArgumentException {
        FindTransactionResponse ftr = findTransactionsByApprovees(approvees);
        if (ftr == null || ftr.getHashes() == null) {
            return new ArrayList<>();
        }

        // get the transaction objects of the transactions
        return findTransactionsObjectsByHashes(ftr.getHashes());
    }

    /**
     * Wrapper function: Finds transactions, gets trytes and turns it into {@link Transaction} objects.
     *
     * @param bundles The bundles for which we will get all its transactions
     * @return {@link Transaction} objects.
     * @throws ArgumentException if addresses is not a valid array of hashes
     * @see #findTransactionsByBundles
     **/
    @Document
    public List<Transaction> findTransactionObjectsByBundle(String... bundles) throws ArgumentException {
        FindTransactionResponse ftr = findTransactionsByBundles(bundles);
        if (ftr == null || ftr.getHashes() == null) {
            return new ArrayList<>();
        }

        // get the transaction objects of the transactions
        return findTransactionsObjectsByHashes(ftr.getHashes());
    }

    /**
     * Prepares transfer by generating bundle, finding and signing inputs.
     *
     * @param seed           The tryte-encoded seed. It should be noted that this seed is not transferred.
     *                       Only required when we are preparing a value transfer
     * @param security       Security level to be used for the private key / address. Can be 1, 2 or 3.
     * @param transfers      List of transfer objects.
     *                       If the total value of the transfers is 0, no signing is performed.
     * @param remainder      If defined, this address will be used for sending the remainder value (of the inputs) to.
     *                       Otherwise, then next available address is used (if the transfer value is over 0)
     * @param inputs         The inputs used for this transfer
     * @param tips           The starting points we walk back from to find the balance of the addresses, can be <tt>null</tt>
     * @param validateInputs Whether or not to validate the balances of the provided inputs
     *                       If no validation is required
     * @return Returns a list of the trytes of each bundle.
     * @throws ArgumentException     If the seed is invalid
     * @throws ArgumentException     If the security level is wrong.
     * @throws IllegalStateException If the transfers are not all valid
     * @throws IllegalStateException If there is not enough balance in the inputs
     */
    @Document
    public List<String> prepareTransfers(String seed, int security,
                                         List<Transfer> transfers,
                                         String remainder,
                                         List<Input> inputs,
                                         List<Transaction> tips,
                                         boolean validateInputs) throws ArgumentException {
        //validate seed
        if ((!InputValidator.isValidSeed(seed))) {
            throw new IllegalStateException(Constants.INVALID_SEED_INPUT_ERROR);
        }

        if (!InputValidator.isValidSecurityLevel(security)) {
            throw new ArgumentException(Constants.INVALID_SECURITY_LEVEL_INPUT_ERROR);
        }

        if (remainder != null && !InputValidator.checkAddress(remainder)) {
            throw new ArgumentException(Constants.INVALID_ADDRESSES_INPUT_ERROR);
        }

        // Input validation of transfers object
        if (!InputValidator.isTransfersCollectionValid(transfers)) {
            throw new ArgumentException(Constants.INVALID_TRANSFERS_INPUT_ERROR);
        }

        if (inputs != null && !InputValidator.areValidInputsList(inputs)) {
            throw new ArgumentException(Constants.INVALID_ADDRESSES_INPUT_ERROR);
        }

        // Create a new bundle
        final Bundle bundle = new Bundle();
        final List<String> signatureFragments = new ArrayList<>();

        long totalValue = 0;
        String tag = "";
        //  Iterate over all transfers, get totalValue
        //  and prepare the signatureFragments, message and tag
        for (final Transfer transfer : transfers) {

            int signatureMessageLength = 1;

            // If message longer than 2187 trytes, increase signatureMessageLength (add 2nd transaction)
            if (transfer.getMessage().length() > Constants.MESSAGE_LENGTH) {

                // Get total length, message / maxLength (2187 trytes)
                signatureMessageLength += Math.floor(transfer.getMessage().length() / Constants.MESSAGE_LENGTH);

                String msgCopy = transfer.getMessage();

                // While there is still a message, copy it
                while (!msgCopy.isEmpty()) {

                    String fragment = StringUtils.substring(msgCopy, 0, Constants.MESSAGE_LENGTH);
                    msgCopy = StringUtils.substring(msgCopy, Constants.MESSAGE_LENGTH, msgCopy.length());

                    // Pad remainder of fragment

                    fragment = StringUtils.rightPad(fragment, Constants.MESSAGE_LENGTH, '9');

                    signatureFragments.add(fragment);
                }
            } else {
                // Else, get single fragment with 2187 of 9's trytes
                String fragment = transfer.getMessage();

                if (transfer.getMessage().length() < Constants.MESSAGE_LENGTH) {
                    fragment = StringUtils.rightPad(fragment, Constants.MESSAGE_LENGTH, '9');
                }
                signatureFragments.add(fragment);
            }

            tag = transfer.getTag();

            // pad for required 27 tryte length
            if (transfer.getTag().length() < Constants.TAG_LENGTH) {
                tag = StringUtils.rightPad(tag, Constants.TAG_LENGTH, '9');
            }


            // get current timestamp in seconds
            long timestamp = (long) Math.floor(Calendar.getInstance().getTimeInMillis() / 1000);

            // Add first entry to the bundle
            bundle.addEntry(signatureMessageLength,
                    Checksum.removeChecksum(transfer.getAddress()),
                    transfer.getValue(),
                    tag, timestamp);
            // Sum up total value
            totalValue += transfer.getValue();
        }

        // Get inputs if we are sending tokens
        if (totalValue != 0) {
            // validate seed
            if ((!InputValidator.isValidSeed(seed))) {
                throw new IllegalStateException(Constants.INVALID_SEED_INPUT_ERROR);
            }

            for (Transfer t : transfers) {
                if (!InputValidator.hasTrailingZeroTrit(t.getAddress())) {
                    throw new ArgumentException(Constants.INVALID_ADDRESSES_INPUT_ERROR);
                }
            }

            //  Case 1: user provided inputs
            //  Validate the inputs by calling getBalances
            if (inputs != null && !inputs.isEmpty()) {

                if (!validateInputs) {
                    return addRemainder(seed, security, inputs, bundle, tag, totalValue, remainder, signatureFragments);
                }

                // Get list if addresses of the provided inputs
                List<String> inputsAddresses = new ArrayList<>();
                for (final Input i : inputs) {
                    inputsAddresses.add(i.getAddress());
                }

                List<String> tipHashes = null;
                if (tips != null) {
                    tipHashes = new ArrayList<>();

                    for (final Transaction tx : tips) {
                        tipHashes.add(tx.getHash());
                    }
                }

                GetBalancesResponse balancesResponse = getBalances(inputsAddresses, tipHashes);
                String[] balances = balancesResponse.getBalances();

                List<Input> confirmedInputs = new ArrayList<>();
                long totalBalance = 0;

                for (int i = 0; i < balances.length; i++) {
                    long thisBalance = Long.parseLong(balances[i]);

                    // If input has balance, add it to confirmedInputs
                    if (thisBalance > 0) {
                        totalBalance += thisBalance;
                        Input inputEl = inputs.get(i);
                        inputEl.setBalance(thisBalance);
                        confirmedInputs.add(inputEl);

                        // if we've already reached the intended input value, break out of loop
                        if (totalBalance >= totalValue) {
                            break;
                        }
                    }

                }

                // Return not enough balance error
                if (totalValue > totalBalance) {
                    throw new IllegalStateException(Constants.NOT_ENOUGH_BALANCE_ERROR);
                }

                return addRemainder(seed, security, confirmedInputs, bundle, tag, totalValue, remainder, signatureFragments);
            }

            //  Case 2: Get inputs deterministically
            //
            //  If no inputs provided, derive the addresses from the seed and
            //  confirm that the inputs exceed the threshold
            else {
                GetBalancesAndFormatResponse newinputs = getInputs(seed, security, 0, 0, totalValue);
                if (newinputs == null || newinputs.getTotalBalance() < totalValue) {
                    throw new IllegalStateException(Constants.NOT_ENOUGH_BALANCE_ERROR);
                }
                // If inputs with enough balance
                return addRemainder(seed, security, newinputs.getInputs(), bundle, tag, totalValue, remainder, signatureFragments);
            }
        } else {

            // If no input required, don't sign and simply finalize the bundle
            bundle.finalize(SpongeFactory.create(SpongeFactory.Mode.KERL));
            bundle.addTrytes(signatureFragments);

            List<Transaction> trxb = bundle.getTransactions();
            List<String> bundleTrytes = new ArrayList<>();

            for (Transaction trx : trxb) {
                bundleTrytes.add(trx.toTrytes());
            }

            Collections.reverse(bundleTrytes);
            return bundleTrytes;
        }
    }

    /**
     * Gets the inputs of a seed
     * If start, end and threshold are 0, checks everything until an address with nothing is found.
     * Addresses are all with checksum appended
     *
     * @param seed      Tryte-encoded seed. It should be noted that this seed is not transferred.
     * @param security  Security level to be used for the private key / address. Can be 1, 2 or 3.
     * @param start     Starting key index, must be at least 0.
     * @param end       Ending key index, must be bigger then <tt>start</tt>, and cant span more than 500 indexes
     * @param threshold Minimum balance required.
     * @param tips      The starting points we walk back from to find the balance of the addresses, can be <tt>null</tt>
     * @return {@link GetBalancesAndFormatResponse}
     * @throws ArgumentException If the seed is invalid
     * @throws ArgumentException If the security level is wrong.
     * @throws ArgumentException when <tt>start</tt> and <tt>end</tt> are more then 500 apart
     * @see #getBalanceAndFormat(List, List, long, int, StopWatch, int)
     **/
    @Document
    public GetBalancesAndFormatResponse getInputs(String seed, int security, int start, int end, long threshold, final String... tips) throws ArgumentException {

        // validate the seed
        if ((!InputValidator.isValidSeed(seed))) {
            throw new IllegalStateException(Constants.INVALID_SEED_INPUT_ERROR);
        }

        if (!InputValidator.isValidSecurityLevel(security)) {
            throw new ArgumentException(Constants.INVALID_SECURITY_LEVEL_INPUT_ERROR);
        }

        // If start value bigger than end, return error
        // or if difference between end and start is bigger than 500 keys
        if (start < 0 || start > end || end > (start + 500)) {
            throw new IllegalStateException(Constants.INVALID_INPUT_ERROR);
        }

        StopWatch stopWatch = new StopWatch();

        List<String> tipsList = tips != null ? Arrays.asList(tips) : null;

        //  Case 1: start and end
        //
        //  If start and end is defined by the user, simply iterate through the keys
        //  and call getBalances
        if (end != 0) {

            List<String> allAddresses = new ArrayList<>();

            for (int i = start; i < end; i++) {
                String address = IotaAPIUtils.newAddress(seed, security, i, true, getCurl());
                allAddresses.add(address);
            }

            return getBalanceAndFormat(allAddresses, tipsList, threshold, start, stopWatch, security);
        }
        //  Case 2: iterate till threshold
        //
        //  Either start from index: 0 or start (if defined) until threshold is reached.
        else {
            List<Input> allInputs = new ArrayList<>();

            boolean thresholdReached = true;
            long currentTotal = 0;

            for (int i = start; thresholdReached; i++) {
                String address = IotaAPIUtils.newAddress(seed, security, i, true, getCurl());

                // Received input, this epoch or previous
                GetBalancesResponse response = getBalances(Collections.singletonList(address), tipsList);
                long balance;
                try {
                    balance = Long.parseLong(response.getBalances()[0]);
                } catch (NumberFormatException e) {
                    throw new ArgumentException(e.getMessage());
                }

                if (balance > 0) {
                    // Is it already spent from?
                    WereAddressesSpentFromResponse wasSpent = this.wereAddressesSpentFrom(address);
                    if (wasSpent.getStates().length > 0 && !wasSpent.getStates()[0]) {
