#
#  Epour - A bittorrent client using EFL and libtorrent
#
#  Copyright 2012-2014 Kai Huuhko <kai.huuhko@gmail.com>
#
#  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, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#

import cgi
import logging
from datetime import timedelta, datetime

import libtorrent as lt

from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL, \
    EVAS_ASPECT_CONTROL_VERTICAL, Rectangle
from efl.ecore import Timer
from efl import elementary as elm
elm.init()
from efl.elementary.genlist import Genlist, GenlistItemClass, \
    ELM_GENLIST_ITEM_FIELD_TEXT, ELM_GENLIST_ITEM_FIELD_CONTENT, \
    ELM_OBJECT_SELECT_MODE_NONE, ELM_LIST_COMPRESS
from efl.elementary.window import StandardWindow
from efl.elementary.icon import Icon
from efl.elementary.box import Box
from efl.elementary.label import Label
from efl.elementary.panel import Panel, ELM_PANEL_ORIENT_BOTTOM
from efl.elementary.table import Table
from efl.elementary.menu import Menu
from efl.elementary.configuration import Configuration
from efl.elementary.toolbar import Toolbar, ELM_TOOLBAR_SHRINK_NONE

from Widgets import ConfirmExit, Error, Information, BlockGraph

from intrepr import intrepr

EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0
FILL_BOTH = EVAS_HINT_FILL, EVAS_HINT_FILL
FILL_HORIZ = EVAS_HINT_FILL, 0.5

elm_conf = Configuration()
scale = elm_conf.scale

log = logging.getLogger("epour.gui")


