#
#  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 mimetypes
import urlparse
import urllib
import logging
import shutil
import cPickle
from collections import OrderedDict

import libtorrent as lt

from efl.ecore import Timer

from xdg.BaseDirectory import save_data_path, load_data_paths


def torrent_path_get(ihash):
    for p in load_data_paths("epour"):
        path = os.path.join(p, "{0}.torrent".format(ihash))
        if os.path.isfile(path):
            return path


class Session(lt.session):
    def __init__(self, conf):
        self.conf = conf
        self.log = logging.getLogger("epour.session")

        from epour import __version__ as version
        ver_ints = []
        for s in version.split("."):
            ver_ints.append(int(s))
        ver_ints.append(0)

        fp = lt.fingerprint("EP", *ver_ints)
        self.log.debug("peer-id: {}".format(fp))

        lt.session.__init__(
            self,
            fingerprint=fp,
            # flags=
                #lt.session_flags_t.add_default_plugins|
                #lt.session_flags_t.start_default_features
        )

        self.log.info("Session started")

        self.torrents = OrderedDict()

                #rsdpipsdtsppe
                #stheprtertoer
                #satrboabaorer
                #|t|flgtucrtro
                #|s|oorugkam|r
                #|||rces|ega||
                #|||mks||rep||
        mask = 0b0000001000001
        self.set_alert_mask(mask)

        self.listen_on(
            conf.getint("Settings", "listen_low"),
            conf.getint("Settings", "listen_high")
        )

        self.alert_manager = AlertManager(self)
        self.alert_manager.callback_add(
            "add_torrent_alert", self._add_torrent_cb)
        self.alert_manager.callback_add(
            "metadata_received_alert", self._metadata_received_cb)

        def torrent_finished_move_cb(a):
            h = a.handle
            if conf.getboolean("Settings", "move_completed_enabled"):
                path = conf.get("Settings", "move_completed_path")
                if h.save_path() == path:
                    return
                h.move_storage(path)

        self.alert_manager.callback_add(
            "torrent_finished_alert", torrent_finished_move_cb)

    def _add_torrent_cb(self, a):
        e = a.error
        if e.value() > 0:
            self.log.error("Adding torrent failed: %r" % (e.message()))
            return
        h = a.handle
        ihash = str(h.info_hash())
        self.torrents[ihash] = a.params
        self.log.debug("Torrent added.")

    def _metadata_received_cb(self, a):
        h = a.handle
        ihash = str(h.info_hash())
        self.log.debug("Metadata received.")
        #self.write_torrent(h)
        t_info = h.get_torrent_info()
        t = {}
        t["info"] = lt.bdecode(t_info.metadata())
        self.torrents[ihash]["ti"] = lt.bencode(t)

    def load_state(self):
        for p in load_data_paths("epour"):
            path = os.path.join(p, "session")
            if os.path.isfile(path):
                try:
                    with open(path, 'rb') as f:
                        state = lt.bdecode(f.read())
                    lt.session.load_state(self, state)
                except Exception as e:
                    self.log.debug("Could not load previous session state.")
                    self.log.debug(e)
                else:
                    self.log.info("Session restored from disk.")
                    break

        settings = self.settings()
        from epour import __version__ as version
        version += ".0"
        ver_s = "Epour/{} libtorrent/{}".format(version, lt.version)
        settings.user_agent = ver_s
        self.log.debug("User agent: {}".format(ver_s))
        self.set_settings(settings)

    def save_state(self):
        state = lt.session.save_state(self)

        path = os.path.join(save_data_path("epour"), "session")
        with open(path, 'wb') as f:
            f.write(lt.bencode(state))

        self.log.debug("Session state saved.")

    def load_torrents(self):
        for p in load_data_paths("epour"):
            torrents_path = os.path.join(p, "torrents")
            if os.path.isfile(torrents_path):
                break

        if not torrents_path:
            self.info.debug("No previous list of torrents found.")
            return

        try:
            pkl_file = open(torrents_path, 'rb')
        except IOError:
            self.log.warning("Could not open the list of torrents.")
        else:
            try:
                torrents = cPickle.load(pkl_file)
            except EOFError:
                self.log.exception("Opening the list of torrents failed.")
            else:
                self.log.debug(
                    "List of torrents opened, "
                    "restoring {} torrents.".format(len(torrents))
                )
                for i, t in torrents.items():
                    try:
                        if isinstance(t, dict):
                            for k, v in t.items():
                                if k == "ti":
                                    t[k] = lt.torrent_info(lt.bdecode(v))
                                    # with open(v, "rb") as f:
                                    #     torrents[i][k] = \
                                    #         lt.torrent_info(lt.bdecode(f.read()))
                                if k == "info_hash":
                                    torrents[i][k] = lt.big_number(v)
                        else:
                            # Upgrade from older versions where t is file path
                            p = t
                            t = {}
                            with open(p, "rb") as f:
                                t["ti"] = \
                                    lt.torrent_info(lt.bdecode(f.read()))
                        # rp = os.path.join(data_dir, i + ".fastresume")
                        # if os.path.exists(rp):
                        #     with open(rp, "rb") as f:
                        #         t["resume_data"] = f.read()
                    except Exception:
                        self.log.exception("Opening torrent file %s failed", v)
                        continue

                    self.async_add_torrent(t)
            finally:
                pkl_file.close()

    def save_torrents(self):
        self.log.debug("Saving {} torrents.".format(len(self.torrents)))

        for i, t in self.torrents.items():
            for k, v in t.items():
                # if k == "ti":
                #     self.torrents[i][k] = torrent_path_get(i)
                if k == "info_hash":
                    if v.is_all_zeros():
                        del self.torrents[i][k]
                    else:
                        self.torrents[i][k] = v.to_string()

        for h in self.get_torrents():
            if h.is_valid() and h.has_metadata():
                i = str(h.info_hash())
                t_dict = self.torrents[i]
                t_info = h.get_torrent_info()
                t = {}
                t["info"] = lt.bdecode(t_info.metadata())
                t_dict["ti"] = lt.bencode(t)
                t_dict["save_path"] = h.save_path()
                resume_data = lt.bencode(h.write_resume_data())
                t_dict["resume_data"] = resume_data

        path = os.path.join(save_data_path("epour"), "torrents")
        with open(path, 'wb') as f:
            cPickle.dump(self.torrents, f, protocol=2)

        self.log.debug("List of torrents saved.")

        # Save fast resume data
        # for h in self.get_torrents():
        #     if not h.is_valid() or not h.has_metadata():
        #         continue
        #     data = lt.bencode(h.write_resume_data())
        #     with open(os.path.join(
        #         data_dir, str(h.info_hash()) + ".fastresume"
        #             ), 'wb') as f:
        #         f.write(data)

        # self.log.debug("Fast resume data saved.")

    def write_torrent(self, h):
        if h is None:
            self.log.debug("Tried to write torrent while handle was empty.")
            return

        t_info = h.get_torrent_info()
        ihash = str(h.info_hash())

        self.log.debug("Writing torrent file {}".format(ihash))

        md = lt.bdecode(t_info.metadata())
        t = {}
        t["info"] = md
        t_path = torrent_path_get(ihash)
        with open(t_path, "wb") as f:
            f.write(lt.bencode(t))

        return t_path

    def remove_torrent(self, h, with_data=False):
        ihash = str(h.info_hash())

        #t_dict = self.torrents[ihash]

        del self.torrents[ihash]
        lt.session.remove_torrent(self, h, option=with_data)

        for p in load_data_paths("epour"):
            fr_path = os.path.join(
                p, "{0}.fastresume".format(ihash)
            )

            try:
                with open(fr_path):
                    pass
            except IOError:
                self.log.debug("Could not remove %s", fr_path)
            else:
                os.remove(fr_path)

        try:
            with open(torrent_path_get(ihash)):
                pass
        except IOError:
            self.log.debug("Could not remove torrent file.")
        else:
            os.remove(torrent_path_get(ihash))

        if not hasattr(lt, "torrent_removed_alert"):
            class torrent_removed_alert(object):
                def __init__(self, h, info_hash):
                    self.handle = h
                    self.info_hash = info_hash

            a = torrent_removed_alert(h, ihash)

            self.alert_manager.signal(a)

        return ihash

    def add_torrent_from_file(self, add_dict, t_uri):
        mimetype = mimetypes.guess_type(t_uri)[0]
        if not mimetype == "application/x-bittorrent":
            self.log.error("Invalid file")
            return

        if t_uri.startswith("file://"):
            t_uri = urllib.unquote(urlparse.urlsplit(t_uri).path)

        with open(t_uri, 'rb') as t:
            t_raw = lt.bdecode(t.read())

        info = lt.torrent_info(t_raw)
        add_dict["ti"] = info

        rd = None
        fr_file_name = "{}.fastresume".format(info.info_hash())
        for p in load_data_paths("epour"):
            path = os.path.join(p, fr_file_name)
            if os.path.isfile(path):
                try:
                    with open(path, "rb") as f:
                        rd = f.read()
                except Exception:
                    self.log.debug("Invalid resume data")
                else:
                    add_dict["resume_data"] = rd
                    break

        ihash = str(info.info_hash())

        path = save_data_path("epour")
        new_uri = os.path.join(path, "{0}.torrent".format(ihash))

        if t_uri == new_uri:
            pass
        else:
            shutil.copy(t_uri, new_uri)

            if self.conf.getboolean("Settings", "delete_original"):
                self.log.debug(
                    "Deleting original torrent file {}".format(t_uri))
                os.remove(t_uri)

            t_uri = new_uri

        self.async_add_torrent(add_dict)

    def add_torrent_from_magnet(self, add_dict, t_uri):
        self.log.debug("Adding %r", t_uri)
        tmp_dict = lt.parse_magnet_uri(bytes(t_uri))
        tmp_dict.update(add_dict)
        self.async_add_torrent(tmp_dict)

    def add_torrent_from_hash(self, add_dict, t_uri):
        add_dict["info_hash"] = lt.info_hash(bytes(t_uri))
        self.log.debug("Adding %s", t_uri)
        self.async_add_torrent(add_dict)


class AlertManager(object):

    log = logging.getLogger("epour.alert")
    update_interval = 0.2
    alerts = {}

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

        self.timer = Timer(self.update_interval, self.update)

    def callback_add(self, alert_type, cb, *args, **kwargs):
        if not alert_type in self.alerts:
            self.alerts[alert_type] = []
        self.alerts[alert_type].append((cb, args, kwargs))

    def callback_del(self, alert_type, cb, *args, **kwargs):
        for i, a in enumerate(self.alerts):
            if a == (cb, args, kwargs):
                del(self.alerts[alert_type][i])

    def signal(self, a):
        a_name = type(a).__name__

        if not a_name in self.alerts:
            self.log.debug("No handler: {} | {}".format(a_name, a))
            return

        for cb, args, kwargs in self.alerts[a_name]:
            try:
                cb(a, *args, **kwargs)
            except:
                self.log.exception("Exception while handling alerts")

    def update(self):
        while 1:
            a = self.session.pop_alert()
            if not a:
                break

            self.signal(a)

        return True
