#
#  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 os
import cgi
import logging
log = logging.getLogger("epour.preferences")

import libtorrent as lt

from efl.elementary.box import Box
from efl.elementary.label import Label
from efl.elementary.button import Button
from efl.elementary.frame import Frame
from efl.elementary.entry import Entry
from efl.elementary.check import Check
from efl.elementary.spinner import Spinner
from efl.elementary.hoversel import Hoversel
from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_AUTO
from efl.elementary.separator import Separator
from efl.elementary.slider import Slider
from efl.elementary.table import Table
from efl.elementary.configuration import Configuration
from efl.elementary.window import StandardWindow
from efl.elementary.background import Background

from efl.evas import Rectangle, EVAS_HINT_EXPAND, EVAS_HINT_FILL

from Widgets import UnitSpinner, Error, Information, ActSWithLabel, FsButton, \
    RangeSpinners

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
SCROLL_BOTH = ELM_SCROLLER_POLICY_AUTO, ELM_SCROLLER_POLICY_AUTO


class PreferencesDialog(StandardWindow):

    """ Base class for all preferences dialogs """

    def __init__(self, title):

        elm_conf = Configuration()
        scale = elm_conf.scale

        StandardWindow.__init__(
            self, "epour", title, autodel=True)

        self.size = scale * 480, scale * 320

        bg = Background(self, size_hint_weight=EXPAND_BOTH)
        self.resize_object_add(bg)
        bg.show()

        self.scroller = Scroller(
            self, policy=SCROLL_BOTH,
            size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH
            )
        self.resize_object_add(self.scroller)
        self.scroller.show()

        self.box = Box(self)
        self.box.size_hint_weight = EXPAND_BOTH
        self.scroller.content = self.box


class PreferencesGeneral(PreferencesDialog):

    """ General preference dialog """

    def __init__(self, parent, session):
        self.session = session
        conf = session.conf
        PreferencesDialog.__init__(
            self, "Epour General Preferences")

        limits = Limits(self, session)
        ports = ListenPorts(self, session)
        pe = EncryptionSettings(self, session)
        save_path_sel = StorageSelector(
            self,
            "Storage path",
            lambda: conf.get("Settings", "storage_path"),
            lambda x: conf.set("Settings", "storage_path", x),
            size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ
            )

        completed_sel_enabled = Check(
            self, size_hint_align=ALIGN_LEFT, text="Move completed torrents",
            )
        completed_sel_enabled.state = conf.getboolean(
            "Settings", "move_completed_enabled"
            )

        completed_sel = StorageSelector(
            self,
            "Completed torrents",
            lambda: conf.get("Settings", "move_completed_path"),
            lambda x: conf.set("Settings", "move_completed_path", x),
            size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ
            )

        def completed_sel_enabled_cb(chk, box, completed_sel):
            enable = chk.state
            conf.set("Settings", "move_completed_enabled", str(enable))
            if enable:
                box.pack_after(completed_sel, chk)
                completed_sel.show()
            else:
                completed_sel.hide()
                box.unpack(completed_sel)

        completed_sel_enabled.callback_changed_add(
            completed_sel_enabled_cb, self.box, completed_sel
            )

        pad = Rectangle(self.evas)
        pad.color = 0, 0, 0, 0
        pad.size_hint_min = 0, 10

        sep1 = Separator(self)
        sep1.horizontal = True

        chk1 = Check(self)
        chk1.size_hint_align = ALIGN_LEFT
        chk1.text = "Delete original .torrent file when added"
        chk1.state = conf.getboolean("Settings", "delete_original")
        chk1.callback_changed_add(lambda x: conf.set(
            "Settings", "delete_original", str(bool(chk1.state))
        ))

        chk2 = Check(self)
        chk2.size_hint_align = ALIGN_LEFT
        chk2.text = "Ask for confirmation on exit"
        chk2.state = conf.getboolean("Settings", "confirm_exit")
        chk2.callback_changed_add(lambda x: conf.set(
            "Settings", "confirm_exit", str(bool(chk2.state))
        ))

        chk3 = Check(self)
        chk3.size_hint_align = ALIGN_LEFT
        chk3.text = (
            "Torrents to be added from dbus or command line open a dialog")
        chk3.state = conf.getboolean("Settings", "add_dialog_enabled")
        chk3.callback_changed_add(lambda x: conf.set(
            "Settings", "add_dialog_enabled", str(bool(chk3.state))
        ))

        sep2 = Separator(self)
        sep2.horizontal = True

        for w in (
            ports, limits, save_path_sel, completed_sel_enabled,
            pe, pad, sep1, chk1, chk2, chk3, sep2
                ):
            w.show()
            self.box.pack_end(w)

        if conf.getboolean("Settings", "move_completed_enabled"):
            self.box.pack_after(completed_sel, completed_sel_enabled)
            completed_sel.show()


