from weakref import ref
from decimal import Decimal
import re
import datetime
import traceback, sys

from kivy.app import App
from kivy.cache import Cache
from kivy.clock import Clock
from kivy.compat import string_types
from kivy.properties import (ObjectProperty, DictProperty, NumericProperty,
                             ListProperty, StringProperty)

from kivy.uix.label import Label

from kivy.lang import Builder
from kivy.factory import Factory
from kivy.utils import platform

from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds
from electrum import bitcoin
from electrum.util import timestamp_to_datetime
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED

from context_menu import ContextMenu


from electrum_gui.kivy.i18n import _

class EmptyLabel(Factory.Label):
    pass

class CScreen(Factory.Screen):
    __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
    action_view = ObjectProperty(None)
    loaded = False
    kvname = None
    context_menu = None
    menu_actions = []
    app = App.get_running_app()

    def _change_action_view(self):
        app = App.get_running_app()
        action_bar = app.root.manager.current_screen.ids.action_bar
        _action_view = self.action_view

        if (not _action_view) or _action_view.parent:
            return
        action_bar.clear_widgets()
        action_bar.add_widget(_action_view)

    def on_enter(self):
        # FIXME: use a proper event don't use animation time of screen
        Clock.schedule_once(lambda dt: self.dispatch('on_activate'), .25)
        pass

    def update(self):
        pass

    @profiler
    def load_screen(self):
        self.screen = Builder.load_file('gui/kivy/uix/ui_screens/' + self.kvname + '.kv')
        self.add_widget(self.screen)
        self.loaded = True
        self.update()
        setattr(self.app, self.kvname + '_screen', self)

    def on_activate(self):
        if self.kvname and not self.loaded:
            self.load_screen()
        #Clock.schedule_once(lambda dt: self._change_action_view())

    def on_leave(self):
        self.dispatch('on_deactivate')

    def on_deactivate(self):
        self.hide_menu()

    def hide_menu(self):
        if self.context_menu is not None:
            self.remove_widget(self.context_menu)
            self.context_menu = None

    def show_menu(self, obj):
        self.hide_menu()
        self.context_menu = ContextMenu(obj, self.menu_actions)
        self.add_widget(self.context_menu)


TX_ICONS = [
    "close",
    "close",
    "close",
    "unconfirmed",
    "close",
    "clock1",
    "clock2",
    "clock3",
    "clock4",
    "clock5",
    "confirmed",
]

class HistoryScreen(CScreen):

    tab = ObjectProperty(None)
    kvname = 'history'
    cards = {}

    def __init__(self, **kwargs):
        self.ra_dialog = None
        super(HistoryScreen, self).__init__(**kwargs)
        self.menu_actions = [ ('Label', self.label_dialog), ('Details', self.show_tx)]

    def show_tx(self, obj):
        tx_hash = obj.tx_hash
        tx = self.app.wallet.transactions.get(tx_hash)
        if not tx:
            return
        self.app.tx_dialog(tx)

    def label_dialog(self, obj):
        from dialogs.label_dialog import LabelDialog
        key = obj.tx_hash
        text = self.app.wallet.get_label(key)
        def callback(text):
            self.app.wallet.set_label(key, text)
            self.update()
        d = LabelDialog(_('Enter Transaction Label'), text, callback)
        d.open()

    def get_card(self, tx_hash, height, conf, timestamp, value, balance):
        status, status_str = self.app.wallet.get_tx_status(tx_hash, height, conf, timestamp)
        icon = "atlas://gui/kivy/theming/light/" + TX_ICONS[status]
        label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
        date = timestamp_to_datetime(timestamp)
        ri = self.cards.get(tx_hash)
        if ri is None:
            ri = Factory.HistoryItem()
            ri.screen = self
            ri.tx_hash = tx_hash
            self.cards[tx_hash] = ri
        ri.icon = icon
        ri.date = status_str
        ri.message = label
        ri.value = value or 0
        ri.value_known = value is not None
        ri.confirmations = conf
        if self.app.fiat_unit and date:
            rate = self.app.fx.history_rate(date)
            if rate:
                s = self.app.fx.value_str(value, rate)
                ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit
        return ri

    def update(self, see_all=False):
        if self.app.wallet is None:
            return
        history = reversed(self.app.wallet.get_history())
        history_card = self.screen.ids.history_container
        history_card.clear_widgets()
        count = 0
        for item in history:
            ri = self.get_card(*item)
            count += 1
            history_card.add_widget(ri)

        if count == 0:
            msg = _('This screen shows your list of transactions. It is currently empty.')
            history_card.add_widget(EmptyLabel(text=msg))


