#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#

import logging

import deluge.component as component
from deluge.decorators import overrides
from deluge.ui.console.modes.basemode import InputKeyHandler
from deluge.ui.console.modes.torrentlist import torrentviewcolumns
from deluge.ui.console.modes.torrentlist.torrentactions import torrent_actions_popup
from deluge.ui.console.utils import curses_util as util
from deluge.ui.console.utils import format_utils
from deluge.ui.console.utils.column import (
    get_column_value,
    get_required_fields,
    torrent_data_fields,
)

from . import ACTION

try:
    import curses
except ImportError:
    pass

log = logging.getLogger(__name__)


state_fg_colors = {
    'Downloading': 'green',
    'Seeding': 'cyan',
    'Error': 'red',
    'Queued': 'yellow',
    'Checking': 'blue',
    'Moving': 'green',
}


reverse_sort_fields = [
    'size',
    'download_speed',
    'upload_speed',
    'num_seeds',
    'num_peers',
    'distributed_copies',
    'time_added',
    'total_uploaded',
    'all_time_download',
    'total_remaining',
    'progress',
    'ratio',
    'seeding_time',
    'active_time',
]


default_column_values = {
    'queue': {'width': 4, 'visible': True},
    'name': {'width': -1, 'visible': True},
    'size': {'width': 8, 'visible': True},
    'progress': {'width': 7, 'visible': True},
    'download_speed': {'width': 7, 'visible': True},
    'upload_speed': {'width': 7, 'visible': True},
    'state': {'width': 13},
    'eta': {'width': 8, 'visible': True},
    'time_added': {'width': 15},
    'tracker': {'width': 15},
    'download_location': {'width': 15},
    'downloaded': {'width': 13},
    'uploaded': {'width': 7},
    'remaining': {'width': 13},
    'completed_time': {'width': 15},
    'last_seen_complete': {'width': 15},
    'max_upload_speed': {'width': 7},
}


default_columns = {}
for col_i, col_name in enumerate(torrentviewcolumns.column_pref_names):
    default_columns[col_name] = {'width': 10, 'order': col_i, 'visible': False}
    if col_name in default_column_values:
        default_columns[col_name].update(default_column_values[col_name])


