#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2015 thomasv@gitorious, kyuupichan@gmail
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from electrum import WalletStorage
from electrum.plugins import run_hook
from util import PrintError
from wallet import Wallet
from i18n import _

MSG_GENERATING_WAIT = _("Electrum is generating your addresses, please wait...")
MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of "
                       "Bitcoin addresses, or a list of private keys")
MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):")
MSG_VERIFY_SEED = _("Your seed is important!\nTo make sure that you have properly saved your seed, please retype it here.")
MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
MSG_SHOW_MPK = _("Here is your master public key:")
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.  "
                       "Enter nothing if you want to disable encryption.")
MSG_RESTORE_PASSPHRASE = \
    _("Please enter the passphrase you used when creating your %s wallet.  "
      "Note this is NOT a password.  Enter nothing if you did not use "
      "one or are unsure.")

class UserCancelled(Exception):
    pass

class WizardBase(PrintError):
    '''Base class for gui-specific install wizards.'''
    user_actions = ('create', 'restore')
    wallet_kinds = [
        ('standard',  _("Standard wallet")),
        ('twofactor', _("Wallet with two-factor authentication")),
        ('multisig',  _("Multi-signature wallet")),
        ('hardware',  _("Hardware wallet")),
    ]


    # Derived classes must set:
    #   self.language_for_seed
    #   self.plugins

    def show_error(self, msg):
        raise NotImplementedError

    def show_warning(self, msg):
        raise NotImplementedError

    def remove_from_recently_open(self, filename):
        """Remove filename from the recently used list."""
        raise NotImplementedError

    def query_create_or_restore(self, wallet_kinds):
        """Ask the user what they want to do, and to what wallet kind.
        wallet_kinds is an array of tuples (kind, description).
        Return a tuple (action, kind).  Action is 'create' or 'restore',
        and kind is one of the wallet kinds passed."""
        raise NotImplementedError

    def query_multisig(self, action):
        """Asks the user what kind of multisig wallet they want.  Returns a
        string like "2of3".  Action is 'create' or 'restore'."""
        raise NotImplementedError

    def query_choice(self, msg, choices):
        """Asks the user which of several choices they would like.
        Return the index of the choice."""
        raise NotImplementedError

    def show_and_verify_seed(self, seed):
        """Show the user their seed.  Ask them to re-enter it.  Return
        True on success."""
        raise NotImplementedError

    def request_passphrase(self, device_text, restore=True):
        """Request a passphrase for a wallet from the given device and
        confirm it.  restore is True if restoring a wallet.  Should return
        a unicode string."""
        raise NotImplementedError

    def request_password(self, msg=None):
        """Request the user enter a new password and confirm it.  Return
        the password or None for no password."""
        raise NotImplementedError

    def request_seed(self, msg, is_valid=None):
        """Request the user enter a seed.  Returns the seed the user entered.
        is_valid is a function that returns True if a seed is valid, for
        dynamic feedback.  If not provided, Wallet.is_any is used."""
        raise NotImplementedError

    def request_many(self, n, xpub_hot=None):
        """If xpub_hot is provided, a new wallet is being created.  Request N
        master public keys for cosigners; xpub_hot is the master xpub
        key for the wallet.

        If xpub_hot is None, request N cosigning master xpub keys,
        xprv keys, or seeds in order to perform wallet restore."""
        raise NotImplementedError

    def choose_server(self, network):
        """Choose a server if one is not set in the config anyway."""
        raise NotImplementedError

    def open_existing_wallet(self, storage, network):
        wallet = Wallet(storage)
        self.update_wallet_format(wallet)
        self.run_wallet_actions(wallet)
        wallet.start_threads(network)
        return wallet, None

    def create_new_wallet(self, storage, network):
        action, wallet = self.create_or_restore(storage)
        self.run_wallet_actions(wallet)

        if network:
            self.choose_server(network)
        else:
            self.show_warning(_('You are offline'))

        def task():
            # Synchronize before starting the threads
            wallet.synchronize()
            wallet.start_threads(network)