class SendScreen(CScreen):

    kvname = 'send'
    payment_request = None

    def set_URI(self, text):
        import electrum
        try:
            uri = electrum.util.parse_URI(text, self.app.on_pr)
        except:
            self.app.show_info(_("Not a Bitcoin URI"))
            return
        amount = uri.get('amount')
        self.screen.address = uri.get('address', '')
        self.screen.message = uri.get('message', '')
        self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
        self.payment_request = None
        self.screen.is_pr = False

    def update(self):
        pass

    def do_clear(self):
        self.screen.amount = ''
        self.screen.message = ''
        self.screen.address = ''
        self.payment_request = None
        self.screen.is_pr = False

    def set_request(self, pr):
        self.screen.address = pr.get_requestor()
        amount = pr.get_amount()
        self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
        self.screen.message = pr.get_memo()
        if pr.is_pr():
            self.screen.is_pr = True
            self.payment_request = pr
        else:
            self.screen.is_pr = False
            self.payment_request = None

    def do_save(self):
        if not self.screen.address:
            return
        if self.screen.is_pr:
            # it sould be already saved
            return
        # save address as invoice
        from electrum.paymentrequest import make_unsigned_request, PaymentRequest
        req = {'address':self.screen.address, 'memo':self.screen.message}
        amount = self.app.get_amount(self.screen.amount) if self.screen.amount else 0
        req['amount'] = amount
        pr = make_unsigned_request(req).SerializeToString()
        pr = PaymentRequest(pr)
        self.app.invoices.add(pr)
        self.app.update_tab('invoices')
        self.app.show_info(_("Invoice saved"))
        if pr.is_pr():
            self.screen.is_pr = True
            self.payment_request = pr
        else:
            self.screen.is_pr = False
            self.payment_request = None

    def do_paste(self):
        contents = unicode(self.app._clipboard.paste())
        if not contents:
            self.app.show_info(_("Clipboard is empty"))
            return
        self.set_URI(contents)

    def do_send(self):
        if self.screen.is_pr:
            if self.payment_request.has_expired():
                self.app.show_error(_('Payment request has expired'))
                return
            outputs = self.payment_request.get_outputs()
        else:
            address = str(self.screen.address)
            if not address:
                self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request'))
                return
            if not bitcoin.is_address(address):
                self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
                return
            try:
                amount = self.app.get_amount(self.screen.amount)
            except:
                self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
                return
            outputs = [(bitcoin.TYPE_ADDRESS, address, amount)]
        message = unicode(self.screen.message)
        amount = sum(map(lambda x:x[2], outputs))
        if self.app.electrum_config.get('use_rbf'):
            from dialogs.question import Question
            d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send(amount, message, outputs, b))
            d.open()
        else:
            self._do_send(amount, message, outputs, False)

    def _do_send(self, amount, message, outputs, rbf):
        # make unsigned transaction
        coins = self.app.wallet.get_spendable_coins()
        config = self.app.electrum_config
        try:
            tx = self.app.wallet.make_unsigned_transaction(coins, outputs, config, None)
        except NotEnoughFunds:
            self.app.show_error(_("Not enough funds"))
            return
        except Exception as e:
            traceback.print_exc(file=sys.stdout)
            self.app.show_error(str(e))
            return
        if rbf:
            tx.set_sequence(0)
        fee = tx.get_fee()
        msg = [
            _("Amount to be sent") + ": " + self.app.format_amount_and_units(amount),
            _("Mining fee") + ": " + self.app.format_amount_and_units(fee),
        ]
        if fee >= config.get('confirm_fee', 100000):
            msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high."))
        msg.append(_("Enter your PIN code to proceed"))
        self.app.protected('\n'.join(msg), self.send_tx, (tx, message))

    def send_tx(self, tx, message, password):
        def on_success(tx):
            if tx.is_complete():
                self.app.broadcast(tx, self.payment_request)
                self.app.wallet.set_label(tx.hash(), message)
            else:
                self.app.tx_dialog(tx)
        def on_failure(error):
            self.app.show_error(error)
        if self.app.wallet.can_sign(tx):
            self.app.show_info("Signing...")
            self.app.sign_tx(tx, password, on_success, on_failure)
        else:
            self.app.tx_dialog(tx)