class MainInterface(object):
    def __init__(self, parent, session):
        self.parent = parent
        self.session = session

        self.torrentitems = {}

        win = self.win = StandardWindow(
            "epour", "Epour", size=(480 * scale, 400 * scale),
            screen_constrain=True
            )
        win.callback_delete_request_add(lambda x: self.quit())

        mbox = Box(win, size_hint_weight=EXPAND_BOTH)
        win.resize_object_add(mbox)
        mbox.show()

        # --- TOOLBAR ---
        tb = Toolbar(
            win, size_hint_align=FILL_HORIZ, homogeneous=False,
            shrink_mode=ELM_TOOLBAR_SHRINK_NONE,
            select_mode=ELM_OBJECT_SELECT_MODE_NONE
            )
        tb.menu_parent = win

        item = tb.item_append(
            "document-new", "Add torrent",
            lambda x, y: self.add_torrent()
            )

        def pause_session(it):
            self.session.pause()
            it.state_set(it.state_next())

        def resume_session(it):
            session.resume()
            del it.state

        item = tb.item_append(
            "media-playback-pause", "Pause Session",
            lambda tb, it: pause_session(it)
            )
        item.state_add(
            "media-playback-start", "Resume Session",
            lambda tb, it: resume_session(it)
            )

        def prefs_general_cb():
            from Preferences import PreferencesGeneral
            PreferencesGeneral(self, self.session).show()

        def prefs_proxy_cb():
            from Preferences import PreferencesProxy
            PreferencesProxy(self, self.session).show()

        def prefs_session_cb():
            from Preferences import PreferencesSession
            PreferencesSession(self, self.session).show()

        item = tb.item_append("preferences-system", "Preferences")
        item.menu = True
        item.menu.item_add(
            None, "General", "preferences-system",
            lambda o, i: prefs_general_cb()
            )
        item.menu.item_add(
            None, "Proxy", "preferences-system",
            lambda o, i: prefs_proxy_cb()
            )
        item.menu.item_add(
            None, "Session", "preferences-system",
            lambda o, i: prefs_session_cb()
            )

        item = tb.item_append("application-exit", "Exit",
                              lambda tb, it: elm.exit())

        mbox.pack_start(tb)
        tb.show()

        # --- TORRENT LIST ---
        self.tlist = tlist = Genlist(
            mbox, select_mode=ELM_OBJECT_SELECT_MODE_NONE,
            mode=ELM_LIST_COMPRESS, size_hint_weight=EXPAND_BOTH,
            size_hint_align=FILL_BOTH, homogeneous=True
            )

        def item_activated_cb(gl, item):
            h = item.data
            ItemMenu(self.win, item, self.session, h)

        tlist.callback_activated_add(item_activated_cb)
        tlist.show()

        mbox.pack_end(tlist)

        topbox = Box(win, size_hint_weight=EXPAND_BOTH, horizontal=False)

        pad1 = Rectangle(win.evas, size_hint_weight=EXPAND_BOTH)

        p = Panel(
            topbox, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH,
            color=(200, 200, 200, 200), orient=ELM_PANEL_ORIENT_BOTTOM
            )
        p.content = SessionStatus(win, session)
        p.hidden = True
        p.show()

        win.resize_object_add(topbox)

        topbox.pack_end(pad1)
        topbox.pack_end(p)
        topbox.stack_above(mbox)
        topbox.show()

        def torrent_added_cb(a):
            h = a.handle
            self.add_torrent_item(h)

        def torrent_removed_cb(a):
            self.remove_torrent_item(str(a.info_hash))

        def torrent_update_cb(a):
            self.remove_torrent_item(str(a.old_ih))
            new_h = self.session.find_torrent(str(a.new_ih))
            self.add_torrent_item(new_h)

        session.alert_manager.callback_add(
            "torrent_added_alert", torrent_added_cb)
        session.alert_manager.callback_add(
            "torrent_removed_alert", torrent_removed_cb)
        session.alert_manager.callback_add(
            "torrent_update_alert", torrent_update_cb)

        def torrent_paused_cb(a):
            self.update_icon(a.handle)

        for a_name in "torrent_paused_alert", "torrent_resumed_alert":
            session.alert_manager.callback_add(a_name, torrent_paused_cb)

        def state_changed_cb(a):
            h = a.handle
            if not h.is_valid():
                log.debug("State changed for invalid handle.")
                return

            ihash = str(h.info_hash())
            if not ihash in self.torrentitems:
                log.debug("%s not in list", str(ihash))
                return

            self.update_icon(h)

        session.alert_manager.callback_add(
            "state_changed_alert", state_changed_cb)

        def torrent_finished_cb(a):
            msg = "Torrent {} has finished downloading.".format(
                cgi.escape(a.handle.name())
            )
            log.info(msg)

            Information(self.win, msg)

        Timer(15.0, lambda: session.alert_manager.callback_add(
            "torrent_finished_alert", torrent_finished_cb))

    def add_torrent(self, t_uri=None):
        from TorrentSelector import TorrentSelector
        TorrentSelector(self.win, self.session, t_uri)

    def run(self):
        self.win.show()

        self.timer = Timer(1.0, self.update)
        elm.run()
        elm.shutdown()

    def update(self):
        for v in self.tlist.realized_items_get():
            v.fields_update("*", ELM_GENLIST_ITEM_FIELD_TEXT)
        return True

    def update_icon(self, h):
        if not h.is_valid():
            return
        ihash = str(h.info_hash())
        if not ihash in self.torrentitems:
            return
        self.torrentitems[ihash].fields_update(
            "elm.swallow.icon", ELM_GENLIST_ITEM_FIELD_CONTENT
        )

    def _torrent_item_tooltip_cb(self, gl, it, tooltip, h):
        if not h.is_valid():
            return

        tt = TorrentTooltip(tooltip, h)

        return tt

    def add_torrent_item(self, h):
        ihash = str(h.info_hash())

        itc = TorrentClass(self.session, "double_label")
        item = self.tlist.item_append(itc, h)
        item.tooltip_content_cb_set(self._torrent_item_tooltip_cb, h)
        self.torrentitems[ihash] = item

    def remove_torrent_item(self, info_hash):
        it = self.torrentitems.pop(info_hash, None)
        if it is not None:
            it.delete()

    def show_error(self, title, text):
        Error(self.win, title, text)

    def quit(self, *args):
        if self.session.conf.getboolean("Settings", "confirm_exit"):
            ConfirmExit(self.win, elm.exit)
        else:
            elm.exit()


class SessionStatus(Table):

    log = logging.getLogger("epour.gui.session")

    def __init__(self, parent, session):
        Table.__init__(self, parent)
        self.session = session

        s = session.status()

        self.homogeneous = True
        self.padding = 5, 2

        ses_pause_ic = self.ses_pause_ic = Icon(parent)
        ses_pause_ic.size_hint_align = -1.0, -1.0
        try:
            if session.is_paused():
                ses_pause_ic.standard = "player_pause"
            else:
                ses_pause_ic.standard = "player_play"
        except RuntimeError:
            log.debug("Setting session ic failed")
        self.pack(ses_pause_ic, 1, 0, 1, 1)
        ses_pause_ic.show()

        title_l = Label(parent, text="<b>Session</b>")
        self.pack(title_l, 0, 0, 1, 1)
        title_l.show()

        d_ic = Icon(parent, size_hint_align=FILL_BOTH)
        try:
            d_ic.standard = "down"
        except RuntimeError:
            log.debug("Setting d_ic failed")
        self.pack(d_ic, 0, 1, 1, 1)
        d_ic.show()

        d_l = self.d_l = Label(parent)
        d_l.text = "{}/s".format(intrepr(s.payload_download_rate))
        self.pack(d_l, 1, 1, 1, 1)
        d_l.show()

        u_ic = Icon(self, size_hint_align=FILL_BOTH)
        try:
            u_ic.standard = "up"
        except RuntimeError:
            log.debug("Setting u_ic failed")
        self.pack(u_ic, 0, 2, 1, 1)
        u_ic.show()

        u_l = self.u_l = Label(parent)
        u_l.text = "{}/s".format(intrepr(s.payload_upload_rate))
        self.pack(u_l, 1, 2, 1, 1)
        u_l.show()

        peer_t = Label(parent, text="Peers")
        self.pack(peer_t, 0, 3, 1, 1)
        peer_t.show()

        peer_l = self.peer_l = Label(parent, text=str(s.num_peers))
        self.pack(peer_l, 1, 3, 1, 1)
        peer_l.show()

        listen_t = Label(parent, text="Listening")
        self.pack(listen_t, 0, 4, 1, 1)
        listen_t.show()

        listen_l = self.listen_l = Label(
            parent, text=str(session.is_listening()))
        self.pack(listen_l, 1, 4, 1, 1)
        listen_l.show()

        self.show()

        self.update_timer = Timer(1.0, self.update)

    def update(self):
        s = self.session.status()
        self.d_l.text = "{}/s".format(intrepr(s.payload_download_rate))
        self.u_l.text = "{}/s".format(intrepr(s.payload_upload_rate))
        self.peer_l.text = str(s.num_peers)
        self.listen_l.text = str(self.session.is_listening())
        if self.session.is_paused():
            icon = "player_pause"
        else:
            icon = "player_play"
        try:
            self.ses_pause_ic.standard = icon
        except Exception:
            log.debug("")

        return True


