#!/usr/bin/env python

# The contents of this file are subject to the BitTorrent Open Source License
# Version 1.0 (the License).  You may not copy or use this file, in either
# source code or executable form, except in compliance with the License.  You
# may obtain a copy of the License at http://www.bittorrent.com/license/.
#
# Software distributed under the License is distributed on an AS IS basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
# for the specific language governing rights and limitations under the
# License.

# Original version written by Henry 'Pi' James, modified by (at least)
# John Hoffman and Uoti Urpala

from __future__ import division

import gettext
gettext.install('bittorrent', 'locale')

SPEW_SCROLL_RATE = 1

import sys
import os
import threading
from time import time, strftime

from BitTorrent.download import Feedback, Multitorrent
from BitTorrent.defaultargs import get_defaults
from BitTorrent.parseargs import printHelp, open_arg
from BitTorrent.zurllib import urlopen
from BitTorrent.bencode import bdecode
from BitTorrent.ConvertedMetainfo import ConvertedMetainfo
from BitTorrent import configfile
from BitTorrent import BTFailure
from BitTorrent import version


try:
    import curses
    import curses.panel
    from curses.wrapper import wrapper as curses_wrapper
    from signal import signal, SIGWINCH
except:
    print _("Textmode GUI initialization failed, cannot proceed.")
    print
    print _("This download interface requires the standard Python module "
            "\"curses\", which is unfortunately not available for the native "
            "Windows port of Python. It is however available for the Cygwin "
            "port of Python, running on all Win32 systems (www.cygwin.com).")
    print
    print _('You may still use "btdownloadheadless.py" to download.')
    sys.exit(1)

def fmttime(n):
    if n == 0:
        return _("download complete!")
    try:
        n = int(n)
        assert n >= 0 and n < 5184000  # 60 days
    except:
        return _("<unknown>")
    m, s = divmod(n, 60)
    h, m = divmod(m, 60)
    return _("finishing in %d:%02d:%02d") % (h, m, s)

def fmtsize(n):
    s = str(n)
    size = s[-3:]
    while len(s) > 3:
        s = s[:-3]
        size = '%s,%s' % (s[-3:], size)
    if n > 999:
        unit = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
        i = 1
        while i + 1 < len(unit) and (n >> 10) >= 999:
            i += 1
            n >>= 10
        n /= (1 << 10)
        size = '%s (%.0f %s)' % (size, n, unit[i])
    return size