class StorageSelector(Frame):

    def __init__(self, parent, title, read, write, *args, **kwargs):
        Frame.__init__(self, parent, *args, **kwargs)

        self.text = title

        b = Box(parent)

        lbl = Label(parent)
        lbl.text = read()

        dlsel = FsButton(
            self, size_hint_align=FILL_HORIZ, inwin_mode=False,
            text="Change path", folder_only=True, expandable=False
            )
        dlsel.path = read()

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

            if not os.path.exists(path):
                Error(
                    self,
                    "Invalid path",
                    "You have selected an invalid storage path."
                    )
                return

            lbl.text = path
            write(path)

        dlsel.callback_file_chosen_add(save_dlpath)

        for w in lbl, dlsel:
            w.show()
            b.pack_end(w)

        b.show()
        self.content = b


class ListenPorts(Frame):

    def __init__(self, parent, session):
        Frame.__init__(self, parent)

        self.session = session

        self.size_hint_align = FILL_HORIZ
        self.text = "Listen port (range)"

        #port = session.listen_port()

        b = Box(parent)
        b.size_hint_weight = EXPAND_HORIZ

        lp = self.lp = RangeSpinners(
            parent,
            low=session.conf.getint("Settings", "listen_low"),
            high=session.conf.getint("Settings", "listen_high"),
            minim=0, maxim=65535)
        lp.show()
        b.pack_end(lp)

        save = Button(parent)
        save.text = "Apply"
        save.callback_clicked_add(self.save_cb)
        save.show()
        b.pack_end(save)

        b.show()

        self.content = b

    def save_cb(self, btn):
        low = int(self.lp.listenlow.value)
        high = int(self.lp.listenhigh.value)
        self.session.listen_on(low, high)
        self.session.conf.set("Settings", "listen_low", str(low))
        self.session.conf.set("Settings", "listen_high", str(high))


class PreferencesProxy(PreferencesDialog):

    """ Proxy preference dialog """

    def __init__(self, parent, session):
        PreferencesDialog.__init__(
            self, "Epour Proxy Preferences")

        proxies = [
            ["Proxy for torrent peer connections",
                session.peer_proxy, session.set_peer_proxy],
            ["Proxy for torrent web seed connections",
                session.web_seed_proxy, session.set_web_seed_proxy],
            ["Proxy for tracker connections",
                session.tracker_proxy, session.set_tracker_proxy],
            ["Proxy for DHT connections",
                session.dht_proxy, session.set_dht_proxy],
        ]

        for title, rfunc, wfunc in proxies:
            pg = ProxyGroup(self, title, rfunc, wfunc)
            pg.show()
            self.box.pack_end(pg)