class ReceiveScreen(CScreen):

    kvname = 'receive'

    def update(self):
        if not self.screen.address:
            self.get_new_address()
        else:
            status = self.app.wallet.get_request_status(self.screen.address)
            self.screen.status = _('Payment received') if status == PR_PAID else ''

    def clear(self):
        self.screen.address = ''
        self.screen.amount = ''
        self.screen.message = ''

    def get_new_address(self):
        if not self.app.wallet:
            return False
        self.clear()
        addr = self.app.wallet.get_unused_address()
        if addr is None:
            addr = self.app.wallet.get_receiving_address()
            b = False
        else:
            b = True
        self.screen.address = addr
        return b

    def on_address(self, addr):
        req = self.app.wallet.get_payment_request(addr, self.app.electrum_config)
        self.screen.status = ''
        if req:
            self.screen.message = unicode(req.get('memo', ''))
            amount = req.get('amount')
            self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
            status = req.get('status', PR_UNKNOWN)
            self.screen.status = _('Payment received') if status == PR_PAID else ''
        Clock.schedule_once(lambda dt: self.update_qr())

    def get_URI(self):
        from electrum.util import create_URI
        amount = self.screen.amount
        if amount:
            a, u = self.screen.amount.split()
            assert u == self.app.base_unit
            amount = Decimal(a) * pow(10, self.app.decimal_point())
        return create_URI(self.screen.address, amount, self.screen.message)

    @profiler
    def update_qr(self):
        uri = self.get_URI()
        qr = self.screen.ids.qr
        qr.set_data(uri)

    def do_share(self):
        uri = self.get_URI()
        self.app.do_share(uri, _("Share Bitcoin Request"))

    def do_copy(self):
        uri = self.get_URI()
        self.app._clipboard.copy(uri)
        self.app.show_info(_('Request copied to clipboard'))

    def save_request(self):
        addr = str(self.screen.address)
        amount = str(self.screen.amount)
        message = unicode(self.screen.message)
        amount = self.app.get_amount(amount) if amount else 0
        req = self.app.wallet.make_payment_request(addr, amount, message, None)
        self.app.wallet.add_payment_request(req, self.app.electrum_config)
        self.app.update_tab('requests')

    def on_amount_or_message(self):
        self.save_request()
        Clock.schedule_once(lambda dt: self.update_qr())

    def do_new(self):
        addr = self.get_new_address()
        if not addr:
            self.app.show_info(_('Please use the existing requests first.'))
        else:
            self.save_request()
            self.app.show_info(_('New request added to your list.'))


invoice_text = {
    PR_UNPAID:_('Pending'),
    PR_UNKNOWN:_('Unknown'),
    PR_PAID:_('Paid'),
    PR_EXPIRED:_('Expired')
}
request_text = {
    PR_UNPAID: _('Pending'),
    PR_UNKNOWN: _('Unknown'),
    PR_PAID: _('Received'),
    PR_EXPIRED: _('Expired')
}
pr_icon = {
    PR_UNPAID: 'atlas://gui/kivy/theming/light/important',
    PR_UNKNOWN: 'atlas://gui/kivy/theming/light/important',
    PR_PAID: 'atlas://gui/kivy/theming/light/confirmed',
    PR_EXPIRED: 'atlas://gui/kivy/theming/light/close'
}


class InvoicesScreen(CScreen):
    kvname = 'invoices'
    cards = {}

    def get_card(self, pr):
        key = pr.get_id()
        ci = self.cards.get(key)
        if ci is None:
            ci = Factory.InvoiceItem()
            ci.key = key
            ci.screen = self
            self.cards[key] = ci

        ci.requestor = pr.get_requestor()
        ci.memo = pr.get_memo()
        amount = pr.get_amount()
        if amount:
            ci.amount = self.app.format_amount_and_units(amount)
            status = self.app.invoices.get_status(ci.key)
            ci.status = invoice_text[status]
            ci.icon = pr_icon[status]
        else:
            ci.amount = _('No Amount')
            ci.status = ''
        exp = pr.get_expiration_date()
        ci.date = format_time(exp) if exp else _('Never')
        return ci

    def update(self):
        self.menu_actions = [('Pay', self.do_pay), ('Details', self.do_view), ('Delete', self.do_delete)]
        invoices_list = self.screen.ids.invoices_container
        invoices_list.clear_widgets()
        _list = self.app.invoices.sorted_list()
        for pr in _list:
            ci = self.get_card(pr)
            invoices_list.add_widget(ci)
        if not _list:
            msg = _('This screen shows the list of payment requests that have been sent to you. You may also use it to store contact addresses.')
            invoices_list.add_widget(EmptyLabel(text=msg))

    def do_pay(self, obj):
        pr = self.app.invoices.get(obj.key)
        self.app.on_pr(pr)

    def do_view(self, obj):
        pr = self.app.invoices.get(obj.key)
        pr.verify(self.app.contacts)
        self.app.show_pr_details(pr.get_dict(), obj.status, True)

    def do_delete(self, obj):
        from dialogs.question import Question
        def cb(result):
            if result:
                self.app.invoices.remove(obj.key)
                self.app.update_tab('invoices')
        d = Question(_('Delete invoice?'), cb)
        d.open()