class CursesDisplayer(object):

    def __init__(self, scrwin, errlist, doneflag, reread_config, ulrate):
        self.scrwin = scrwin
        self.errlist = errlist
        self.doneflag = doneflag

        signal(SIGWINCH, self.winch_handler)
        self.changeflag = threading.Event()

        self.done = False
        self.reread_config = reread_config
        self.ulrate = ulrate
        self.activity = ''
        self.status = ''
        self.progress = ''
        self.downRate = '---'
        self.upRate = '---'
        self.shareRating = ''
        self.seedStatus = ''
        self.peerStatus = ''
        self.errors = []
        self.file = ''
        self.downloadTo = ''
        self.fileSize = ''
        self.numpieces = 0
        self.spew_scroll_time = 0
        self.spew_scroll_pos = 0

        self._remake_window()

    def set_torrent_values(self, name, path, size, numpieces):
        self.file = name
        self.downloadTo = path
        self.fileSize = fmtsize(size)
        self.numpieces = numpieces
        self._remake_window()

    def winch_handler(self, signum, stackframe):
        self.changeflag.set()
        curses.endwin()
        self.scrwin.refresh()
        self.scrwin = curses.newwin(0, 0, 0, 0)
        self._remake_window()

    def _remake_window(self):
        self.scrh, self.scrw = self.scrwin.getmaxyx()
        self.scrpan = curses.panel.new_panel(self.scrwin)
        self.labelh, self.labelw, self.labely, self.labelx = 11, 9, 1, 2
        self.labelwin = curses.newwin(self.labelh, self.labelw,
                                      self.labely, self.labelx)
        self.labelpan = curses.panel.new_panel(self.labelwin)
        self.fieldh, self.fieldw, self.fieldy, self.fieldx = (
                            self.labelh, self.scrw-2 - self.labelw-3,
                            1, self.labelw+3)
        self.fieldwin = curses.newwin(self.fieldh, self.fieldw,
                                      self.fieldy, self.fieldx)
        self.fieldwin.nodelay(1)
        self.fieldpan = curses.panel.new_panel(self.fieldwin)
        self.spewh, self.speww, self.spewy, self.spewx = (
            self.scrh - self.labelh - 2, self.scrw - 3, 1 + self.labelh, 2)
        self.spewwin = curses.newwin(self.spewh, self.speww,
                                     self.spewy, self.spewx)
        self.spewpan = curses.panel.new_panel(self.spewwin)
        try:
            self.scrwin.border(ord('|'),ord('|'),ord('-'),ord('-'),ord(' '),ord(' '),ord(' '),ord(' '))
        except:
            pass
        self.labelwin.addstr(0, 0, _("file:"))
        self.labelwin.addstr(1, 0, _("size:"))
        self.labelwin.addstr(2, 0, _("dest:"))
        self.labelwin.addstr(3, 0, _("progress:"))
        self.labelwin.addstr(4, 0, _("status:"))
        self.labelwin.addstr(5, 0, _("dl speed:"))
        self.labelwin.addstr(6, 0, _("ul speed:"))
        self.labelwin.addstr(7, 0, _("sharing:"))
        self.labelwin.addstr(8, 0, _("seeds:"))
        self.labelwin.addstr(9, 0, _("peers:"))
        curses.panel.update_panels()
        curses.doupdate()
        self.changeflag.clear()


    def finished(self):
        self.done = True
        self.downRate = '---'
        self.display({'activity':_("download succeeded"), 'fractionDone':1})

    def error(self, errormsg):
        newerrmsg = strftime('[%H:%M:%S] ') + errormsg
        self.errors.append(newerrmsg.split('\n')[0])
        self.errlist.append(newerrmsg)
        self.display({})

    def display(self, statistics):
        fractionDone = statistics.get('fractionDone')
        activity = statistics.get('activity')
        timeEst = statistics.get('timeEst')
        downRate = statistics.get('downRate')
        upRate = statistics.get('upRate')
        spew = statistics.get('spew')

        inchar = self.fieldwin.getch()
        if inchar == 12: # ^L
            self._remake_window()
        elif inchar in (ord('q'),ord('Q')):
            self.doneflag.set()
        elif inchar in (ord('r'),ord('R')):
            self.reread_config()
        elif inchar in (ord('u'),ord('U')):
            curses.echo()
            self.fieldwin.nodelay(0)
            s = self.fieldwin.getstr(6,10)
            curses.noecho()
            self.fieldwin.nodelay(1)
            r = None
            try:
                r = int(s)
            except ValueError:
                pass
            if r is not None:
                self.ulrate(r)

        if timeEst is not None:
            self.activity = fmttime(timeEst)
        elif activity is not None:
            self.activity = activity
        if self.changeflag.isSet():
            return

        if fractionDone is not None:
            blocknum = int(self.fieldw * fractionDone)
            self.progress = blocknum * '#' + (self.fieldw - blocknum) * '_'
            self.status = '%s (%.1f%%)' % (self.activity, fractionDone * 100)

        if downRate is not None:
            self.downRate = '%.1f KB/s' % (downRate / (1 << 10))
        if upRate is not None:
            self.upRate = '%.1f KB/s' % (upRate / (1 << 10))
        downTotal = statistics.get('downTotal')
        if downTotal is not None:
            upTotal = statistics['upTotal']
            if downTotal <= upTotal / 100:
                self.shareRating = _("oo  (%.1f MB up / %.1f MB down)") % (
                    upTotal / (1<<20), downTotal / (1<<20))
            else:
                self.shareRating = _("%.3f  (%.1f MB up / %.1f MB down)") % (
                   upTotal / downTotal, upTotal / (1<<20), downTotal / (1<<20))
            numCopies = statistics['numCopies']
            nextCopies = ', '.join(["%d:%.1f%%" % (a,int(b*1000)/10) for a,b in
                    zip(xrange(numCopies+1, 1000), statistics['numCopyList'])])
            if not self.done:
                self.seedStatus = _("%d seen now, plus %d distributed copies"
                                    "(%s)") % (statistics['numSeeds' ],
                                               statistics['numCopies'],
                                               nextCopies)
            else:
                self.seedStatus = _("%d distributed copies (next: %s)") % (
                    statistics['numCopies'], nextCopies)
            self.peerStatus = _("%d seen now") % statistics['numPeers']

        self.fieldwin.erase()
        self.fieldwin.addnstr(0, 0, self.file, self.fieldw, curses.A_BOLD)
        self.fieldwin.addnstr(1, 0, self.fileSize, self.fieldw)
        self.fieldwin.addnstr(2, 0, self.downloadTo, self.fieldw)
        if self.progress:
            self.fieldwin.addnstr(3, 0, self.progress, self.fieldw, curses.A_BOLD)
        self.fieldwin.addnstr(4, 0, self.status, self.fieldw)
        self.fieldwin.addnstr(5, 0, self.downRate, self.fieldw)
        self.fieldwin.addnstr(6, 0, self.upRate, self.fieldw)
        self.fieldwin.addnstr(7, 0, self.shareRating, self.fieldw)
        self.fieldwin.addnstr(8, 0, self.seedStatus, self.fieldw)
        self.fieldwin.addnstr(9, 0, self.peerStatus, self.fieldw)

        self.spewwin.erase()

        if not spew:
            errsize = self.spewh
            if self.errors:
                self.spewwin.addnstr(0, 0, _("error(s):"), self.speww, curses.A_BOLD)
                errsize = len(self.errors)
                displaysize = min(errsize, self.spewh)
                displaytop = errsize - displaysize
                for i in range(displaysize):
                    self.spewwin.addnstr(i, self.labelw, self.errors[displaytop + i],
                                 self.speww-self.labelw-1, curses.A_BOLD)
        else:
            if self.errors:
                self.spewwin.addnstr(0, 0, _("error:"), self.speww, curses.A_BOLD)
                self.spewwin.addnstr(0, self.labelw, self.errors[-1],
                                 self.speww-self.labelw-1, curses.A_BOLD)
            self.spewwin.addnstr(2, 0, _("  #     IP                 Upload           Download     Completed  Speed"), self.speww, curses.A_BOLD)


            if self.spew_scroll_time + SPEW_SCROLL_RATE < time():
                self.spew_scroll_time = time()
                if len(spew) > self.spewh-5 or self.spew_scroll_pos > 0:
                    self.spew_scroll_pos += 1
            if self.spew_scroll_pos > len(spew):
                self.spew_scroll_pos = 0

            for i in range(len(spew)):
                spew[i]['lineno'] = i+1
            spew.append({'lineno': None})
            spew = spew[self.spew_scroll_pos:] + spew[:self.spew_scroll_pos]

            for i in range(min(self.spewh - 5, len(spew))):
                if not spew[i]['lineno']:
                    continue
                self.spewwin.addnstr(i+3, 0, '%3d' % spew[i]['lineno'], 3)
                self.spewwin.addnstr(i+3, 4, spew[i]['ip'], 15)
                ul = spew[i]['upload']
                if ul[1] > 100:
                    self.spewwin.addnstr(i+3, 20, '%6.0f KB/s' % (
                        ul[1] / 1000), 11)
                self.spewwin.addnstr(i+3, 32, '-----', 5)
                if ul[2]:
                    self.spewwin.addnstr(i+3, 33, 'I', 1)
                if ul[3]:
                    self.spewwin.addnstr(i+3, 35, 'C', 1)
                dl = spew[i]['download']
                if dl[1] > 100:
                    self.spewwin.addnstr(i+3, 38, '%6.0f KB/s' % (
                        dl[1] / 1000), 11)
                self.spewwin.addnstr(i+3, 50, '-------', 7)
                if dl[2]:
                    self.spewwin.addnstr(i+3, 51, 'I', 1)
                if dl[3]:
                    self.spewwin.addnstr(i+3, 53, 'C', 1)
                if dl[4]:
                    self.spewwin.addnstr(i+3, 55, 'S', 1)
                self.spewwin.addnstr(i+3, 58, '%5.1f%%' % (int(spew[i]['completed']*1000)/10), 6)
                if spew[i]['speed'] is not None:
                    self.spewwin.addnstr(i+3, 64, '%5.0f KB/s' % (spew[i]['speed']/1000), 10)

            self.spewwin.addnstr(self.spewh-1, 0,
                    _("downloading %d pieces, have %d fragments, "
                      "%d of %d pieces completed") %
                    (statistics['storage_active'], statistics['storage_dirty'],
                     statistics['storage_numcomplete'], self.numpieces),
                    self.speww-1)

        curses.panel.update_panels()
        curses.doupdate()