class ProxyGroup(Frame):

    proxy_types = {
        lt.proxy_type.none.name: lt.proxy_type.none,
        lt.proxy_type.socks4.name: lt.proxy_type.socks4,
        lt.proxy_type.socks5.name: lt.proxy_type.socks5,
        lt.proxy_type.socks5_pw.name: lt.proxy_type.socks5_pw,
        lt.proxy_type.http.name: lt.proxy_type.http,
        lt.proxy_type.http_pw.name: lt.proxy_type.http_pw,
    }

    def __init__(self, parent, title, rfunc, wfunc):
        Frame.__init__(
            self, parent, size_hint_weight=EXPAND_HORIZ,
            size_hint_align=FILL_HORIZ, text=title
            )

        t = Table(
            self, homogeneous=True, padding=(3, 3),
            size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ
            )
        t.show()

        ps = rfunc()

        l = Label(self, text="Proxy type", size_hint_align=ALIGN_LEFT)
        l.show()
        ptype = Hoversel(parent, size_hint_align=FILL_HORIZ)
        ptype.text = ps.type.name
        for n in lt.proxy_type.names.keys():
            ptype.item_add(n, callback=lambda x, y, z=n: ptype.text_set(z))
        ptype.show()
        t.pack(l, 0, 0, 1, 1)
        t.pack(ptype, 1, 0, 1, 1)

        l = Label(self, text="Hostname", size_hint_align=ALIGN_LEFT)
        l.show()
        phost = Entry(
            parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
            single_line=True, scrollable=True
            )
        phost.entry = ps.hostname
        phost.show()
        t.pack(l, 0, 1, 1, 1)
        t.pack(phost, 1, 1, 1, 1)

        l = Label(self, text="Port", size_hint_align=ALIGN_LEFT)
        l.show()
        pport = Spinner(parent, size_hint_align=FILL_HORIZ, min_max=(0, 65535))
        pport.value = ps.port
        pport.show()
        t.pack(l, 0, 2, 1, 1)
        t.pack(pport, 1, 2, 1, 1)

        l = Label(self, text="Username", size_hint_align=ALIGN_LEFT)
        l.show()
        puser = Entry(
            parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
            single_line=True, scrollable=True
            )
        puser.entry = ps.username
        puser.show()
        t.pack(l, 0, 3, 1, 1)
        t.pack(puser, 1, 3, 1, 1)

        l = Label(self, text="Password", size_hint_align=ALIGN_LEFT)
        l.show()
        ppass = Entry(
            parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
            single_line=True, scrollable=True, password=True
            )
        ppass.entry = ps.password
        ppass.show()
        t.pack(l, 0, 4, 1, 1)
        t.pack(ppass, 1, 4, 1, 1)

        l = Label(
            self, text="Proxy hostname lookups", size_hint_align=ALIGN_LEFT)
        l.show()
        phostlu = Check(parent, size_hint_align=ALIGN_RIGHT)
        phostlu.state = ps.proxy_hostnames
        phostlu.show()
        t.pack(l, 0, 5, 1, 1)
        t.pack(phostlu, 1, 5, 1, 1)

        l = Label(
            self, text="Proxy peer connections", size_hint_align=ALIGN_LEFT)
        l.show()
        ppeer = Check(parent, size_hint_align=ALIGN_RIGHT)
        ppeer.state = ps.proxy_peer_connections
        ppeer.show()
        t.pack(l, 0, 6, 1, 1)
        t.pack(ppeer, 1, 6, 1, 1)

        entries = [ptype, phost, pport, puser, ppass, phostlu, ppeer]

        save = Button(parent, text="Apply")
        save.callback_clicked_add(self.save_conf, wfunc, entries)
        save.show()
        t.pack(save, 0, 7, 2, 1)

        self.content = t

    def save_conf(self, btn, wfunc, entries):
        ptype, phost, pport, puser, ppass, phostlu, ppeer = entries
        p = lt.proxy_settings()

        p.hostname = phost.entry.encode("utf-8")
        p.username = puser.entry.encode("utf-8")
        p.password = ppass.entry.encode("utf-8")
        p.type = lt.proxy_type.names[ptype.text]
        p.port = int(pport.value)
        p.proxy_hostnames = phostlu.state
        p.proxy_peer_connections = ppeer.state

        Information(self.top_widget, "%s settings saved" % (ptype))

        wfunc(p)