# FIXME
#            if action == 'create':
#                msg = _('Wallet addresses generated.')
#            else:
#                wallet.wait_until_synchronized()
#                if network:
#                    if wallet.is_found():
#                        msg = _("Recovery successful")
#                    else:
#                        msg = _("No transactions found for this seed")
#                else:
#                    msg = _("This wallet was restored offline. It may "
#                            "contain more addresses than displayed.")
#            self.show_message(msg)

        return wallet, (MSG_GENERATING_WAIT, task)


    def open_wallet(self, network, filename):
        '''The main entry point of the wizard.  Open a wallet from the given
        filename.  If the file doesn't exist launch the GUI-specific
        install wizard proper.'''
        storage = WalletStorage(filename)
        if storage.file_exists:
            return self.open_existing_wallet(storage, network)
        else:
            return self.create_new_wallet(storage, network)

    def run_wallet_actions(self, wallet):
        if not wallet:
            return
        action = orig_action = wallet.get_action()
        while action:
            self.run_wallet_action(wallet, action)
            action = wallet.get_action()
        # Save the wallet after successful completion of actions.
        # It will be saved again once synchronized.
        if orig_action:
            wallet.storage.write()

    def run_wallet_action(self, wallet, action):
        self.print_error("action %s on %s" % (action, wallet.basename()))
        # Run the action on the wallet plugin, if any, then the
        # wallet and finally ourselves
        calls = [(wallet, (wallet, )),
                 (self, (wallet, ))]
        if hasattr(wallet, 'plugin'):
            calls.insert(0, (wallet.plugin, (wallet, self)))
        calls = [(getattr(actor, action), args) for (actor, args) in calls
                 if hasattr(actor, action)]
        if not calls:
            raise RuntimeError("No handler found for %s action" % action)
        for method, args in calls:
            method(*args)

    def create_or_restore(self, storage):
        '''After querying the user what they wish to do, create or restore
        a wallet and return it.'''
        self.remove_from_recently_open(storage.path)

        action, kind = self.query_create_or_restore(WizardBase.wallet_kinds)

        assert action in WizardBase.user_actions
        assert kind in [k for k, desc in WizardBase.wallet_kinds]

        if kind == 'multisig':
            wallet_type = self.query_multisig(action)
        elif kind == 'hardware':
            wallet_types, choices = self.plugins.hardware_wallets(action)
            if action == 'create':
                msg = _('Select the hardware wallet to create')
            else:
                msg = _('Select the hardware wallet to restore')
            choice = self.query_choice(msg, choices)
            wallet_type = wallet_types[choice]
        elif kind == 'twofactor':
            wallet_type = '2fa'
        else:
            wallet_type = 'standard'

        if action == 'create':
            wallet = self.create_wallet(storage, wallet_type, kind)
        else:
            wallet = self.restore_wallet(storage, wallet_type, kind)

        return action, wallet

    def construct_wallet(self, storage, wallet_type):
        storage.put('wallet_type', wallet_type)
        return Wallet(storage)

    def create_wallet(self, storage, wallet_type, kind):
        wallet = self.construct_wallet(storage, wallet_type)
        if kind == 'hardware':
            wallet.plugin.on_create_wallet(wallet, self)
        return wallet

    def restore_wallet(self, storage, wallet_type, kind):
        if wallet_type == 'standard':
            return self.restore_standard_wallet(storage)

        if kind == 'multisig':
            return self.restore_multisig_wallet(storage, wallet_type)

        # Plugin (two-factor or hardware)
        wallet = self.construct_wallet(storage, wallet_type)
        return wallet.plugin.on_restore_wallet(wallet, self)

    def restore_standard_wallet(self, storage):
        text = self.request_seed(MSG_ENTER_ANYTHING)
        need_password = Wallet.should_encrypt(text)
        password = self.request_password() if need_password else None
        return Wallet.from_text(text, password, storage)

    def restore_multisig_wallet(self, storage, wallet_type):
        # FIXME: better handling of duplicate keys
        m, n = Wallet.multisig_type(wallet_type)
        key_list = self.request_many(n - 1)
        need_password = any(Wallet.should_encrypt(text) for text in key_list)
        password = self.request_password() if need_password else None
        return Wallet.from_multisig(key_list, password, storage, wallet_type)

    def create_seed(self, wallet):
        '''The create_seed action creates a seed and then generates
        wallet account(s).'''
        seed = wallet.make_seed(self.language_for_seed)
        self.show_and_verify_seed(seed)
        password = self.request_password()
        wallet.add_seed(seed, password)
        wallet.create_master_keys(password)
        wallet.create_main_account(password)

    def add_cosigners(self, wallet):
        # FIXME: better handling of duplicate keys
        m, n = Wallet.multisig_type(wallet.wallet_type)
        xpub1 = wallet.master_public_keys.get("x1/")
        xpubs = self.request_many(n - 1, xpub1)
        for i, xpub in enumerate(xpubs):
            wallet.add_master_public_key("x%d/" % (i + 2), xpub)

    def update_wallet_format(self, wallet):
        # Backwards compatibility: convert old-format imported keys
        if wallet.imported_keys:
            msg = _("Please enter your password in order to update "
                    "imported keys")
            if wallet.use_encryption:
                password = self.request_password(msg)
            else:
                password = None

            try:
                wallet.convert_imported_keys(password)
            except Exception as e:
                self.show_error(str(e))

        # Call synchronize to regenerate addresses in case we're offline
        if wallet.get_master_public_keys() and not wallet.addresses():
            wallet.synchronize()