#
#  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 2 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 sys
import os
import pipes
from datetime import datetime, timedelta
import logging
log = logging.getLogger("epour")

import libtorrent as lt

from efl import ecore
from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL
from efl.elementary.genlist import Genlist, GenlistItemClass, \
    ELM_GENLIST_ITEM_NONE, ELM_GENLIST_ITEM_TREE, ELM_GENLIST_ITEM_FIELD_TEXT
from efl.elementary.window import StandardWindow
from efl.elementary.button import Button
from efl.elementary.box import Box
from efl.elementary.check import Check
from efl.elementary.label import Label, ELM_WRAP_CHAR
from efl.elementary.entry import Entry
from efl.elementary.frame import Frame
from efl.elementary.object import ELM_SEL_FORMAT_TEXT, \
    ELM_SEL_TYPE_CLIPBOARD
from efl.elementary.table import Table
from efl.elementary.progressbar import Progressbar
from efl.elementary.configuration import Configuration
elm_conf = Configuration()
SCALE = elm_conf.scale
from efl.elementary.scroller import Scroller
from efl.elementary.spinner import Spinner
from efl.elementary.fileselector import Fileselector
from efl.elementary.fileselector_entry import FileselectorEntry

from intrepr import intrepr
from Widgets import Information, UnitSpinner

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
ALIGN_LEFT = 0.0, 0.5
ALIGN_RIGHT = 1.0, 0.5


from collections import defaultdict
FILE_MARKER = '<files>'


class FileClass(GenlistItemClass):

    def text_get(self, obj, part, data):
        fn, file_entry, n = data
        progress = obj.data["progress"][n]
        return "{} - {}/{} ({:.0%})".format(
            fn,
            intrepr(progress),
            intrepr(file_entry.size),
            float(progress)/float(file_entry.size)
        )

    def content_get(self, obj, part, data):
        fn, file_entry, n = data
        h = obj.data["handle"]
        if part == "elm.swallow.icon":
            check = Check(obj)
            check.tooltip_text_set("Enable/disable file download")
            check.state = h.file_priority(n)
            check.callback_changed_add(lambda x: h.file_priority(n, x.state))
            return check


class DirectoryClass(GenlistItemClass):
    def text_get(self, obj, part, data):
        dir_name, d = data
        return "{}".format(
            dir_name
        )


ITEM_CLASS_FILES = FileClass()
ITEM_CLASS_DIRS = DirectoryClass()


def attach(branch, file_entry, n, trunk):
    """Insert a branch of directories on its trunk."""
    parts = branch.split('/', 1)
    if len(parts) == 1:  # branch is a file
        trunk[FILE_MARKER].append((parts[0], file_entry, n))
    else:
        node, others = parts
        if node not in trunk:
            trunk[node] = defaultdict(dict, ((FILE_MARKER, []),))
        attach(others, file_entry, n, trunk[node])


class TorrentProps(StandardWindow):

    def __init__(self, h):
        if not h.is_valid():
            Information(self, "Invalid torrent handle.")
            return

        StandardWindow.__init__(
            self, "epour", "Epour - %s" % (h.name()),
            size=(SCALE * 480, SCALE * 320),
            autodel=True
            )

        scroller = Scroller(self)
        self.resize_object_add(scroller)

        box = Box(self, size_hint_weight=EXPAND_BOTH)

        scroller.content = box

        tname = Label(
            self, size_hint_align=FILL_HORIZ, line_wrap=ELM_WRAP_CHAR,
            ellipsis=True, scale=2.0
            )
        tname.text = "{}".format(cgi.escape(h.name()))
        tname.show()
        box.pack_end(tname)

        if h.has_metadata():
            f = Frame(self, text="Torrent info", size_hint_align=FILL_HORIZ)
            ti = TorrentInfo(
                f, h, size_hint_align=FILL_HORIZ)
            f.content = ti
            box.pack_end(f)
            ti.show()
            f.show()

        f = Frame(self, text="Torrent settings", size_hint_align=FILL_HORIZ)
        ts = TorrentSettings(
            f, h, size_hint_align=FILL_HORIZ)
        f.content = ts
        box.pack_end(f)
        ts.show()
        f.show()

        f = Frame(self, text="Torrent status", size_hint_align=FILL_HORIZ)
        ts = TorrentStatus(f, h, size_hint_align=FILL_HORIZ)
        f.content = ts
        box.pack_end(f)
        ts.show()
        f.show()

        magnet_uri = lt.make_magnet_uri(h)

        f = Frame(self, size_hint_align=FILL_HORIZ, text="Magnet URI")
        me_box = Box(f, horizontal=True)
        me = Entry(
            me_box, size_hint_align=FILL_HORIZ, size_hint_weight=EXPAND_HORIZ,
            editable=False, entry=magnet_uri, line_wrap=ELM_WRAP_CHAR
            )
        me_box.pack_end(me)
        me.show()
        me_btn = Button(me_box, text="Copy")
        me_btn.callback_clicked_add(
            lambda x: me.top_widget.cnp_selection_set(
                ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT, me.text
                )
            )
        me_btn.show()
        me_box.pack_end(me_btn)
        me_box.show()
        f.content = me_box
        f.show()
        box.pack_end(f)

        # xbtn = Button(box, text="Close")
        # xbtn.callback_clicked_add(lambda x: self.delete())
        # box.pack_end(xbtn)
        # xbtn.show()

        box.show()
        scroller.show()