class EncryptionSettings(Frame):

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

        Frame.__init__(self, parent)
        self.size_hint_align = FILL_HORIZ
        self.text = "Encryption settings"

        pes = self.pes = session.get_pe_settings()

        b = Box(parent)

        enc_values = lt.enc_policy.disabled, lt.enc_policy.enabled, \
            lt.enc_policy.forced
        enc_levels = lt.enc_level.plaintext, lt.enc_level.rc4, \
            lt.enc_level.both

        inc = self.inc = ActSWithLabel(
            parent, "Incoming encryption", enc_values, pes.in_enc_policy
            )
        b.pack_end(inc)
        inc.show()

        out = self.out = ActSWithLabel(
            parent, "Outgoing encryption", enc_values, pes.out_enc_policy
            )
        b.pack_end(out)
        out.show()

        lvl = self.lvl = ActSWithLabel(
            parent, "Allowed encryption level", enc_levels,
            pes.allowed_enc_level
            )
        b.pack_end(lvl)
        lvl.show()

        prf = self.prf = Check(
            parent, style="toggle", text="Prefer RC4 ecryption",
            state=pes.prefer_rc4
            )
        b.pack_end(prf)
        prf.show()

        a_btn = Button(parent, text="Apply")
        a_btn.callback_clicked_add(self.apply)
        b.pack_end(a_btn)
        a_btn.show()

        b.show()
        self.content = b

    def apply(self, btn):
        self.pes.in_enc_policy = self.inc.get_value()
        self.pes.out_enc_policy = self.out.get_value()

        self.pes.allowed_enc_level = self.lvl.get_value()
        self.pes.prefer_rc4 = self.prf.state

        self.session.set_pe_settings(self.pes)


