/*
 * Copyright 2015, 2016 Ross Nicoll.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.libdohj.cate.controller;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.ResourceBundle;

import com.sun.deploy.uitoolkit.impl.fx.HostServicesFactory;
import com.sun.javafx.application.HostServicesDelegate;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ComboBox;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.Node;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.StringConverter;

import com.google.common.util.concurrent.Service;
import javafx.event.EventType;

import org.controlsfx.control.NotificationPane;
import org.libdohj.cate.CATE;
import org.libdohj.cate.util.*;
import org.spongycastle.crypto.params.KeyParameter;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.KeyCrypterException;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;

import org.libdohj.cate.Network;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base window from which the rest of CATE is launched. Lists any active
 * wallets, their status, and options to add new wallets.
 *
 * @author Ross Nicoll
 */
public class MainController {

    private static final int BANNER_DISPLAY_MILLIS = 3000;
    private static final int NETWORK_PUSH_TIMEOUT_MILLIS = 500;

    @FXML // ResourceBundle that was given to the FXMLLoader
    private ResourceBundle resources;

    @FXML
    private MenuItem menuExit;

    @FXML
    private ComboBox<Network> receiveSelector;
    @FXML
    private TextField myAddress;
    @FXML
    private TableView txList;
    @FXML
    private TableColumn<WalletTransaction, String> txNetworkColumn;
    @FXML
    private TableColumn<WalletTransaction, String> txDateColumn;
    @FXML
    private TableColumn<WalletTransaction, String> txAmountColumn;
    @FXML
    private TableColumn<WalletTransaction, String> txMemoColumn;
    @FXML
    private TextField sendAddress;
    @FXML
    private TextField sendAmount;
    @FXML
    private ComboBox<Network> sendSelector;
    @FXML
    private Button sendButton;
    @FXML
    private TableView<Network> walletList;
    @FXML
    private TableColumn<Network, String> networkName;
    @FXML
    private TableColumn<Network, String> networkBalance;
    @FXML
    private TableColumn<Network, String> networkStatus;

    private NotificationPane notificationPane;

    /** All networks this controller is aware of */
    private final ObservableList<Network> networks = FXCollections.observableArrayList();
    /** All networks which are in starting or running state */
    private final ObservableList<Network> activeNetworks = FXCollections.observableArrayList();
    private final ObservableList<WalletTransaction> transactions = FXCollections.observableArrayList();
    private final Map<Network, NetworkDetail> networkDetails = new HashMap<>();
    private KeyCrypterScrypt keyCrypter;

    private final Logger logger = LoggerFactory.getLogger(MainController.class);
    private CATE cate;
    private volatile boolean stopping = false;

    @FXML
    public void initialize() {
        
        receiveSelector.setItems(activeNetworks);
        sendSelector.setItems(activeNetworks);
        receiveSelector.setConverter(new WalletToNetworkNameConvertor());
        sendSelector.setConverter(receiveSelector.getConverter());

        initializeWalletList();
        initializeTransactionList();

        receiveSelector.setDisable(true);
        sendSelector.setDisable(true);
        receiveSelector.setOnAction((ActionEvent event) -> {
            if (event.getTarget().equals(receiveSelector)) {
                final Network network = receiveSelector.getValue();
                if (network != null) {
                    final Wallet wallet = network.wallet();
                    final Address address = wallet.currentReceiveAddress();
                    Platform.runLater(() -> {
                        myAddress.setText(address.toBase58());
                    });
                } else {
                    myAddress.setText("");
                }
            }
        });

        sendButton.setOnAction(this::sendCoinsOnUIThread);
        menuExit.setOnAction(this::stop);
    }

    /**
     * Connect to the specified network.
     *
     * @param params network parameters for the relay network to connect to.
     * @param dataDir directory to store data files in.
     * @return the service that has been started. Can be used to test start
     * completes successfully.
     */
    public Service connectTo(final NetworkParameters params, final File dataDir) {
        final Context context = new Context(params);
        final NetworkThreadFactory threadFactory = new NetworkThreadFactory(context);
        final ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory);
        final Network network = new Network(context, this, dataDir, executor, this::registerWallet);
        final StringProperty statusProperty = new SimpleStringProperty("Starting");

        threadFactory.setUncaughtExceptionHandler(buildUncaughtExceptionHandler(network));
        networks.add(network);