class TorrentClass(GenlistItemClass):

    state_str = (
        'Queued', 'Checking', 'Downloading metadata', 'Downloading',
        'Finished', 'Seeding', 'Allocating', 'Checking resume data'
        )

    log = logging.getLogger("epour.gui.torrent_list")

    def __init__(self, session, *args, **kwargs):
        GenlistItemClass.__init__(self, *args, **kwargs)

        self.session = session

    def text_get(self, obj, part, item_data):
        h = item_data

        if part == "elm.text":
            name = h.name()
            return '%s' % (
                name
            )
        elif part == "elm.text.sub":
            if not h.is_valid():
                return "Invalid torrent"
            s = h.status(0)
            qp = h.queue_position()
            if qp == -1:
                qp = "seeding"

            return "{:.0%} complete, ETA: {} " \
                "(Down: {}/s Up: {}/s Peers: {} Queue pos: {})".format(
                    s.progress,
                    timedelta(seconds=self.get_eta(s)),
                    intrepr(s.download_payload_rate, precision=0),
                    intrepr(s.upload_payload_rate, precision=0),
                    s.num_peers,
                    qp,
                )

    def content_get(self, obj, part, item_data):
        if part != "elm.swallow.icon":
            return

        h = item_data

        if not h.is_valid():
            return

        s = h.status(0)
        ic = Icon(obj)
        try:
            if h.is_paused():
                ic.standard = "player_pause"
            elif h.is_seed():
                ic.standard = "up"
            else:
                ic.standard = "down"
        except RuntimeError:
            log.debug("Setting torrent ic failed")
        ic.tooltip_text_set(self.state_str[s.state])
        ic.size_hint_aspect_set(EVAS_ASPECT_CONTROL_VERTICAL, 1, 1)
        return ic

    def get_eta(self, s):
        # if self.is_finished and self.options["stop_at_ratio"]:
        #     # We're a seed, so calculate the time to the 'stop_share_ratio'
        #     if not s.upload_payload_rate:
        #         return 0
        #     stop_ratio = self.session.settings().share_ratio_limit
        #     return (
        #         (s.all_time_download * stop_ratio) -
        #         s.all_time_upload
        #         ) / s.upload_payload_rate

        left = s.total_wanted - s.total_wanted_done

        if left <= 0 or s.download_payload_rate == 0:
            return 0

        try:
            eta = left / s.download_payload_rate
        except ZeroDivisionError:
            eta = 0

        return eta


