# coding: utf-8
# Copyright (c) 2015 Fabian Barkhau <fabian.barkhau@gmail.com>
# License: MIT (see LICENSE file)


from __future__ import print_function
from __future__ import unicode_literals
import binascii
from . import serialize
from . import deserialize
from . import control
from . import exceptions
from . import common
from . insight import InsightService  # XXX rm when added to pycoin


class BtcTxStore():  # TODO use apigen when ported to python 3
    """Bitcoin nulldata output io library."""

    def __init__(self, testnet=False, dryrun=False):
        self.testnet = deserialize.flag(testnet)
        self.dryrun = deserialize.flag(dryrun)
        if self.testnet:
            self.service = InsightService("https://test-insight.bitpay.com/")
        else:
            self.service = InsightService("https://insight.bitpay.com/")

    ###############################
    # wallets, keys and addresses #
    ###############################

    def create_wallet(self, master_secret=b""):
        """Create a BIP0032-style hierarchical wallet.

        @param: master_secret Create from master secret, otherwise random.
        """
        master_secret = deserialize.bytes_str(master_secret)
        bip32node = control.create_wallet(self.testnet,
                                          master_secret=master_secret)
        return bip32node.hwif(as_private=True)

    def get_key(self, hwif):  # TODO add optional path
        bip32node = deserialize.wallet(self.testnet, hwif)
        return bip32node.wif()

    def create_key(self, master_secret=b""):
        """Create new private key and return in wif format.
        
        @param: master_secret Create from master secret, otherwise random.
        """
        master_secret = deserialize.bytes_str(master_secret)
        bip32node = control.create_wallet(self.testnet,
                                          master_secret=master_secret)
        return bip32node.wif()

    def validate_address(self, address):  # TODO test
        try:
            deserialize.address(self.testnet, address)
            return True
        except exceptions.InvalidInput:
            return False

    def get_address(self, wif):
        """Return bitcoin address for given wallet. """
        return deserialize.key(self.testnet, wif).address()

    def validate_key(self, wif):  # TODO test
        try:
            deserialize.key(self.testnet, wif)
            return True
        except exceptions.InvalidInput:
            return False

    ###############
    # transaction #
    ###############

    def create_tx(self, txins=None, txouts=None, lock_time=0):
        """Create unsigned rawtx with given txins/txouts as json data.
        <txins>: '[{"txid" : hexdata, "index" : integer}, ...]'
        <txouts>: '[{"address" : hexdata, "value" : satoshis}, ...]'
        """
        txins = [] if txins is None else txins
        txouts = [] if txouts is None else txouts
        lock_time = deserialize.positive_integer(lock_time)
        txins = deserialize.txins(txins)
        txouts = deserialize.txouts(self.testnet, txouts)
        tx = control.create_tx(self.service, self.testnet, txins, txouts,
                               lock_time=lock_time)
        return serialize.tx(tx)

    def send(self, wifs, txouts, change_address=None, lock_time=0, fee=10000):
        """TODO add doc string"""
        # FIXME test!!
        rawtx = self.create_tx(txouts=txouts, lock_time=lock_time)
        rawtx = self.add_inputs(rawtx, wifs, change_address=change_address,
                                fee=fee)
        return self.publish(rawtx)

    def add_inputs(self, rawtx, wifs, change_address=None, fee=10000,
                   dont_sign=False):
        """Add sufficient inputs from given <wifs> to cover <rawtx> outputs
        and <fee>. If no <change_address> is given, change will be sent to
        first wif.
        """
        tx = deserialize.tx(rawtx)
        keys = deserialize.keys(self.testnet, wifs)
        fee = deserialize.positive_integer(fee)
        if change_address is not None:
            change_address = deserialize.address(self.testnet, change_address)
        tx = control.add_inputs(self.service, self.testnet, tx, keys,
                                change_address=change_address, fee=fee)

        if not dont_sign:
            tx = control.sign_tx(self.service, self.testnet, tx, keys)

        return serialize.tx(tx)

    def sign_tx(self, rawtx, wifs):
        """Sign <rawtx> with  given <wifs> as json data.
        <wifs>: '["privatekey_in_wif_format", ...]'
        """
        tx = deserialize.tx(rawtx)
        keys = deserialize.keys(self.testnet, wifs)
        tx = control.sign_tx(self.service, self.testnet, tx, keys)
        return serialize.tx(tx)

    #################
    # blockchain io #
    #################

    def retrieve_tx(self, txid):
        """Returns rawtx for <txid>."""
        txid = deserialize.txid(txid)
        tx = self.service.get_tx(txid)
        return serialize.tx(tx)

    def retrieve_utxos(self, addresses):
        """Get current utxos for <address>."""
        addresses = deserialize.addresses(self.testnet, addresses)
        spendables = control.retrieve_utxos(self.service, addresses)
        return serialize.utxos(spendables)

    def publish(self, rawtx):
        """Publish signed <rawtx> to bitcoin network."""
        tx = deserialize.signedtx(rawtx)
        if not self.dryrun:
            self.service.send_tx(tx)
        return serialize.txid(tx.hash())

    ###########
    # signing #
    ###########

    def sign_data(self, wif, hexdata):
        """Signing <hexdata> with <wif> private key."""
        data = deserialize.binary(hexdata)
        key = deserialize.key(self.testnet, wif)
        sigdata = control.sign_data(self.testnet, data, key)
        return serialize.signature(sigdata)

    def verify_signature(self, address, signature, hexdata):
        """Verify <signature> of <hexdata> by <address>."""
        try:
            address = deserialize.address(self.testnet, address)
            data = deserialize.binary(hexdata)
            signature = deserialize.signature(signature)
            return control.verify_signature(self.testnet, address,
                                            signature, data)
        except exceptions.InvalidAddress:
            return False

    def sign_unicode(self, wif, message):
        """Signing <unicode> with <wif> private key."""
        hexdata = binascii.hexlify(message.encode("utf-8"))
        return self.sign_data(wif, hexdata)

    def verify_signature_unicode(self, address, signature, message):
        """Verify <signature> of <unicode> by <address>."""
        hexdata = binascii.hexlify(message.encode("utf-8"))
        return self.verify_signature(address, signature, hexdata)

    ###############
    # hash160data #
    ###############

    def add_hash160data(self, rawtx, hexdata, dust_limit=common.DUST_LIMIT):
        """Writes <hexdata> as new Pay-to-PubkeyHash output to <rawtx>."""
        tx = deserialize.unsignedtx(rawtx)
        dust_limit = deserialize.positive_integer(dust_limit)
        hash160data_txout = deserialize.hash160data_txout(hexdata, dust_limit)
        tx = control.add_hash160data_output(tx, hash160data_txout)
        return serialize.tx(tx)

    def get_hash160data(self, rawtx, output_index):
        """TODO doc string"""
        tx = deserialize.unsignedtx(rawtx)
        output_index = deserialize.positive_integer(output_index)
        data = control.get_hash160_data(tx, output_index)
        return serialize.data(data)

    def store_hash160data(self, hexdata, wifs, change_address=None,
                          txouts=None, fee=10000, lock_time=0,
                          dust_limit=common.DUST_LIMIT):
        """TODO doc string"""
        rawtx = self.create_tx(txouts=txouts, lock_time=lock_time)
        rawtx = self.add_hash160data(rawtx, hexdata, dust_limit=dust_limit)
        rawtx = self.add_inputs(rawtx, wifs, change_address=change_address,
                                fee=fee)
        return self.publish(rawtx)

    def retrieve_hash160data(self, txid, output_index):
        """TODO doc string"""
        rawtx = self.retrieve_tx(txid)
        return self.get_hash160_data(rawtx, output_index)

    ############
    # nulldata #
    ############

    def add_nulldata(self, rawtx, hexdata):
        """Writes <hexdata> as new nulldata output to <rawtx>."""
        tx = deserialize.unsignedtx(rawtx)
        nulldata_txout = deserialize.nulldata_txout(hexdata)
        tx = control.add_nulldata_output(tx, nulldata_txout)
        return serialize.tx(tx)

    def get_nulldata(self, rawtx):
        """Returns nulldata from <rawtx> as hexdata."""
        tx = deserialize.tx(rawtx)
        index, data = control.get_nulldata(tx)
        return serialize.data(data)

    def store_nulldata(self, hexdata, wifs, change_address=None, txouts=None,
                       fee=10000, lock_time=0):
        """Store <hexdata> in blockchain and return new txid.
        Utxos taken from <wifs> and change sent to <change_address>.
        <wifs>: '["privatekey_in_wif_format", ...]'
        """
        rawtx = self.create_tx(txouts=txouts, lock_time=lock_time)
        rawtx = self.add_nulldata(rawtx, hexdata)
        rawtx = self.add_inputs(rawtx, wifs, change_address=change_address,
                                fee=fee)
        return self.publish(rawtx)

    def retrieve_nulldata(self, txid):
        """Returns nulldata stored in blockchain <txid> as hexdata."""
        rawtx = self.retrieve_tx(txid)
        return self.get_nulldata(rawtx)

    #############
    # data blob #
    #############

    def get_data_blob(self, rawtx):
        """TODO add docstring"""
        tx = deserialize.tx(rawtx)
        data = control.get_data_blob(tx)
        return serialize.data(data)

    def add_data_blob(self, rawtx, hexdata, dust_limit=common.DUST_LIMIT):
        """TODO add docstring"""
        tx = deserialize.tx(rawtx)
        data = deserialize.binary(hexdata)
        tx = control.add_data_blob(tx, data, dust_limit=dust_limit)
        return serialize.tx(tx)

    def store_data_blob(self, hexdata, wifs, change_address=None,
                        txouts=None, fee=10000, lock_time=0,
                        dust_limit=common.DUST_LIMIT):
        """TODO add docstring"""
        rawtx = self.create_tx(txouts=txouts, lock_time=lock_time)
        rawtx = self.add_data_blob(rawtx, hexdata, dust_limit=dust_limit)
        rawtx = self.add_inputs(rawtx, wifs, change_address=change_address,
                                fee=fee)
        return self.publish(rawtx)

    def retrieve_data_blob(self, txid):
        """TODO add docstring"""
        rawtx = self.retrieve_tx(txid)
        return self.get_data_blob(rawtx)

    #####################
    # broadcast message #
    #####################

    def add_broadcast_message(self, rawtx, message, sender_wif,
                              dust_limit=common.DUST_LIMIT):
        """TODO add docstring"""
        tx = deserialize.tx(rawtx)
        message = deserialize.unicode_str(message)
        sender_key = deserialize.key(self.testnet, sender_wif)
        tx = control.add_broadcast_message(self.testnet, tx, message,
                                           sender_key, dust_limit=dust_limit)
        return serialize.tx(tx)

    def get_broadcast_message(self, rawtx):
        """TODO add docstring"""
        tx = deserialize.tx(rawtx)
        result = control.get_broadcast_message(self.testnet, tx)
        result["signature"] = serialize.signature(result["signature"])
        return result

    def store_broadcast_message(self, message, sender_wif, wifs,
                                change_address=None, txouts=None, fee=10000,
                                lock_time=0, dust_limit=common.DUST_LIMIT):
        """TODO add docstring"""
        rawtx = self.create_tx(txouts=txouts, lock_time=lock_time)
        rawtx = self.add_broadcast_message(rawtx, message, sender_wif,
                                           dust_limit=dust_limit)
        rawtx = self.add_inputs(rawtx, wifs, change_address=change_address,
                                fee=fee)
        return self.publish(rawtx)

    def retrieve_broadcast_message(self, txid):
        """TODO add docstring"""
        rawtx = self.retrieve_tx(txid)
        return self.get_broadcast_message(rawtx)

    ########
    # misc #
    ########

    def split_utxos(self, wif, limit, fee=10000, max_outputs=100):
        """Split utxos of <wif> unitil <limit> or <max_outputs> reached."""
        key = deserialize.key(self.testnet, wif)
        limit = deserialize.positive_integer(limit)
        fee = deserialize.positive_integer(fee)
        max_outputs = deserialize.positive_integer(max_outputs)
        spendables = control.retrieve_utxos(self.service, [key.address()])
        txids = control.split_utxos(self.service, self.testnet, key,
                                    spendables, limit, fee=fee,
                                    max_outputs=max_outputs,
                                    publish=(not self.dryrun))
        return serialize.txids(txids)