class PreferencesSession(PreferencesDialog):

    """ Session preference dialog """

    def __init__(self, parent, session):
        PreferencesDialog.__init__(
            self, "Epour Session Preferences")

        self.session = session

        widgets = {}

        #elm_conf = Configuration()
        #scale = elm_conf.scale

        s = session.settings()

        t = Table(
            self, padding=(5, 5), homogeneous=True, size_hint_align=FILL_BOTH
            )
        self.box.pack_end(t)
        t.show()

        i = 0

        INT_MIN = -2147483648
        INT_MAX = 2147483647

        import time
        t1 = time.time()

        for k in dir(s):
            if k.startswith("__"):
                continue
            try:
                if k == "peer_tos" or k == "outgoing_ports":
                    # XXX: these don't have a C++ -> Python equivalent.
                    continue
                a = getattr(s, k)
                if isinstance(a, lt.disk_cache_algo_t):
                    w = Spinner(t)
                    w.size_hint_align = FILL_HORIZ
                    w.min_max = 0, len(lt.disk_cache_algo_t.values) - 1
                    for name, val in lt.disk_cache_algo_t.names.items():
                        w.special_value_add(val, name)
                    w.value = a
                elif k == "suggest_mode":
                    w = Spinner(t)
                    w.size_hint_align = FILL_HORIZ
                    w.min_max = 0, len(lt.suggest_mode_t.values) - 1
                    for name, val in lt.suggest_mode_t.names.items():
                        w.special_value_add(val, name)
                    w.value = a
                elif k == "choking_algorithm":
                    w = Spinner(t)
                    w.size_hint_align = FILL_HORIZ
                    w.min_max = 0, len(lt.choking_algorithm_t.values) - 1
                    for name, val in lt.choking_algorithm_t.names.items():
                        w.special_value_add(val, name)
                    w.value = a
                elif k == "seed_choking_algorithm":
                    w = Spinner(t)
                    w.size_hint_align = FILL_HORIZ
                    w.min_max = 0, len(lt.seed_choking_algorithm_t.values) - 1
                    for name, val in lt.seed_choking_algorithm_t.names.items():
                        w.special_value_add(val, name)
                    w.value = a
                elif k == "mixed_mode_algorithm":
                    w = Spinner(t)
                    w.size_hint_align = FILL_HORIZ
                    w.min_max = 0, len(lt.bandwidth_mixed_algo_t.values) - 1
                    for name, val in lt.bandwidth_mixed_algo_t.names.items():
                        w.special_value_add(val, name)
                    w.value = a
                elif k == "disk_io_write_mode" or k == "disk_io_read_mode":
                    w = Spinner(t)
                    w.size_hint_align = FILL_HORIZ
                    w.min_max = 0, len(lt.io_buffer_mode_t.values) - 1
                    for name, val in lt.io_buffer_mode_t.names.items():
                        w.special_value_add(val, name)
                    w.value = a
                elif isinstance(a, bool):
                    w = Check(t)
                    #w.style = "toggle"
                    w.size_hint_align = FILL_HORIZ
                    w.state = a
                elif isinstance(a, int):
                    w = Spinner(t)
                    w.size_hint_align = FILL_HORIZ
                    w.min_max = INT_MIN, INT_MAX
                    w.value = a
                elif isinstance(a, float):
                    w = Slider(t)
                    w.size_hint_align = FILL_HORIZ
                    w.size_hint_weight = EXPAND_HORIZ
                    w.unit_format = "%1.2f"
                    if k.startswith("peer_turnover"):
                        w.min_max = 0.0, 1.0
                    else:
                        w.min_max = 0.0, 20.0
                    w.value = a
                elif k == "user_agent":
                    w = Entry(t)
                    w.size_hint_align = 1.0, 0.0
                    w.size_hint_weight = EXPAND_HORIZ
                    w.single_line = True
                    w.editable = False
                    w.entry = cgi.escape(a)
                else:
                    log.debug("Using string for %s type %s", k, type(a))
                    w = Entry(t)
                    w.size_hint_align = FILL_HORIZ
                    w.size_hint_weight = EXPAND_HORIZ
                    w.single_line = True
                    w.entry = cgi.escape(a)
                    w.part_text_set("guide", "Enter here")
                l = Label(t)
                l.text = k.replace("_", " ").capitalize()
                l.size_hint_align = ALIGN_LEFT
                l.size_hint_weight = EXPAND_HORIZ
                l.show()
                t.pack(l, 0, i, 1, 1)
                #w.size_hint_min = scale * 150, scale * 25
                t.pack(w, 1, i, 1, 1)
                w.show()
                widgets[k] = w
                i += 1
            except TypeError as e:
                log.debug("Error in {}: {}".format(k, e))

        t2 = time.time()
        log.debug("Session settings time: %f", t2-t1)

        save_btn = Button(self)
        save_btn.text = "Apply session settings"
        save_btn.callback_clicked_add(self.apply_settings, widgets, session)
        save_btn.show()
        self.box.pack_end(save_btn)

    def apply_settings(self, btn, widgets, session):
        s = lt.session_settings()

        for k, w in widgets.iteritems():

            if k == "disk_cache_algorithm":
                v = lt.disk_cache_algo_t(w.value)
            elif isinstance(w, Spinner):
                v = int(w.value)
            elif isinstance(w, Slider):
                v = w.value
            elif isinstance(w, Entry):
                v = w.entry.encode("utf-8")
            elif isinstance(w, Check):
                v = bool(w.state)
            else:
                v = None

            setattr(s, k, v)

        session.set_settings(s)
        Information(self, "Session settings saved.")


class Limits(Frame):

    def __init__(self, parent, session):
        Frame.__init__(self, parent)

        self.text = "Limits"
        self.size_hint_align = FILL_HORIZ

        t = Table(parent)
        for r, values in enumerate((
            ("Upload limit",
                session.upload_rate_limit,
                session.set_upload_rate_limit),
            ("Download limit",
                session.download_rate_limit,
                session.set_download_rate_limit),
            ("Upload limit for local connections",
                session.local_upload_rate_limit,
                session.set_local_upload_rate_limit),
            ("Download limit for local connections",
                session.local_download_rate_limit,
                session.set_local_download_rate_limit),
        )):
            title, rfunc, wfunc = values

            l = Label(parent)
            l.text = title
            l.size_hint_align = FILL_HORIZ
            t.pack(l, 0, r, 1, 1)
            l.show()

            usw = UnitSpinner(parent, "B/s", 1024, UnitSpinner.binary_prefixes)
            usw.size_hint_weight = EXPAND_HORIZ
            usw.size_hint_align = FILL_HORIZ
            usw.set_value(rfunc())
            usw.callback_changed_add(lambda x, y, z=wfunc: z(y), delay=2.0)
            t.pack(usw, 1, r, 1, 1)
            usw.show()

        self.content = t