class TorrentFiles(StandardWindow):
    def __init__(self, h):
        StandardWindow.__init__(
            self,
            "epour",
            "Epour - Files for torrent: %s" % (h.name()),
            size=(SCALE * 480, SCALE * 320),
            autodel=True
            )

        box = Box(self, size_hint_weight=EXPAND_BOTH)
        self.resize_object_add(box)

        file_dict = defaultdict(dict, ((FILE_MARKER, []),))

        filelist = Genlist(
            box, size_hint_align=FILL_BOTH, size_hint_weight=EXPAND_BOTH,
            homogeneous=True, tree_effect_enabled=True
            )
        filelist.data["handle"] = h
        filelist.data["file_dict"] = file_dict
        filelist.data["progress"] = h.file_progress()

        def update_progress(h):
            filelist.data["progress"] = h.file_progress()
            for it in filelist.realized_items:
                it.fields_update("*", ELM_GENLIST_ITEM_FIELD_TEXT)
            return True

        ecore.Timer(5.0, update_progress, h)

        filelist.callback_expand_request_add(self.expand_request_cb)
        filelist.callback_contract_request_add(self.contract_request_cb)

        filelist.callback_expanded_add(self.item_expanded_cb)
        filelist.callback_contracted_add(self.item_contracted_cb)
        filelist.callback_activated_add(self.item_activated_cb)

        self.populate(filelist)

        filelist.show()

        sel_all = Button(self, text="Select all")
        sel_all.callback_clicked_add(self.select_all_cb, filelist, h, True)
        sel_all.show()

        sel_none = Button(self, text="Select none")
        sel_none.callback_clicked_add(self.select_all_cb, filelist, h, False)
        sel_none.show()

        # xbtn = Button(self)
        # xbtn.text = "Close"
        # xbtn.callback_clicked_add(lambda x: parent.item_pop())
        # xbtn.show()

        btn_box = Box(self, horizontal=True)
        btn_box.pack_end(sel_all)
        btn_box.pack_end(sel_none)
        #btn_box.pack_end(xbtn)
        btn_box.show()

        box.pack_end(filelist)
        box.pack_end(btn_box)
        box.show()
        self.show()

    def select_all_cb(self, btn, filelist, h, all_selected=True):
        priorities = h.file_priorities()
        for n, p in enumerate(priorities):
            priorities[n] = all_selected
        h.prioritize_files(priorities)
        filelist.realized_items_update()

    def populate(self, filelist):
        h = filelist.data["handle"]
        file_dict = filelist.data["file_dict"]
        filelist.clear()

        i = h.get_torrent_info()
        entries = i.files()

        for n, f in enumerate(entries):
            attach(f.path, f, n, file_dict)

        def construct(d, parent=None):
            """Construct a genlist tree."""
            for key in sorted(d.keys()):
                value = d[key]
                if key == FILE_MARKER:
                    if value:
                        for fn, fe, n in value:
                            filelist.item_append(
                                ITEM_CLASS_FILES,
                                (fn, fe, n),
                                parent,
                                ELM_GENLIST_ITEM_NONE)
                else:
                    it = filelist.item_append(
                        ITEM_CLASS_DIRS,
                        (key, value),
                        parent,
                        ELM_GENLIST_ITEM_TREE)
                    it.expanded = True

        construct(file_dict)

    def expand_request_cb(self, gl, item):
        item.expanded = True

    def contract_request_cb(self, gl, item):
        item.expanded = False

    def item_expanded_cb(self, gl, item):
        dir_node, d = item.data

        for key in sorted(d.keys()):
            value = d[key]
            if key == FILE_MARKER:
                if value:
                    for fn, fe, n in value:
                        gl.item_append(
                            ITEM_CLASS_FILES,
                            (fn, fe, n),
                            item,
                            ELM_GENLIST_ITEM_NONE)
            else:
                gl.item_append(
                    ITEM_CLASS_DIRS,
                    (key, value),
                    item,
                    ELM_GENLIST_ITEM_TREE)

    def item_contracted_cb(self, gl, item):
        item.subitems_clear()

    def item_activated_cb(self, gl, item):
        if item.type != ELM_GENLIST_ITEM_NONE:
            return
        fn, fe, n = item.data
        h = gl.data["handle"]
        progress = h.file_progress()[n]

        if progress == 0:
            log.error("Tried to open a file with size 0")
            return

        if progress < fe.size:
            log.warn("Opening an incomplete file")

        path = os.path.join(h.save_path().rstrip("\0"), fe.path)

        if sys.platform == 'linux2':
            ecore.Exe('xdg-open %s' % pipes.quote(path))
        else:
            os.startfile(path)