class TorrentView(InputKeyHandler):
    def __init__(self, torrentlist, config):
        super().__init__()
        self.torrentlist = torrentlist
        self.config = config
        self.filter_dict = {}
        self.curr_filter = None
        self.cached_rows = {}
        self.sorted_ids = None
        self.torrent_names = None
        self.numtorrents = -1
        self.column_string = ''
        self.curoff = 0
        self.marked = []
        self.cursel = 0

    @property
    def rows(self):
        return self.torrentlist.rows

    @property
    def torrent_rows(self):
        return self.torrentlist.rows - 3  # Account for header lines + columns line

    @property
    def torrentlist_offset(self):
        return 2

    def update_state(self, state, refresh=False):
        self.curstate = state  # cache in case we change sort order
        self.cached_rows.clear()
        self.numtorrents = len(state)
        self.sorted_ids = self._sort_torrents(state)
        self.torrent_names = []
        for torrent_id in self.sorted_ids:
            ts = self.curstate[torrent_id]
            self.torrent_names.append(ts['name'])

        if refresh:
            self.torrentlist.refresh()

    def set_torrent_filter(self, state):
        self.curr_filter = state
        filter_dict = {'state': [state]}
        if state == 'All':
            self.curr_filter = None
            filter_dict = {}
        self.filter_dict = filter_dict
        self.torrentlist.go_top = True
        self.torrentlist.update()
        return True

    def _scroll_up(self, by):
        cursel = self.cursel
        prevoff = self.curoff
        self.cursel = max(self.cursel - by, 0)
        if self.cursel < self.curoff:
            self.curoff = self.cursel
        affected = []
        if prevoff == self.curoff:
            affected.append(cursel)
            if cursel != self.cursel:
                affected.insert(0, self.cursel)
        return affected

    def _scroll_down(self, by):
        cursel = self.cursel
        prevoff = self.curoff
        self.cursel = min(self.cursel + by, self.numtorrents - 1)
        if (self.curoff + self.torrent_rows) <= self.cursel:
            self.curoff = self.cursel - self.torrent_rows + 1
        affected = []
        if prevoff == self.curoff:
            affected.append(cursel)
            if cursel != self.cursel:
                affected.append(self.cursel)
        return affected

    def current_torrent_id(self):
        if not self.sorted_ids:
            return None
        return self.sorted_ids[self.cursel]

    def _selected_torrent_ids(self):
        if not self.sorted_ids:
            return None
        ret = []
        for i in self.marked:
            ret.append(self.sorted_ids[i])
        return ret

    def clear_marked(self):
        self.marked = []
        self.last_mark = -1

    def mark_unmark(self, idx):
        if idx in self.marked:
            self.marked.remove(idx)
            self.last_mark = -1
        else:
            self.marked.append(idx)
            self.last_mark = idx

    def add_marked(self, indices, last_marked):
        for i in indices:
            if i not in self.marked:
                self.marked.append(i)
        self.last_mark = last_marked

    def update_marked(self, index, last_mark=True, clear=False):
        if index not in self.marked:
            if clear:
                self.marked = []
            self.marked.append(index)
            if last_mark:
                self.last_mark = index
            return True
        return False

    def _sort_torrents(self, state):
        """Sorts by primary and secondary sort fields."""

        if not state:
            return {}

        s_primary = self.config['torrentview']['sort_primary']
        s_secondary = self.config['torrentview']['sort_secondary']

        result = state

        # Sort first by secondary sort field and then primary sort field
        # so it all works out

        def sort_by_field(state, to_sort, field):
            field = torrent_data_fields[field]['status'][0]
            reverse = field in reverse_sort_fields

            # Get first element so we can check if it has given field
            # and if it's a string
            first_element = state[list(state)[0]]
            if field in first_element:

                def sort_key(s):
                    try:
                        # Sort case-insensitively but preserve A>a order.
                        return state.get(s)[field].lower()
                    except AttributeError:
                        # Not a string.
                        return state.get(s)[field]

                to_sort = sorted(to_sort, key=sort_key, reverse=reverse)

            if field == 'eta':
                to_sort = sorted(to_sort, key=lambda s: state.get(s)['eta'] == 0)

            return to_sort

        # Just in case primary and secondary fields are empty and/or
        # both are too ambiguous, also sort by queue position first
        if 'queue' not in [s_secondary, s_primary]:
            result = sort_by_field(state, result, 'queue')
        if s_secondary != s_primary:
            result = sort_by_field(state, result, s_secondary)
        result = sort_by_field(state, result, s_primary)

        if self.config['torrentview']['separate_complete']:
            result = sorted(
                result, key=lambda s: state.get(s).get('progress', 0) == 100.0
            )

        return result

    def _get_colors(self, row, tidx):
        # default style
        colors = {'fg': 'white', 'bg': 'black', 'attr': None}

        if tidx in self.marked:
            colors.update({'bg': 'blue', 'attr': 'bold'})

        if tidx == self.cursel:
            col_selected = {'bg': 'white', 'fg': 'black', 'attr': 'bold'}
            if tidx in self.marked:
                col_selected['fg'] = 'blue'
            colors.update(col_selected)

        colors['fg'] = state_fg_colors.get(row[1], colors['fg'])

        if self.torrentlist.minor_mode:
            self.torrentlist.minor_mode.update_colors(tidx, colors)
        return colors

    def update_torrents(self, lines):
        # add all the torrents
        if self.numtorrents == 0:
            cols = self.torrentlist.torrentview_columns()
            msg = 'No torrents match filter'.center(cols)
            self.torrentlist.add_string(
                3, '{!info!}%s' % msg, scr=self.torrentlist.torrentview_panel
            )
        elif self.numtorrents == 0:
            self.torrentlist.add_string(1, 'Waiting for torrents from core...')
            return

        def draw_row(index):
            if index not in self.cached_rows:
                ts = self.curstate[self.sorted_ids[index]]
                self.cached_rows[index] = (
                    format_utils.format_row(
                        [get_column_value(name, ts) for name in self.cols_to_show],
                        self.column_widths,
                    ),
                    ts['state'],
                )
            return self.cached_rows[index]

        tidx = self.curoff
        currow = 0
        todraw = []
        # Affected lines are given when changing selected torrent
        if lines:
            for line in lines:
                if line < tidx:
                    continue
                if line >= (tidx + self.torrent_rows) or line >= self.numtorrents:
                    break
                todraw.append((line, line - self.curoff, draw_row(line)))
        else:
            for i in range(tidx, tidx + self.torrent_rows):
                if i >= self.numtorrents:
                    break
                todraw.append((i, i - self.curoff, draw_row(i)))

        for tidx, currow, row in todraw:
            if (currow + self.torrentlist_offset - 1) > self.torrent_rows:
                continue
            colors = self._get_colors(row, tidx)
            if colors['attr']:
                colorstr = '{!%(fg)s,%(bg)s,%(attr)s!}' % colors
            else:
                colorstr = '{!%(fg)s,%(bg)s!}' % colors

            self.torrentlist.add_string(
                currow + self.torrentlist_offset,
                f'{colorstr}{row[0]}',
                trim=False,
                scr=self.torrentlist.torrentview_panel,
            )

    def update(self, refresh=False):
        d = component.get('SessionProxy').get_torrents_status(
            self.filter_dict, self.status_fields
        )
        d.addCallback(self.update_state, refresh=refresh)

    def on_config_changed(self):
        s_primary = self.config['torrentview']['sort_primary']
        s_secondary = self.config['torrentview']['sort_secondary']
        changed = None
        for col in default_columns:
            if col not in self.config['torrentview']['columns']:
                changed = self.config['torrentview']['columns'][col] = default_columns[
                    col
                ]
        if changed:
            self.config.save()

        self.cols_to_show = [
            col
            for col in sorted(
                self.config['torrentview']['columns'],
                key=lambda k: self.config['torrentview']['columns'][k]['order'],
            )
            if self.config['torrentview']['columns'][col]['visible']
        ]
        self.status_fields = get_required_fields(self.cols_to_show)

        # we always need these, even if we're not displaying them
        for rf in ['state', 'name', 'queue', 'progress']:
            if rf not in self.status_fields:
                self.status_fields.append(rf)

        # same with sort keys
        if s_primary and s_primary not in self.status_fields:
            self.status_fields.append(s_primary)
        if s_secondary and s_secondary not in self.status_fields:
            self.status_fields.append(s_secondary)

        self.update_columns()

    def update_columns(self):
        self.column_widths = [
            self.config['torrentview']['columns'][col]['width']
            for col in self.cols_to_show
        ]
        requested_width = sum(width for width in self.column_widths if width >= 0)

        cols = self.torrentlist.torrentview_columns()
        if requested_width > cols:  # can't satisfy requests, just spread out evenly
            cw = int(cols / len(self.cols_to_show))
            for i in range(0, len(self.column_widths)):
                self.column_widths[i] = cw
        else:
            rem = cols - requested_width
            var_cols = len([width for width in self.column_widths if width < 0])
            if var_cols > 0:
                vw = int(rem / var_cols)
                for i in range(0, len(self.column_widths)):
                    if self.column_widths[i] < 0:
                        self.column_widths[i] = vw

        self.column_string = '{!header!}'

        primary_sort_col_name = self.config['torrentview']['sort_primary']

        for i, column in enumerate(self.cols_to_show):
            ccol = torrent_data_fields[column]['name']
            width = self.column_widths[i]

            # Trim the column if it's too long to fit
            if len(ccol) > width:
                ccol = ccol[: width - 1]

            # Padding
            ccol += ' ' * (width - len(ccol))

            # Highlight the primary sort column
            if column == primary_sort_col_name:
                if i != len(self.cols_to_show) - 1:
                    ccol = '{!black,green,bold!}%s{!header!}' % ccol
                else:
                    ccol = ('{!black,green,bold!}%s' % ccol)[:-1]

            self.column_string += ccol

    @overrides(InputKeyHandler)
    def handle_read(self, c):
        affected_lines = None
        if c == curses.KEY_UP:
            if self.cursel != 0:
                affected_lines = self._scroll_up(1)
        elif c == curses.KEY_PPAGE:
            affected_lines = self._scroll_up(int(self.torrent_rows / 2))
        elif c == curses.KEY_DOWN:
            if self.cursel < self.numtorrents:
                affected_lines = self._scroll_down(1)
        elif c == curses.KEY_NPAGE:
            affected_lines = self._scroll_down(int(self.torrent_rows / 2))
        elif c == curses.KEY_HOME:
            affected_lines = self._scroll_up(self.cursel)
        elif c == curses.KEY_END:
            affected_lines = self._scroll_down(self.numtorrents - self.cursel)
        elif c == curses.KEY_DC:  # DEL
            added = self.update_marked(self.cursel)

            def on_close(**kwargs):
                if added:
                    self.marked.pop()

            torrent_actions_popup(
                self.torrentlist,
                self._selected_torrent_ids(),
                action=ACTION.REMOVE,
                close_cb=on_close,
            )
        elif c in [curses.KEY_ENTER, util.KEY_ENTER2] and self.numtorrents:
            added = self.update_marked(self.cursel)

            def on_close(data, **kwargs):
                if added:
                    self.marked.remove(self.cursel)

            torrent_actions_popup(
                self.torrentlist,
                self._selected_torrent_ids(),
                details=True,
                close_cb=on_close,
            )
            self.torrentlist.refresh()
        elif c == ord('j'):
            affected_lines = self._scroll_down(1)
        elif c == ord('k'):
            affected_lines = self._scroll_up(1)
        elif c == ord('m'):
            self.mark_unmark(self.cursel)
            affected_lines = [self.cursel]
        elif c == ord('M'):
            if self.last_mark >= 0:
                if self.cursel > self.last_mark:
                    mrange = list(range(self.last_mark, self.cursel + 1))
                else:
                    mrange = list(range(self.cursel, self.last_mark))
                self.add_marked(mrange, self.cursel)
                affected_lines = mrange
            else:
                self.mark_unmark(self.cursel)
                affected_lines = [self.cursel]
        elif c == ord('c'):
            self.clear_marked()
        elif c == ord('o'):
            if not self.marked:
                added = self.update_marked(self.cursel, clear=True)
            else:
                self.last_mark = -1
            torrent_actions_popup(
                self.torrentlist,
                self._selected_torrent_ids(),
                action=ACTION.TORRENT_OPTIONS,
            )
        elif c in [ord('>'), ord('<')]:
            try:
                i = self.cols_to_show.index(self.config['torrentview']['sort_primary'])
            except ValueError:
                i = 0 if chr(c) == '<' else len(self.cols_to_show)
            else:
                i += 1 if chr(c) == '>' else -1

            i = max(0, min(len(self.cols_to_show) - 1, i))
            self.config['torrentview']['sort_primary'] = self.cols_to_show[i]
            self.config.save()
            self.on_config_changed()
            self.update_columns()
            self.torrentlist.refresh([])
        else:
            return util.ReadState.IGNORED

        self.set_input_result(affected_lines)
        return util.ReadState.CHANGED if affected_lines else util.ReadState.READ
