# This Python file uses the following encoding: utf-8
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from builtins import str
from builtins import object
from dpaygo.instance import shared_dpay_instance
import random
from dpaygobase import memo as BtsMemo
from dpaygographenebase.account import PrivateKey, PublicKey
from .account import Account
from .exceptions import MissingKeyError


class Memo(object):
    """ Deals with Memos that are attached to a transfer

        :param dpaygo.account.Account from_account: Account that has sent the memo
        :param dpaygo.account.Account to_account: Account that has received the memo
        :param dpaygo.dpay.DPay dpay_instance: DPay instance

        A memo is encrypted with a shared secret derived from a private key of
        the sender and a public key of the receiver. Due to the underlying
        mathematics, the same shared secret can be derived by the private key
        of the receiver and the public key of the sender. The encrypted message
        is perturbed by a nonce that is part of the transmitted message.

        .. code-block:: python

            from dpaygo.memo import Memo
            m = Memo("dpayeu", "wallet.jared")
            m.dpay.wallet.unlock("secret")
            enc = (m.encrypt("foobar"))
            print(enc)
            >> {'nonce': '17329630356955254641', 'message': '8563e2bb2976e0217806d642901a2855'}
            print(m.decrypt(enc))
            >> foobar

        To decrypt a memo, simply use

        .. code-block:: python

            from dpaygo.memo import Memo
            m = Memo()
            m.dpay.wallet.unlock("secret")
            print(m.decrypt(op_data["memo"]))

        if ``op_data`` being the payload of a transfer operation.

        Memo Keys

        In dPay, memos are AES-256 encrypted with a shared secret between sender and
        receiver. It is derived from the memo private key of the sender and the memo
        public key of the receiver.

        In order for the receiver to decode the memo, the shared secret has to be
        derived from the receiver's private key and the senders public key.

        The memo public key is part of the account and can be retrieved with the
        `get_account` call:

        .. code-block:: js

            get_account <accountname>
            {
              [...]
              "options": {
                "memo_key": "GPH5TPTziKkLexhVKsQKtSpo4bAv5RnB8oXcG4sMHEwCcTf3r7dqE",
                [...]
              },
              [...]
            }

        while the memo private key can be dumped with `dump_private_keys`

        Memo Message

        The take the following form:

        .. code-block:: js

                {
                  "from": "GPH5mgup8evDqMnT86L7scVebRYDC2fwAWmygPEUL43LjstQegYCC",
                  "to": "GPH5Ar4j53kFWuEZQ9XhxbAja4YXMPJ2EnUg5QcrdeMFYUNMMNJbe",
                  "nonce": "13043867485137706821",
                  "message": "d55524c37320920844ca83bb20c8d008"
                }

        The fields `from` and `to` contain the memo public key of sender and receiver.
        The `nonce` is a random integer that is used for the seed of the AES encryption
        of the message.

        Encrypting a memo

        The high level memo class makes use of the dpaygo wallet to obtain keys
        for the corresponding accounts.

        .. code-block:: python

            from dpaygo.memo import Memo
            from dpaygo.account import Account

            memoObj = Memo(
                from_account=Account(from_account),
                to_account=Account(to_account)
            )
            encrypted_memo = memoObj.encrypt(memo)

        Decoding of a received memo

        .. code-block:: python

            from getpass import getpass
            from dpaygo.block import Block
            from dpaygo.memo import Memo

            # Obtain a transfer from the blockchain
            block = Block(23755086)                   # block
            transaction = block["transactions"][3]    # transactions
            op = transaction["operations"][0]         # operation
            op_id = op[0]                             # operation type
            op_data = op[1]                           # operation payload

            # Instantiate Memo for decoding
            memo = Memo()

            # Unlock wallet
            memo.unlock_wallet(getpass())

            # Decode memo
            # Raises exception if required keys not available in the wallet
            print(memo.decrypt(op_data["transfer"]))

    """
    def __init__(
        self,
        from_account=None,
        to_account=None,
        dpay_instance=None
    ):

        self.dpay = dpay_instance or shared_dpay_instance()

        if to_account:
            self.to_account = Account(to_account, dpay_instance=self.dpay)
        if from_account:
            self.from_account = Account(from_account, dpay_instance=self.dpay)

    def unlock_wallet(self, *args, **kwargs):
        """ Unlock the library internal wallet
        """
        self.dpay.wallet.unlock(*args, **kwargs)
        return self

    def encrypt(self, memo, bts_encrypt=False):
        """ Encrypt a memo

            :param str memo: clear text memo message
            :returns: encrypted memo
            :rtype: str
        """
        if not memo:
            return None

        nonce = str(random.getrandbits(64))
        memo_wif = self.dpay.wallet.getPrivateKeyForPublicKey(
            self.from_account["memo_key"]
        )
        if not memo_wif:
            raise MissingKeyError("Memo key for %s missing!" % self.from_account["name"])

        if not hasattr(self, 'chain_prefix'):
            self.chain_prefix = self.dpay.prefix

        if bts_encrypt:
            enc = BtsMemo.encode_memo_bts(
                PrivateKey(memo_wif),
                PublicKey(
                    self.to_account["memo_key"],
                    prefix=self.chain_prefix
                ),
                nonce,
                memo
            )

            return {
                "message": enc,
                "nonce": nonce,
                "from": self.from_account["memo_key"],
                "to": self.to_account["memo_key"]
            }
        else:
            enc = BtsMemo.encode_memo(
                PrivateKey(memo_wif),
                PublicKey(
                    self.to_account["memo_key"],
                    prefix=self.chain_prefix
                ),
                nonce,
                memo,
                prefix=self.chain_prefix
            )

            return {
                "message": enc,
                "from": self.from_account["memo_key"],
                "to": self.to_account["memo_key"]
            }

    def decrypt(self, memo):
        """ Decrypt a memo

            :param str memo: encrypted memo message
            :returns: encrypted memo
            :rtype: str
        """
        if not memo:
            return None

        # We first try to decode assuming we received the memo
        if isinstance(memo, dict) and "to" in memo and "from" in memo and "memo" in memo:
            memo_to = Account(memo["to"], dpay_instance=self.dpay)
            memo_from = Account(memo["from"], dpay_instance=self.dpay)
            message = memo["memo"]
        else:
            memo_to = self.to_account
            memo_from = self.from_account
            message = memo
        if isinstance(memo, dict) and "nonce" in memo:
            nonce = memo.get("nonce")
        else:
            nonce = ""

        try:
            memo_wif = self.dpay.wallet.getPrivateKeyForPublicKey(
                memo_to["memo_key"]
            )
            pubkey = memo_from["memo_key"]
        except MissingKeyError:
            try:
                # if that failed, we assume that we have sent the memo
                memo_wif = self.dpay.wallet.getPrivateKeyForPublicKey(
                    memo_from["memo_key"]
                )
                pubkey = memo_to["memo_key"]
            except MissingKeyError:
                # if all fails, raise exception
                raise MissingKeyError(
                    "Non of the required memo keys are installed!"
                    "Need any of {}".format(
                    [memo_to["name"], memo_from["name"]]))

        if not hasattr(self, 'chain_prefix'):
            self.chain_prefix = self.dpay.prefix

        if message[0] == '#':
            return BtsMemo.decode_memo(
                PrivateKey(memo_wif),
                message
            )
        else:
            return BtsMemo.decode_memo_bts(
                PrivateKey(memo_wif),
                PublicKey(pubkey, prefix=self.chain_prefix),
                nonce,
                message
            )