class TorrentInfo(Box):
    def __init__(self, parent, h, *args, **kwargs):
        Box.__init__(self, parent, *args, **kwargs)

        info = h.get_torrent_info()

        table = Table(self, homogeneous=True, size_hint_align=FILL_HORIZ)

        i = 0
        for func in info.comment, info.creation_date, info.creator:
            try:
                w = func()
            except Exception as e:
                log.debug(e)
            else:
                if w:
                    l = Label(table, size_hint_align=ALIGN_LEFT)
                    l.text = func.__name__.replace("_", " ").capitalize()
                    table.pack(l, 0, i, 1, 1)
                    l.show()
                    v = Label(table, ellipsis=True, size_hint_align=ALIGN_LEFT)
                    v.text = cgi.escape(str(w))
                    table.pack(v, 1, i, 1, 1)
                    v.show()
                    i += 1

        tpriv = Check(self)
        tpriv.size_hint_align = 0.0, 0.0
        tpriv.text = "Private"
        tpriv.tooltip_text_set(
            "Whether this torrent is private,<br>"
            "i.e. it should not be distributed on the<br>"
            "trackerless network (the kademlia DHT)."
            )
        tpriv.disabled = True
        tpriv.state = info.priv()

        fl_btn = Button(self, size_hint_align=ALIGN_LEFT)
        fl_btn.text = "Files ->"
        fl_btn.callback_clicked_add(lambda x: TorrentFiles(h))

        self.pack_end(table)
        self.pack_end(tpriv)
        self.pack_end(fl_btn)

        table.show()
        tpriv.show()
        fl_btn.show()


class TorrentSettings(Box):

    status_checks = {
        "super_seeding": "super_seeding",
        "sequential_download": "set_sequential_download",
        "upload_mode": "set_upload_mode",
        "share_mode": "set_share_mode",
        "ip_filter_applies": "apply_ip_filter"
        }

    handle_unit_spinners = {
        "upload_limit": "set_upload_limit",
        "download_limit": "set_download_limit"
    }

    handle_spinners = {
        "max_uploads": "set_max_uploads",
        "max_connections": "set_max_connections"
    }

    def __init__(self, parent, handle, *args, **kwargs):
        super(self.__class__, self).__init__(parent, *args, **kwargs)

        s = handle.status()

        t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)

        l = Label(t)
        l.size_hint_align = ALIGN_LEFT
        l.text = "Storage path"
        t.pack(l, 0, 0, 1, 1)
        l.show()

        fs = FsEntry(t)
        fs.size_hint_align = FILL_HORIZ
        fs.text = "Select"
        fs.folder_only = True
        fs.expandable = False
        fs.path = handle.save_path()

        def fs_cb(fs, path):
            if not path:
                return

            if os.path.isdir(path):
                handle.move_storage(path)

        fs.callback_file_chosen_add(fs_cb)
        t.pack(fs, 1, 0, 1, 1)
        fs.show()

        self.pack_end(t)
        t.show()

        t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)

        #Checks (bool)
        for i, (getter, setter) in enumerate(self.status_checks.items()):
            l = Label(t)
            l.size_hint_align = ALIGN_LEFT
            l.text = getter.replace("_", " ").capitalize()
            t.pack(l, 0, i, 1, 1)
            l.show()
            w = Check(t)
            w.size_hint_align = ALIGN_RIGHT
            w.state = getattr(s, getter)
            setter = getattr(handle, self.status_checks[getter])
            w.callback_changed_add(lambda x, z=setter: z(x.state))
            t.pack(w, 1, i, 1, 1)
            w.show()

        self.pack_end(t)
        t.show()

        t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)

        #Unit Spinners (int)
        for i, (getter, setter) in enumerate(
            self.handle_unit_spinners.items()
                ):
            l = Label(t)
            l.size_hint_align = ALIGN_LEFT
            l.text = getter.replace("_", " ").capitalize()
            t.pack(l, 0, i, 1, 1)
            l.show()
            w = UnitSpinner(self, "B/s", 1024, UnitSpinner.binary_prefixes)
            w.size_hint_align = FILL_HORIZ
            w.spinner.special_value_add(0, "disabled")
            w.set_value(getattr(handle, getter)())
            setter = getattr(handle, self.handle_unit_spinners[getter])
            w.callback_changed_add(lambda x, y, z=setter: z(y))
            t.pack(w, 1, i, 1, 1)
            w.show()

        self.pack_end(t)
        t.show()

        t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)

        #Spinners (int)
        for i, (getter, setter) in enumerate(self.handle_spinners.items()):
            l = Label(t)
            l.size_hint_align = ALIGN_LEFT
            l.text = getter.replace("_", " ").capitalize()
            t.pack(l, 0, i, 1, 1)
            l.show()
            w = Spinner(t)
            w.size_hint_align = FILL_HORIZ
            w.min_max = -1.0, 16777215.0
            w.special_value_add(-1.0, "disabled")
            w.value = getattr(handle, getter)()
            setter = getattr(handle, self.handle_spinners[getter])
            w.callback_delay_changed_add(lambda x, z=setter: z(int(x.value)))
            t.pack(w, 1, i, 1, 1)
            w.show()

        self.pack_end(t)
        t.show()