class DL(Feedback):

    def __init__(self, metainfo, config, errlist):
        self.doneflag = threading.Event()
        self.metainfo = metainfo
        self.config = config
        self.errlist = errlist

    def run(self, scrwin):
        def reread():
            self.multitorrent.rawserver.external_add_task(self.reread_config,0)
        def ulrate(value):
            self.multitorrent.set_option('max_upload_rate', value)
            self.torrent.set_option('max_upload_rate', value)

        self.d = CursesDisplayer(scrwin, self.errlist, self.doneflag, reread, ulrate)
        try:
            self.multitorrent = Multitorrent(self.config, self.doneflag,
                                             self.global_error)
            # raises BTFailure if bad
            metainfo = ConvertedMetainfo(bdecode(self.metainfo))
            torrent_name = metainfo.name_fs
            if config['save_as']:
                if config['save_in']:
                    raise BTFailure(_("You cannot specify both --save_as and "
                                      "--save_in"))
                saveas = config['save_as']
            elif config['save_in']:
                saveas = os.path.join(config['save_in'], torrent_name)
            else:
                saveas = torrent_name

            self.d.set_torrent_values(metainfo.name, os.path.abspath(saveas),
                                metainfo.total_bytes, len(metainfo.hashes))
            self.torrent = self.multitorrent.start_torrent(metainfo,
                                self.config, self, saveas)
        except BTFailure, e:
            errlist.append(str(e))
            return
        self.get_status()
        self.multitorrent.rawserver.install_sigint_handler()
        self.multitorrent.rawserver.listen_forever()
        self.d.display({'activity':_("shutting down"), 'fractionDone':0})
        self.torrent.shutdown()

    def reread_config(self):
        try:
            newvalues = configfile.get_config(self.config, 'btdownloadcurses')
        except Exception, e:
            self.d.error(_("Error reading config: ") + str(e))
            return
        self.config.update(newvalues)
        # The set_option call can potentially trigger something that kills
        # the torrent (when writing this the only possibility is a change in
        # max_files_open causing an IOError while closing files), and so
        # the self.failed() callback can run during this loop.
        for option, value in newvalues.iteritems():
            self.multitorrent.set_option(option, value)
        for option, value in newvalues.iteritems():
            self.torrent.set_option(option, value)

    def get_status(self):
        self.multitorrent.rawserver.add_task(self.get_status,
                                             self.config['display_interval'])
        status = self.torrent.get_status(self.config['spew'])
        self.d.display(status)

    def global_error(self, level, text):
        self.d.error(text)

    def error(self, torrent, level, text):
        self.d.error(text)

    def failed(self, torrent, is_external):
        self.doneflag.set()

    def finished(self, torrent):
        self.d.finished()


if __name__ == '__main__':
    uiname = 'btdownloadcurses'
    defaults = get_defaults(uiname)

    metainfo = None
    if len(sys.argv) <= 1:
        printHelp(uiname, defaults)
        sys.exit(1)
    try:
        config, args = configfile.parse_configuration_and_args(defaults,
                                       uiname, sys.argv[1:], 0, 1)

        torrentfile = None
        if len(args):
            torrentfile = args[0]
        for opt in ('responsefile', 'url'):
            if config[opt]:
                print '"--%s"' % opt, _("deprecated, do not use")
                torrentfile = config[opt]
        if torrentfile is not None:
            metainfo, errors = open_arg(torrentfile)
            if errors:
                raise BTFailure(_("Error reading .torrent file: ") + '\n'.join(errors))
        else:
            raise BTFailure(_("you must specify a .torrent file"))
    except BTFailure, e:
        print str(e)
        sys.exit(1)

    errlist = []
    dl = DL(metainfo, config, errlist)
    curses_wrapper(dl.run)

    if errlist:
       print _("These errors occurred during execution:")
       for error in errlist:
          print error