        // Add a listener to shut down the executor service once the network service
        // it's responsible for terminates.
        network.addListener(new Service.Listener() {
            @Override
            public void starting() {
                statusProperty.setValue(resources.getString("walletList.networkStatus.starting"));
            }

            @Override
            public void running() {
                statusProperty.setValue(resources.getString("walletList.networkStatus.running"));
            }

            @Override
            public void stopping(Service.State from) {
                statusProperty.setValue(resources.getString("walletList.networkStatus.stopping"));
            }

            @Override
            public void terminated(Service.State from) {
                executor.shutdown();
                Platform.runLater(() -> { activeNetworks.remove(network); });
                statusProperty.setValue(resources.getString("walletList.networkStatus.terminated"));
            }

            @Override
            public void failed(Service.State from, Throwable failure) {
                statusProperty.setValue(resources.getString("walletList.networkStatus.failed"));
            }
        }, executor);

        final NetworkDetail detail = new NetworkDetail(executor, statusProperty);
        networkDetails.put(network, detail);

        final Service service = network.startAsync();
        return service;
    }

    private void initializeTransactionList() {
        txList.setItems(transactions);
        txList.setRowFactory(value ->{
            final TableRow<WalletTransaction> row = new TableRow<>();
            final ContextMenu rowMenu = new ContextMenu();
            final MenuItem transactionIdItem = new MenuItem(resources.getString("menuItem.copyTransactionId"));
            final MenuItem explorerItem = new MenuItem(resources.getString("menuItem.showOnExplorer"));
            final MenuItem detailsItem = new MenuItem(resources.getString("menuItem.txDetails"));
            final MenuItem receivingAddressItem = new MenuItem(resources.getString("menuItem.receivingAddress"));

            transactionIdItem.setOnAction(action -> GenericUtils.copyToClipboard(row.getItem().getTransaction().getHashAsString()));
            explorerItem.setOnAction(action -> openBlockExplorer(row.getItem()));
            detailsItem.setOnAction(action -> showTxDetailsDialog(row.getItem()));

            receivingAddressItem.setOnAction(action -> GenericUtils.copyToClipboard(
                    TransactionFormatter.getRelevantOutputs(row.getItem()).get(0).getScriptPubKey().getToAddress(
                            row.getItem().getParams()).toString()));

            rowMenu.getItems().addAll(transactionIdItem, receivingAddressItem, detailsItem, explorerItem);

            row.contextMenuProperty().set(rowMenu);

            row.setOnMouseClicked(event -> {
                if (event.getClickCount() == 2 && (!row.isEmpty())) {
                    showTxDetailsDialog(row.getItem());
                }
            });

            return row;
        });
        txNetworkColumn.setCellValueFactory(dataFeatures -> {
            return dataFeatures.getValue().networkNameProperty();
        });
        txDateColumn.setCellValueFactory(dataFeatures -> {
            return dataFeatures.getValue().dateProperty();
        });
        txAmountColumn.setCellValueFactory(dataFeatures -> {
            return dataFeatures.getValue().amountProperty();
        });
        txMemoColumn.setCellValueFactory(dataFeatures -> {
            return dataFeatures.getValue().memoProperty();
        });
    }

    private void openBlockExplorer(WalletTransaction item) {
        HostServicesDelegate hostServices = HostServicesFactory.getInstance(CATE.getInstance());
        hostServices.showDocument(BlockExplorerResolver.getUrl(item));
    }

    private boolean showTxDetailsDialog(WalletTransaction item) {
        try {
            final Stage dialog = TransactionDetailsDialog.build(resources, item);
            dialog.showAndWait();
            return true;
        } catch (IOException e) {
            logger.error(resources.getString("alert.txDetailsError"), e);
            Alert alert = new Alert(Alert.AlertType.ERROR, resources.getString("alert.txDetailsError")
                + e.getMessage());
            alert.setTitle(resources.getString("internalError.title"));
            alert.showAndWait();
        }
        return false;
    }

    private void initializeWalletList() {
        walletList.setItems(networks);
        walletList.setRowFactory(view -> {
            final TableRow<Network> row = new TableRow<>();
            final ContextMenu rowMenu = new ContextMenu();
            final MenuItem encryptItem = new MenuItem(resources.getString("menuItem.encrypt"));
            final MenuItem decryptItem = new MenuItem(resources.getString("menuItem.decrypt"));

            // TODO: Enable/disable options based on whether the wallet is locked.
            // Alternatively have two different context menus that display different
            // options.
            encryptItem.setOnAction(action -> encryptWalletOnUIThread(row.getItem()));
            decryptItem.setOnAction(action -> decryptWalletOnUIThread(row.getItem()));

            rowMenu.getItems().addAll(encryptItem, decryptItem);

            row.contextMenuProperty().set(rowMenu);

            return row;
        });
        networkName.setCellValueFactory(dataFeatures -> {
            final Network network = dataFeatures.getValue();
            final NetworkParameters params = network.getParams();
            return new SimpleStringProperty(NetworkResolver.getName(params));
        });
        networkBalance.setCellValueFactory(dataFeatures -> {
            final Network network = dataFeatures.getValue();
            return network.getEstimatedBalanceProperty();
        });
        networkStatus.setCellValueFactory(dataFeatures -> {
            final Network network = dataFeatures.getValue();
            return getStatusProperty(network);
        });
    }

    public void setNotificationPane(NotificationPane notificationPane) {
        this.notificationPane = notificationPane;
    }

    /**
     * Add a transaction to those displayed by this controller.
     *
     * @param network network the transaction is from.
     * @param tx the underlying transaction to add.
     * @param prevBalance previous wallet balance.
     * @param newBalance new wallet balance.
     */
    public void addTransaction(Network network, Transaction tx, Coin prevBalance, Coin newBalance) {
        // TODO: Transaction lists should be aggregated from listed held by each
        // network.
        // For now we do the actual modification on the UI thread to avoid
        // race conditions
        Platform.runLater(() -> {
            transactions.add(0, new WalletTransaction(network, tx, newBalance.subtract(prevBalance)));
        });
    }

    /**
     * Prompts the user for the wallet password and then decrypts the underlying
     * wallet. Shows a warning then takes no further action if the wallet is not
     * encrypted.
     */
    private void decryptWalletOnUIThread(final Network network) {
        if (!network.getEncryptedStateProperty().getValue()) {
            Alert alert = new Alert(Alert.AlertType.ERROR,resources.getString("alert.walletUnencrypted.msg"));
            alert.setTitle(resources.getString("alert.walletUnencrypted.title"));
            alert.showAndWait();
            return;
        }

        PasswordInputDialog passwordDialog = new PasswordInputDialog();
        passwordDialog.setTitle(resources.getString("dialogDecrypt.title"));
        passwordDialog.setHeaderText(resources.getString("dialogDecrypt.msg"));
        passwordDialog.setContentText(resources.getString("dialogDecrypt.label"));

        passwordDialog.showAndWait().ifPresent(value -> {
            network.decrypt(value, o -> {
                Platform.runLater(() -> {
                    Alert alert = new Alert(Alert.AlertType.INFORMATION);
                    alert.setTitle(resources.getString("alert.decryptWallet.successTitle"));
                    alert.setContentText(resources.getString("alert.decryptWallet.successMsg"));
                    alert.showAndWait();
                });
            }, t -> {
                Platform.runLater(() -> {
                    Alert alert = new Alert(Alert.AlertType.WARNING,
                            resources.getString("alert.decryptWallet.noticeMsg"));
                    alert.setTitle(resources.getString("alert.decryptWallet.noticeTitle"));
                    alert.showAndWait();
                });
            }, t -> {
                Platform.runLater(() -> {
                    Alert alert = new Alert(Alert.AlertType.ERROR,
                            t.getMessage());
                    alert.setTitle(resources.getString("alert.decryptWallet.errorTitle"));
                    alert.showAndWait();
                });
            }, NETWORK_PUSH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
        });
    }

    /**
     * Prompts the user for the wallet password and then encrypts the underlying
     * wallet. If the wallet is already encrypted it changes the encryption key.
     */
    private void encryptWalletOnUIThread(final Network network) {
        if (network.getEncryptedStateProperty().getValue()) {
            Alert alert = new Alert(Alert.AlertType.ERROR, resources.getString("alert.walletEncrypted.msg"));
            alert.setTitle(resources.getString("alert.walletEncrypted.title"));
            alert.showAndWait();
            return;
        }

        DualPasswordInputDialog dialog = new DualPasswordInputDialog(resources);
        dialog.setTitle(resources.getString("dialogEncrypt.title"));
        dialog.setHeaderText(resources.getString("dialogEncrypt.msg"));

        dialog.showAndWait().ifPresent(value -> {
            if (value == null) {
                Platform.runLater(() -> {
                    Alert alert = new Alert(Alert.AlertType.INFORMATION,
                            resources.getString("alert.encryptWallet.mismatchMsg"));
                    alert.setTitle(resources.getString("alert.encryptWallet.errorTitle"));
                    alert.showAndWait();
                });
            } else {
                network.encrypt(value, o -> {
                    Platform.runLater(() -> {
                        Alert alert = new Alert(Alert.AlertType.INFORMATION,
                                resources.getString("alert.encryptWallet.successMsg"));
                        alert.setTitle(resources.getString("alert.encryptWallet.successTitle"));
                        alert.showAndWait();
                    });
                }, t -> {
                    Platform.runLater(() -> {
                        Alert alert = new Alert(Alert.AlertType.WARNING,
                                resources.getString("alert.encryptWallet.noticeMsg"));
                        alert.setTitle(resources.getString("alert.encryptWallet.noticeTitle"));
                        alert.showAndWait();
                    });
                }, t -> {
                    Platform.runLater(() -> {
                        Alert alert = new Alert(Alert.AlertType.ERROR,
                                t.getMessage());
                        alert.setTitle(resources.getString("alert.encryptWallet.errorTitle"));
                        alert.showAndWait();
                    });
                }, NETWORK_PUSH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
            }
        });
    }
    public static final int DIALOG_VGAP = 10;
    public static final int DIALOG_HGAP = 10;

    /**
     * Take coin values to send from the user interface, prompt the user to
     * confirm, and then send the coins. MUST be called on the UI thread.
     */
    private void sendCoinsOnUIThread(ActionEvent event) {
        final Address address;
        final Coin amount;
        final Network network = (Network) sendSelector.getValue();
        final SendRequest req;

        try {
            address = Address.fromBase58(network.getParams(), sendAddress.getText());
        } catch (AddressFormatException ex) {
            Alert alert = new Alert(Alert.AlertType.ERROR, resources.getString("sendCoins.addressError.msg")
                    + ex.getMessage());
            alert.setTitle(resources.getString("sendCoins.addressError.title"));
            alert.showAndWait();
            return;
        }

        try {
            amount = Coin.parseCoin(sendAmount.getText());
            // The wallet send request will reject negative amounts as well, but
            // by doing it here we can localize the error message.
            if (!amount.isPositive()) {
                Alert alert = new Alert(Alert.AlertType.ERROR, resources.getString("sendCoins.amountError.notPositive.msg"));
                alert.setTitle(resources.getString("sendCoins.amountError.title"));
                alert.showAndWait();
                return;
            }
            req = SendRequest.to(address, amount);
        } catch (IllegalArgumentException ex) {
            Alert alert = new Alert(Alert.AlertType.ERROR, resources.getString("sendCoins.amountError.msg"));
            alert.setTitle(resources.getString("sendCoins.amountError.title"));
            alert.showAndWait();
            return;
        }

        final TransactionConfirmationAlert confirmSend
            = new TransactionConfirmationAlert(network.getParams(), this.resources);

        // TODO: Show details of fees and total including fees
        confirmSend.setAddress(address);
        confirmSend.setAmount(amount);
        confirmSend.setMemo("");
        confirmSend.initOwner(((Node) event.getTarget()).getScene().getWindow());

        confirmSend.showAndWait()
            .filter(response -> response == ButtonType.OK)
            .ifPresent(response -> {
                req.memo = confirmSend.getMemo().trim();
                doSendCoins(network, req);
        });
    }

    /**
     * Actually send coins, called once the user has confirmed, and then let the
     * user know once it succeeds/fails.
     *
     * @param network the network to send coins over.
     * @param req send request to execute
     */
    private void doSendCoins(final Network network, final SendRequest req) {
        // Prompt for password if the wallet is encrypted
        // TODO: Should have an unlock() method we call here,
        // and uses cached password for ~5 minutes, rather than prompting every
        // time
        if (network.getEncryptedStateProperty().getValue()) {
            req.aesKey = getAESKeyFromUser(network);
            if (req.aesKey == null) {
                // No key available, which means the user hit cancel
                return;
            }
        }

        network.sendCoins(req,
                (Wallet.SendResult sendResult) -> {
                    Platform.runLater(() -> {
                        showTopBannerOnUIThread(resources.getString("doSendCoin.successNotification"));
                        sendAddress.clear();
                        sendAmount.clear();
                    });
                }, (Coin missing) -> {
                    Platform.runLater(() -> {
                        Alert alert = new Alert(Alert.AlertType.WARNING);
                        alert.setTitle(resources.getString("doSendCoins.moneyError.title"));
                        alert.setHeaderText(resources.getString("doSendCoins.moneyError.head"));
                        alert.setContentText(resources.getString("doSendCoins.moneyError.msg1")
                                + (missing == null
                                        ? resources.getString("doSendCoins.moneyError.msg2")
                                        : network.format(missing))
                                + resources.getString("doSendCoins.moneyError.msg3"));

                        alert.showAndWait();
                    });
                }, (KeyCrypterException ex) -> {
                    // TODO: This needs to be a bit more useful in explaining
                    // what's going on where the user has unconfirmed transactions
                    // sufficient to cover a payment, but cannot spend them until
                    // they have confirmed.
                    Platform.runLater(() -> {
                        Alert alert = new Alert(Alert.AlertType.WARNING);
                        alert.setTitle(resources.getString("doSendCoins.walletLocked.title"));
                        alert.setHeaderText(resources.getString("doSendCoins.walletLocked.head"));
                        alert.setContentText(resources.getString("doSendCoins.walletLocked.msg"));
                        alert.showAndWait();
                    });
                }, NETWORK_PUSH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    }

    /**
     * Show a top banner with a 3 second timeout and the specified text. Must
     * be called from the UI thread.
     *
     * @param text Text to show in the banner.
     */
    private void showTopBannerOnUIThread(String text) {
        notificationPane.setText(text);
        notificationPane.getStyleClass().add(NotificationPane.STYLE_CLASS_DARK);
        notificationPane.show();
        // TODO: We should have a single persistent thread that does this, and
        // handles resetting the timer if a new banner is shown before the old is
        // hidden
        new Thread(() -> {
            try {
                Thread.sleep(BANNER_DISPLAY_MILLIS);
            } catch (InterruptedException ex) {
                // Ignore
            }
            Platform.runLater(() -> {
                notificationPane.hide();
            });
        }).start();
    }

    private KeyParameter getAESKeyFromUser(final Network network) {
        // TODO: Cache the key for ~5 minutes

        // I don't like that we have to hold the password as a string, so we
        // can't wipe the values once we're done.
        final PasswordInputDialog passwordDialog = new PasswordInputDialog();
        passwordDialog.setContentText(resources.getString("getAESKey.msg"));

        // We don't use lambdas here because we're returning a value based on
        // the evaluation
        final Optional<String> password = passwordDialog.showAndWait();
        if (password.isPresent()) {
            return network.getKeyFromPassword(password.get());
        } else {
            return null;
        }
    }

    /**
     * Register a wallet to be tracked by this controller. This recalculates
     * wallet transactions, which is a long running task, and must be run on a
     * background thread.
     */
    public void registerWallet(final Network network, final Wallet wallet) {
        // We rebuild the transactions on the current thread, rather than slowing
        // down the UI thread, and so keep a temporary copy to be pushed into the
        // main transaction list later.
        final List<WalletTransaction> tempTransactions = rebuildTransactions(network, wallet);

        Collections.reverse(tempTransactions);
        Platform.runLater(() -> {
            this.activeNetworks.add(network);
            if (this.activeNetworks.size() == 1) {
                // We've just added the first wallet, choose it
                receiveSelector.setValue(network);
                sendSelector.setValue(network);
                receiveSelector.setDisable(false);
                sendSelector.setDisable(false);
            }

            // TODO: Need to enforce order of transactions by time, not by
            // network and then time as this does
            transactions.addAll(tempTransactions);
        });
    }

    /**
     * Stops the controller, which includes shutting down the various networks
     * it is managing. Convenience method for use in lambda expressions.
     */
    public void stop(final ActionEvent event) {
        stop();
    }

    /**
     * Stops the controller, which includes shutting down the various networks
     * it is managing. Convenience method for use in lambda expressions.
     */
    public void stop(final WindowEvent event) {
        stop();
    }

    /**
     * Stops the controller, which includes shutting down the various networks
     * it is managing.
     */
    private void stop() {
        if (stopping) {
            return;
        }
        stopping = true;

        // Stop any further event handling from occurring
        receiveSelector.setOnAction(null);
        sendButton.setOnAction(null);
        
        final Alert alert = new Alert(Alert.AlertType.INFORMATION, resources.getString("alert.shuttingDown"));
        alert.setTitle(resources.getString("alert.shuttingDown.title"));
        alert.getButtonTypes().clear();
        Platform.runLater(() -> {
            alert.show();
        });
        networks.stream()
            .forEach(network -> {
                logger.info("Shutting down " + network);
                network.stopAsync();
            });
        new Thread(() -> {
            networks.stream()
                .forEach(service -> service.awaitTerminated());
            alert.hide();
            Platform.exit();
        }).start();
    }

    /**
     * Builds an uncaught exception handler for threads belonging to a relay
     * network.
     *
     * @param network network the handler is for.
     * @return an uncaught exception handler.
     */
    public Thread.UncaughtExceptionHandler buildUncaughtExceptionHandler(final Network network) {
        return (Thread thread, Throwable thrwbl) -> {
            logger.error("Internal error from network "
                + network.getParams().getId(), thrwbl);
            if (thrwbl instanceof Exception) {
                Platform.runLater(() -> {
                    Alert alert = new Alert(Alert.AlertType.ERROR);
                    alert.setTitle(resources.getString("internalError.title"));
                    alert.setContentText(thrwbl.getMessage());
                    alert.showAndWait();
                    // TODO: Shut down and de-register the wallet from currently
                    // running
                });
            } else {
                // Fatal, begin shutdown
                MainController.this.stop();
            }
        };
    }

    /**
     * Handle a network service failing.
     *
     * @param network the service which failed.
     * @param from the status the service was in before it failed.
     * @param thrwbl the exception causing the service to fail.
     */
    public void onNetworkFailed(Network network, Service.State from, Throwable thrwbl) {
        networks.remove(network);
        Platform.runLater(() -> {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle(resources.getString("internalError.title"));
            alert.setContentText(thrwbl.getMessage());
            alert.showAndWait();
        });
    }

    public void refreshTransactions(Network network, Wallet wallet) {
        // TODO: Clear transactions from the given wallet out of the list
        // then re-apply them
    }

    private List<WalletTransaction> rebuildTransactions(final Network network, final Wallet wallet) {
        // We rebuild the transactions on the current thread, rather than slowing
        // down the UI thread, and so keep a temporary copy to be pushed into the
        // main transaction list later.
        final List<WalletTransaction> tempTransactions = new ArrayList<>();

        // Pre-sort transactions by date
        final SortedSet<Transaction> rawTransactions = new TreeSet<>(
                (Transaction a, Transaction b) -> a.getUpdateTime().compareTo(b.getUpdateTime())
        );
        rawTransactions.addAll(wallet.getTransactions(false));

        final Map<TransactionOutPoint, Coin> balances = new HashMap<>();
        for (Transaction tx : rawTransactions) {
            long valueChange = 0;
            for (TransactionInput in : tx.getInputs()) {
                Coin balance = balances.get(in.getOutpoint());
                // Spend the value on the listed input
                if (balance != null) {
                    valueChange -= balance.value;
                    balances.remove(in.getOutpoint());
                }
            }
            for (TransactionOutput out : tx.getOutputs()) {
                if (out.isMine(wallet)) {
                    valueChange += out.getValue().value;
                    Coin balance = balances.get(out.getOutPointFor());
                    if (balance == null) {
                        balance = out.getValue();
                    } else {
                        balance.add(out.getValue());
                    }
                    balances.put(out.getOutPointFor(), balance);
                }
            }
            tempTransactions.add(new WalletTransaction(network, tx, Coin.valueOf(valueChange)));
        }

        Collections.reverse(tempTransactions);
        return tempTransactions;
    }

    private StringProperty getStatusProperty(Network network) {
        return networkDetails.get(network).statusProperty;
    }

    public CATE getCate() {
        return cate;
    }

    public void setCate(CATE cate) {
        this.cate = cate;
    }

    private class NetworkDetail extends Object {
        private StringProperty statusProperty;
        private ExecutorService executor;

        private NetworkDetail(final ExecutorService executor, final StringProperty statusProperty) {
           this.executor = executor;
           this.statusProperty = statusProperty;
        }
    }

    private class WalletToNetworkNameConvertor extends StringConverter<Network> {

        public WalletToNetworkNameConvertor() {
        }

        @Override
        public String toString(Network network) {
            return NetworkResolver.getName(network.getParams());
        }

        @Override
        public Network fromString(String string) {
            final NetworkParameters params = NetworkResolver.getParameter(string);
            for (Network network: networks) {
                if (network.getParams().equals(params)) {
                    return network;
                }
            }
            return null;
        }
    }

}