class RequestsScreen(CScreen):
    kvname = 'requests'
    cards = {}

    def get_card(self, req):
        address = req['address']
        timestamp = req.get('time', 0)
        amount = req.get('amount')
        expiration = req.get('exp', None)
        status = req.get('status')
        signature = req.get('sig')

        ci = self.cards.get(address)
        if ci is None:
            ci = Factory.RequestItem()
            ci.screen = self
            ci.address = address
            self.cards[address] = ci

        ci.memo = self.app.wallet.get_label(address)
        if amount:
            status = req.get('status')
            ci.status = request_text[status]
        else:
            received = self.app.wallet.get_addr_received(address)
            ci.status = self.app.format_amount_and_units(amount)
        ci.icon = pr_icon[status]
        ci.amount = self.app.format_amount_and_units(amount) if amount else _('No Amount')
        ci.date = format_time(timestamp)
        return ci

    def update(self):
        self.menu_actions = [('Show', self.do_show), ('Details', self.do_view), ('Delete', self.do_delete)]
        requests_list = self.screen.ids.requests_container
        requests_list.clear_widgets()
        _list = self.app.wallet.get_sorted_requests(self.app.electrum_config) if self.app.wallet else []
        for req in _list:
            ci = self.get_card(req)
            requests_list.add_widget(ci)
        if not _list:
            msg = _('This screen shows the list of payment requests you made.')
            requests_list.add_widget(EmptyLabel(text=msg))

    def do_show(self, obj):
        self.app.show_request(obj.address)

    def do_view(self, obj):
        req = self.app.wallet.get_payment_request(obj.address, self.app.electrum_config)
        status = req.get('status')
        amount = req.get('amount')
        address = req['address']
        if amount:
            status = req.get('status')
            status = request_text[status]
        else:
            received_amount = self.app.wallet.get_addr_received(address)
            status = self.app.format_amount_and_units(received_amount)

        self.app.show_pr_details(req, status, False)

    def do_delete(self, obj):
        from dialogs.question import Question
        def cb(result):
            if result:
                self.app.wallet.remove_payment_request(obj.address, self.app.electrum_config)
                self.update()
        d = Question(_('Delete request?'), cb)
        d.open()




class TabbedCarousel(Factory.TabbedPanel):
    '''Custom TabbedPanel using a carousel used in the Main Screen
    '''

    carousel = ObjectProperty(None)

    def animate_tab_to_center(self, value):
        scrlv = self._tab_strip.parent
        if not scrlv:
            return
        idx = self.tab_list.index(value)
        n = len(self.tab_list)
        if idx in [0, 1]:
            scroll_x = 1
        elif idx in [n-1, n-2]:
            scroll_x = 0
        else:
            scroll_x = 1. * (n - idx - 1) / (n - 1)
        mation = Factory.Animation(scroll_x=scroll_x, d=.25)
        mation.cancel_all(scrlv)
        mation.start(scrlv)

    def on_current_tab(self, instance, value):
        self.animate_tab_to_center(value)

    def on_index(self, instance, value):
        current_slide = instance.current_slide
        if not hasattr(current_slide, 'tab'):
            return
        tab = current_slide.tab
        ct = self.current_tab
        try:
            if ct.text != tab.text:
                carousel = self.carousel
                carousel.slides[ct.slide].dispatch('on_leave')
                self.switch_to(tab)
                carousel.slides[tab.slide].dispatch('on_enter')
        except AttributeError:
            current_slide.dispatch('on_enter')

    def switch_to(self, header):
        # we have to replace the functionality of the original switch_to
        if not header:
            return
        if not hasattr(header, 'slide'):
            header.content = self.carousel
            super(TabbedCarousel, self).switch_to(header)
            try:
                tab = self.tab_list[-1]
            except IndexError:
                return
            self._current_tab = tab
            tab.state = 'down'
            return

        carousel = self.carousel
        self.current_tab.state = "normal"
        header.state = 'down'
        self._current_tab = header
        # set the carousel to load  the appropriate slide
        # saved in the screen attribute of the tab head
        slide = carousel.slides[header.slide]
        if carousel.current_slide != slide:
            carousel.current_slide.dispatch('on_leave')
            carousel.load_slide(slide)
            slide.dispatch('on_enter')

    def add_widget(self, widget, index=0):
        if isinstance(widget, Factory.CScreen):
            self.carousel.add_widget(widget)
            return
        super(TabbedCarousel, self).add_widget(widget, index=index)
