"""
Created on 1 févr. 2014

@author: inso
"""

from ucoinpy.documents.transaction import InputSource, OutputSource, Transaction
from ucoinpy.key import SigningKey

from ucoinpy.api import bma
from ucoinpy.api.bma import PROTOCOL_VERSION
from ..tools.exceptions import NotEnoughMoneyError, NoPeerAvailable, LookupFailureError
from .transfer import Transfer
from .txhistory import TxHistory
from .registry import IdentitiesRegistry, Identity

from PyQt5.QtCore import QObject, pyqtSignal, QCoreApplication

import logging
import asyncio


class Wallet(QObject):
    """
    A wallet is used to manage money with a unique key.
    """
    refresh_progressed = pyqtSignal(int, int, str)
    refresh_finished = pyqtSignal(list)

    def __init__(self, walletid, pubkey, name, identities_registry):
        """
        Constructor of a wallet object

        :param int walletid: The wallet number, unique between all wallets
        :param str pubkey: The wallet pubkey
        :param str name: The wallet name
        """
        super().__init__()
        self.coins = []
        self.walletid = walletid
        self.pubkey = pubkey
        self.name = name
        self._identities_registry = identities_registry
        self.caches = {}

    @classmethod
    def create(cls, walletid, salt, password, name, identities_registry):
        """
        Factory method to create a new wallet

        :param int walletid: The wallet number, unique between all wallets
        :param str salt: The account salt
        :param str password: The account password
        :param str name: The account name
        """
        if walletid == 0:
            key = SigningKey(salt, password)
        else:
            key = SigningKey(b"{0}{1}".format(salt, walletid), password)
        return cls(walletid, key.pubkey, name, identities_registry)

    @classmethod
    def load(cls, json_data, identities_registry):
        """
        Factory method to load a saved wallet.

        :param dict json_data: The wallet as a dict in json format
        """
        walletid = json_data['walletid']
        pubkey = json_data['pubkey']
        name = json_data['name']
        return cls(walletid, pubkey, name, identities_registry)

    def load_caches(self, app, json_data):
        """
        Load this wallet caches.
        Each cache correspond to one different community.

        :param dict json_data: The caches as a dict in json format
        """
        for currency in json_data:
            if currency != 'version':
                self.caches[currency] = TxHistory(app, self)
                self.caches[currency].load_from_json(json_data[currency])

    def jsonify_caches(self):
        """
        Get this wallet caches as json.

        :return: The wallet caches as a dict in json format
        """
        data = {}
        for currency in self.caches:
            data[currency] = self.caches[currency].jsonify()
        return data

    def init_cache(self, app, community):
        """
        Init the cache of this wallet for the specified community.

        :param community: The community to refresh its cache
        """
        if community.currency not in self.caches:
            self.caches[community.currency] = TxHistory(app, self)

    def refresh_transactions(self, community, received_list):
        """
        Refresh the cache of this wallet for the specified community.

        :param community: The community to refresh its cache
        """
        logging.debug("Refresh transactions for {0}".format(self.pubkey))
        asyncio.async(self.caches[community.currency].refresh(community, received_list))

    def rollback_transactions(self, community, received_list):
        """
        Rollback the transactions of this wallet for the specified community.

        :param community: The community to refresh its cache
        """
        logging.debug("Refresh transactions for {0}".format(self.pubkey))
        asyncio.async(self.caches[community.currency].rollback(community, received_list))

    def check_password(self, salt, password):
        """
        Check if wallet password is ok.

        :param salt: The account salt
        :param password: The given password
        :return: True if (salt, password) generates the good public key
        .. warning:: Generates a new temporary SigningKey from salt and password
        """
        key = None
        if self.walletid == 0:
            key = SigningKey(salt, password)
        else:
            key = SigningKey("{0}{1}".format(salt, self.walletid), password)
        return (key.pubkey == self.pubkey)

    @asyncio.coroutine
    def relative_value(self, community):
        """
        Get wallet value relative to last generated UD

        :param community: The community to get value
        :return: The wallet relative value
        """
        value = yield from self.value(community)
        ud = community.dividend
        relative_value = value / float(ud)
        return relative_value

    @asyncio.coroutine
    def value(self, community):
        """
        Get wallet absolute value

        :param community: The community to get value
        :return: The wallet absolute value
        """
        value = 0
        sources = yield from self.sources(community)
        for s in sources:
            value += s.amount
        return value

    def tx_inputs(self, amount, community):
        """
        Get inputs to generate a transaction with a given amount of money

        :param int amount: The amount target value
        :param community: The community target of the transaction

        :return: The list of inputs to use in the transaction document
        """
        value = 0
        inputs = []
        cache = self.caches[community.currency]

        logging.debug("Available inputs : {0}".format(cache.available_sources))
        buf_inputs = list(cache.available_sources)
        for s in cache.available_sources:
            value += s.amount
            s.index = 0
            inputs.append(s)
            buf_inputs.remove(s)
            if value >= amount:
                return (inputs, buf_inputs)

        raise NotEnoughMoneyError(value, community.currency,
                                  len(inputs), amount)

    def tx_outputs(self, pubkey, amount, inputs):
        """
        Get outputs to generate a transaction with a given amount of money

        :param str pubkey: The target pubkey of the transaction
        :param int amount: The amount to send
        :param list inputs: The inputs used to send the given amount of money

        :return: The list of outputs to use in the transaction document
        """
        outputs = []
        inputs_value = 0
        for i in inputs:
            logging.debug(i)
            inputs_value += i.amount

        overhead = inputs_value - int(amount)
        outputs.append(OutputSource(pubkey, int(amount)))
        if overhead != 0:
            outputs.append(OutputSource(self.pubkey, overhead))
        return outputs

    @asyncio.coroutine
    def send_money(self, salt, password, community,
                   recipient, amount, message):
        """
        Send money to a given recipient in a specified community

        :param str salt: The account salt
        :param str password: The account password
        :param community: The community target of the transfer
        :param str recipient: The pubkey of the recipient
        :param int amount: The amount of money to transfer
        :param str message: The message to send with the transfer
        """
        blockid = yield from community.blockid()
        block = yield from community.bma_access.future_request(bma.blockchain.Block,
                                  req_args={'number': blockid.number})
        time = block['medianTime']
        txid = len(block['transactions'])
        key = None
        logging.debug("Key : {0} : {1}".format(salt, password))
        if self.walletid == 0:
            key = SigningKey(salt, password)
        else:
            key = SigningKey("{0}{1}".format(salt, self.walletid), password)
        logging.debug("Sender pubkey:{0}".format(key.pubkey))

        try:
            issuer = yield from self._identities_registry.future_find(key.pubkey, community)
            issuer_uid = issuer.uid
        except LookupFailureError as e:
            issuer_uid = ""

        try:
            receiver = yield from self._identities_registry.future_find(recipient, community)
            receiver_uid = receiver.uid
        except LookupFailureError as e:
            receiver_uid = ""

        metadata = {'block': None,
                    'time': time,
                    'amount': amount,
                    'issuer': key.pubkey,
                    'issuer_uid': issuer_uid,
                    'receiver': recipient,
                    'receiver_uid': receiver_uid,
                    'comment': message,
                    'txid': txid
                    }
        transfer = Transfer.initiate(metadata)

        self.caches[community.currency]._transfers.append(transfer)

        result = self.tx_inputs(int(amount), community)
        inputs = result[0]
        self.caches[community.currency].available_sources = result[1][1:]
        logging.debug("Inputs : {0}".format(inputs))

        outputs =  self.tx_outputs(recipient, amount, inputs)
        logging.debug("Outputs : {0}".format(outputs))
        tx = Transaction(PROTOCOL_VERSION, community.currency,
                         [self.pubkey], inputs,
                         outputs, message, None)
        logging.debug("TX : {0}".format(tx.raw()))

        tx.sign([key])
        logging.debug("Transaction : {0}".format(tx.signed_raw()))
        return (yield from transfer.send(tx, community))

    @asyncio.coroutine
    def sources(self, community):
        """
        Get available sources in a given community

        :param cutecoin.core.community.Community community: The community where we want available sources
        :return: List of InputSource ucoinpy objects
        """
        tx = []
        try:
            data = yield from community.bma_access.future_request(bma.tx.Sources,
                                     req_args={'pubkey': self.pubkey})
            for s in data['sources']:
                tx.append(InputSource.from_bma(s))
        except NoPeerAvailable as e:
            logging.debug(str(e))
        return tx

    def transfers(self, community):
        """
        Get all transfers objects of this wallet

        :param community: The community we want to get the executed transfers
        :return: A list of Transfer objects
        """
        if community.currency in self.caches:
            return self.caches[community.currency].transfers
        else:
            return []

    def dividends(self, community):
        """
        Get all the dividends received by this wallet

        :param community:  The community we want to get received dividends
        :return: Result of udhistory request
        """
        if community.currency in self.caches:
            return self.caches[community.currency].dividends
        else:
            return []

    def stop_coroutines(self):
        for c in self.caches.values():
            c.stop_coroutines()

    def jsonify(self):
        """
        Get the wallet as json format.

        :return: The wallet as a dict in json format.
        """
        return {'walletid': self.walletid,
                'pubkey': self.pubkey,
                'name': self.name}