class ItemMenu(Menu):
    def __init__(self, parent, item, session, h):
        Menu.__init__(self, parent)

        self.item_add(
            None,
            "Resume" if h.is_paused() else "Pause",
            None,
            self.resume_torrent_cb if h.is_paused() else self.pause_torrent_cb,
            h
        )
        q = self.item_add(None, "Queue", None, None)
        self.item_add(
            q, "Up", None, lambda x, y: h.queue_position_up()
            )
        self.item_add(
            q, "Down", None, lambda x, y: h.queue_position_down()
            )
        self.item_add(
            q, "Top", None, lambda x, y: h.queue_position_top()
            )
        self.item_add(
            q, "Bottom", None, lambda x, y: h.queue_position_bottom()
            )
        rem = self.item_add(
            None, "Remove torrent", None,
            self.remove_torrent_cb, item, session, h, False
            )
        self.item_add(
            rem, "and data files", None,
            self.remove_torrent_cb, item, session, h, True
            )
        self.item_separator_add(None)
        it = self.item_add(
            None, "Force reannounce", None, lambda x, y: h.force_reannounce()
            )
        it.tooltip_text_set(
            "<b>Force reannounce</b> will force this torrent<br>"
            "to do another tracker request, to receive new<br>"
            "peers.<br><br>"
            "If the tracker's min_interval has not passed<br>"
            "since the last announce, the forced announce<br>"
            "will be scheduled to happen immediately as<br>"
            "the min_interval expires. This is to honor<br>"
            "trackers minimum re-announce interval settings."
            )
        self.item_add(
            None, "Force DHT reannounce", None,
            lambda x, y: h.force_dht_reannounce()
            )
        it = self.item_add(
            None, "Scrape tracker", None,
            lambda x, y: h.scrape_tracker()
            )
        it.tooltip_text_set(
            "<b>Scrape tracker</b> will send a scrape request to the<br>"
            "tracker. A scrape request queries the tracker for<br>"
            "statistics such as total number of incomplete peers,<br>"
            "complete peers, number of downloads etc."
            )
        it = self.item_add(
            None, "Force re-check", None,
            lambda x, y: h.force_recheck()
            )
        it.tooltip_text_set(
            "force_recheck puts the torrent back in a state<br>"
            "where it assumes to have no resume data.<br>"
            "All peers will be disconnected and the torrent<br>"
            "will stop announcing to the tracker. The torrent<br>"
            "will be added to the checking queue, and will be<br>"
            "checked (all the files will be read and compared<br>"
            "to the piece hashes).<br>"
            "Once the check is complete, the torrent will start<br>"
            "connecting to peers again, as normal."
            )
        self.item_separator_add(None)
        self.item_add(
            None, "Torrent properties", None,
            self.torrent_props_cb, h
            )

        self.move(*parent.evas.pointer_canvas_xy_get())
        self.show()

    def remove_torrent_cb(
        self, menu, item, glitem, session, h, with_data=False
            ):
        menu.close()
        session.remove_torrent(h, with_data)

    def resume_torrent_cb(self, menu, item, h):
        h.resume()
        h.auto_managed(True)

    def pause_torrent_cb(self, menu, item, h):
        h.auto_managed(False)
        h.pause(flags=lt.pause_flags_t.graceful_pause)

    def torrent_props_cb(self, menu, item, h):
        from TorrentProps import TorrentProps
        TorrentProps(h).show()


class TorrentTooltip(Table):

    items = (
        ("Time when added", datetime.fromtimestamp, "added_time"),
        ("Time when completed", datetime.fromtimestamp, "completed_time"),
        ("All time downloaded", intrepr, "all_time_download"),
        ("All time uploaded", intrepr, "all_time_upload"),
        ("Total wanted done", intrepr, "total_wanted_done"),
        ("Total wanted", intrepr, "total_wanted"),
        ("Total downloaded this session",
            intrepr, "total_payload_download"),
        ("Total uploaded this session", intrepr, "total_payload_upload"),
        ("Total failed", intrepr, "total_failed_bytes"),
        ("Number of seeds", None, "num_seeds"),
        ("Number of peers", None, "num_peers"),
        )

    def __init__(self, parent, h):

        Table.__init__(self, parent, size_hint_weight=EXPAND_BOTH)

        s = h.status(8)

        value_labels = []

        for i, (desc, conv, attr_name) in enumerate(self.items):
            l1 = Label(self, text=desc)
            l1.show()
            self.pack(l1, 0, i, 1, 1)
            v = getattr(s, attr_name)
            if conv:
                v = conv(v)
            l2 = Label(self, text=str(v))
            value_labels.append(l2)
            l2.show()
            self.pack(l2, 1, i, 1, 1)

        i += 1

        l = Label(self)
        self.pack(l, 0, i, 1, 1)
        l.show()

        WIDTH = 30
        HEIGHT = 4

        g = BlockGraph(
            self, s.pieces, s.num_pieces,
            size=(WIDTH, HEIGHT), size_hint_align=FILL_BOTH
            )

        l.text = "Pieces (scaled 1:%d)" % g.block_size

        self.pack(g, 1, i, 1, 1)
        g.show()

        self.timer = Timer(1.0, self.update, h, self.items, value_labels, g)
        self.on_del_add(lambda x: self.timer.delete())

    @staticmethod
    def update(h, items, value_labels, g):
        s = h.status(8)
        for i, l in enumerate(value_labels):
            conv, attr_name = items[i][1:]
            v = getattr(s, attr_name)
            if conv:
                v = conv(v)
            l.text = str(v)

        num_pieces = s.num_pieces
        if g.num_complete_pieces != num_pieces:
            g.update(s.pieces)
            g.num_complete_pieces = num_pieces

        return True