class TorrentStatus(Table):

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

    ignored_keys = (
        "progress_ppm", "distributed_copies", "states", "auto_managed"
        )

    byte_values = (
        "all_time_download", "all_time_upload", "block_size", "total_done",
        "total_wanted", "total_wanted_done", "total_failed_bytes",
        "total_payload_download", "total_payload_upload", "total_download",
        "total_upload", "total_redundant_bytes"
        )

    byte_transfer_values = (
        "download_rate", "download_payload_rate", "upload_rate",
        "upload_payload_rate"
        )

    timedelta_values = (
        "active_time", "seeding_time", "time_since_download",
        "time_since_upload", "finished_time"
        )

    datetime_values = (
        "added_time", "last_seen_complete", "completed_time"
        )

    def __init__(self, parent, h, *args, **kwargs):
        Table.__init__(self, parent, *args, **kwargs)

        s = h.status()

        self.widgets = []

        i = 0

        for k in dir(s):
            if k.startswith("__") or k in self.ignored_keys:
                continue
            try:
                v = getattr(s, k)
            except Exception as e:
                log.debug(e)
                continue

            v = self.convert_value(k, v)

            w = None

            if k == "state":
                pass
            elif isinstance(v, lt.torrent_status.states):
                continue
            elif isinstance(v, list):
                continue  # TODO
            elif k == "progress":
                w = Progressbar(self)
                w.size_hint_align = FILL_HORIZ
            elif isinstance(v, bool):
                w = Check(self)
                w.size_hint_align = ALIGN_RIGHT
                w.disabled = True
            if not w:
                try:
                    w = Label(self)
                    w.size_hint_align = ALIGN_RIGHT
                except Exception as e:
                    log.debug(e)
                    continue

            w.data["key"] = k
            self.populate(w, v)
            self.widgets.append(w)

            l = Label(self)
            l.size_hint_align = ALIGN_LEFT
            l.text = str(k).replace("_", " ").capitalize()
            self.pack(l, 0, i, 1, 1)
            l.show()

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

            i += 1

        def update():
            s = h.status()
            for w in self.widgets:
                key = w.data["key"]
                v = self.convert_value(key, getattr(s, key))
                self.populate(w, v)

            return True

        timer = ecore.Timer(5.0, update)
        self.on_del_add(lambda x: timer.delete())

    @staticmethod
    def populate(w, v):
        if isinstance(w, Label):
            w.text = str(v)
        elif isinstance(w, Check):
            w.state = v
        elif isinstance(w, Progressbar):
            w.value = v

    def convert_value(self, k, v):
        if k == "state":
            v = str(v).replace("_", " ").capitalize()
        elif k in self.datetime_values:
            v = datetime.fromtimestamp(v)
        elif k in self.timedelta_values:
            v = timedelta(seconds=v)
        elif isinstance(v, list):
            pass
        elif isinstance(v, lt.storage_mode_t):
            v = str(v).replace("_", " ").capitalize()
        elif isinstance(v, (int, long, basestring)):
            if k in self.byte_values:
                v = intrepr(v)
            if k in self.byte_transfer_values:
                v = intrepr(v) + "/s"

        return v


class FsEntry(Fileselector, FileselectorEntry):

    def __init__(self, parent, *args, **kwargs):
        FileselectorEntry.__init__(self, parent, *args, **kwargs)
