#
# input_popup.py
#
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
#
# Complete function from commands/add.py:
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
# Deluge is free software.
#
# You may 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.
#
# deluge 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 deluge.    If not, write to:
# 	The Free Software Foundation, Inc.,
# 	51 Franklin Street, Fifth Floor
# 	Boston, MA  02110-1301, USA.
#
#    In addition, as a special exception, the copyright holders give
#    permission to link the code of portions of this program with the OpenSSL
#    library.
#    You must obey the GNU General Public License in all respects for all of
#    the code used other than OpenSSL. If you modify file(s) with this
#    exception, you may extend this exception to your version of the file(s),
#    but you are not obligated to do so. If you do not wish to do so, delete
#    this exception statement from your version. If you delete this exception
#    statement from all source files in the program, then also delete it here.
#
#

try:
    import curses
except ImportError:
    pass

import logging,os,os.path

from popup import Popup

log = logging.getLogger(__name__)

class InputField:
    depend = None
    # render the input.  return number of rows taken up
    def render(self,screen,row,width,selected,col=1):
        return 0
    def handle_read(self, c):
        if c in [curses.KEY_ENTER, 10, 127, 113]:
            return True
        return False
    def get_value(self):
        return None
    def set_value(self, value):
        pass

    def set_depend(self,i,inverse=False):
        if not isinstance(i,CheckedInput):
            raise Exception("Can only depend on CheckedInputs")
        self.depend = i
        self.inverse = inverse

    def depend_skip(self):
        if not self.depend:
            return False
        if self.inverse:
            return self.depend.checked
        else:
            return not self.depend.checked

class CheckedInput(InputField):
    def __init__(self, parent, message, name, checked=False):
        self.parent = parent
        self.chkd_inact = "[X] %s"%message
        self.unchkd_inact = "[ ] %s"%message
        self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s"%message
        self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s"%message
        self.name = name
        self.checked = checked

    def render(self, screen, row, width, active, col=1):
        if self.checked and active:
            self.parent.add_string(row,self.chkd_act,screen,col,False,True)
        elif self.checked:
            self.parent.add_string(row,self.chkd_inact,screen,col,False,True)
        elif active:
            self.parent.add_string(row,self.unchkd_act,screen,col,False,True)
        else:
            self.parent.add_string(row,self.unchkd_inact,screen,col,False,True)
        return 1
        
    def handle_read(self, c):
        if c == 32:
            self.checked = not self.checked

    def get_value(self):
        return self.checked

    def set_value(self, c):
        self.checked = c


class CheckedPlusInput(InputField):
    def __init__(self, parent, message, name, child,checked=False):
        self.parent = parent
        self.chkd_inact = "[X] %s"%message
        self.unchkd_inact = "[ ] %s"%message
        self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s"%message
        self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s"%message
        self.name = name
        self.checked = checked
        self.msglen = len(self.chkd_inact)+1
        self.child = child
        self.child_active = False

    def render(self, screen, row, width, active, col=1):
        isact = active and not self.child_active
        if self.checked and isact:
            self.parent.add_string(row,self.chkd_act,screen,col,False,True)
        elif self.checked:
            self.parent.add_string(row,self.chkd_inact,screen,col,False,True)
        elif isact:
            self.parent.add_string(row,self.unchkd_act,screen,col,False,True)
        else:
            self.parent.add_string(row,self.unchkd_inact,screen,col,False,True)

        if active and self.checked and self.child_active:
            self.parent.add_string(row+1,"(esc to leave)",screen,col,False,True)
        elif active and self.checked:
            self.parent.add_string(row+1,"(right arrow to edit)",screen,col,False,True)
        rows = 2
        # show child
        if self.checked:
            if isinstance(self.child,(TextInput,IntSpinInput,FloatSpinInput)):
                crows = self.child.render(screen,row,width-self.msglen,self.child_active and active,col+self.msglen,self.msglen)
            else:
                crows = self.child.render(screen,row,width-self.msglen,self.child_active and active,col+self.msglen)
            rows = max(rows,crows)
        else:
            self.parent.add_string(row,"(enable to view/edit value)",screen,col+self.msglen,False,True)
        return rows
        
    def handle_read(self, c):
        if self.child_active:
            if c == 27: # leave child on esc
                self.child_active = False
                return
            # pass keys through to child
            self.child.handle_read(c)
        else:
            if c == 32:
                self.checked = not self.checked
            if c == curses.KEY_RIGHT:
                self.child_active = True

    def get_value(self):
        return self.checked

    def set_value(self, c):
        self.checked = c

    def get_child(self):
        return self.child



class IntSpinInput(InputField):
    def __init__(self, parent, message, name, move_func, value, min_val, max_val):
        self.parent = parent
        self.message = message
        self.name = name
        self.value = int(value)
        self.initvalue = self.value
        self.valstr = "%d"%self.value
        self.cursor = len(self.valstr)
        self.cursoff = len(self.message)+4 # + 4 for the " [ " in the rendered string
        self.move_func = move_func
        self.min_val = min_val
        self.max_val = max_val
        self.need_update = False

    def render(self, screen, row, width, active, col=1, cursor_offset=0):
        if not active and self.need_update:
            if not self.valstr or self.valstr == '-':
                self.value = self.initvalue
            else:
                self.value = int(self.valstr)
                if self.value < self.min_val:
                    self.value = self.min_val
                if self.value > self.max_val:
                    self.value = self.max_val
            self.valstr = "%d"%self.value 
            self.cursor = len(self.valstr)
            self.need_update = False
        if not self.valstr:
            self.parent.add_string(row,"%s [  ]"%self.message,screen,col,False,True)
        elif active:
            self.parent.add_string(row,"%s [ {!black,white,bold!}%s{!white,black!} ]"%(self.message,self.valstr),screen,col,False,True)
        else:
            self.parent.add_string(row,"%s [ %s ]"%(self.message,self.valstr),screen,col,False,True)

        if active:
            self.move_func(row,self.cursor+self.cursoff+cursor_offset)

        return 1

    def handle_read(self, c):
        if c  == curses.KEY_PPAGE and self.value < self.max_val:
            self.value+=1
            self.valstr = "%d"%self.value
            self.cursor = len(self.valstr)
        elif c == curses.KEY_NPAGE and self.value > self.min_val:
            self.value-=1
            self.valstr = "%d"%self.value
            self.cursor = len(self.valstr)
        elif c == curses.KEY_LEFT:
            self.cursor = max(0,self.cursor-1)
        elif c == curses.KEY_RIGHT:
            self.cursor = min(len(self.valstr),self.cursor+1)
        elif c == curses.KEY_HOME:
            self.cursor = 0
        elif c == curses.KEY_END:
            self.cursor = len(self.valstr)
        elif c == curses.KEY_BACKSPACE or c == 127:
            if self.valstr and  self.cursor > 0:
                self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
                self.cursor-=1
                self.need_update = True
        elif c == curses.KEY_DC:
            if self.valstr and self.cursor < len(self.valstr):
                self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:]
                self.need_update = True
        elif c == 45 and self.cursor == 0 and self.min_val < 0:
            minus_place = self.valstr.find('-')
            if minus_place >= 0: return
            self.valstr = chr(c)+self.valstr
            self.cursor += 1
            self.need_update = True
        elif c > 47 and c < 58:
            if c == 48 and self.cursor == 0: return
            minus_place = self.valstr.find('-')
            if self.cursor <= minus_place: return
            if self.cursor == len(self.valstr):
                self.valstr += chr(c)
            else:
                # Insert into string
                self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
            self.need_update = True
            # Move the cursor forward
            self.cursor+=1
            

    def get_value(self):
        return self.value

    def set_value(self, val):
        self.value = int(val)
        self.valstr = "%d"%self.value
        self.cursor = len(self.valstr)


class FloatSpinInput(InputField):
    def __init__(self, parent, message, name, move_func, value, inc_amt, precision, min_val, max_val):
        self.parent = parent
        self.message = message
        self.name = name
        self.precision = precision
        self.inc_amt = inc_amt
        self.value = round(float(value),self.precision)
        self.initvalue = self.value
        self.fmt = "%%.%df"%precision
        self.valstr = self.fmt%self.value
        self.cursor = len(self.valstr)
        self.cursoff = len(self.message)+4 # + 4 for the " [ " in the rendered string
        self.move_func = move_func
        self.min_val = min_val
        self.max_val = max_val
        self.need_update = False

    def __limit_value(self):
        if self.value < self.min_val:
            self.value = self.min_val
        if self.value > self.max_val:
            self.value = self.max_val

    def render(self, screen, row, width, active, col=1, cursor_offset=0):
        if not active and self.need_update:
            try:
                self.value = round(float(self.valstr),self.precision)
                self.__limit_value()
            except ValueError:
                self.value = self.initvalue
            self.valstr = self.fmt%self.value
            self.cursor = len(self.valstr)
            self.need_update = False
        if not self.valstr:
            self.parent.add_string(row,"%s [  ]"%self.message,screen,col,False,True)
        elif active:
            self.parent.add_string(row,"%s [ {!black,white,bold!}%s{!white,black!} ]"%(self.message,self.valstr),screen,col,False,True)
        else:
            self.parent.add_string(row,"%s [ %s ]"%(self.message,self.valstr),screen,col,False,True)
        if active:
            self.move_func(row,self.cursor+self.cursoff+cursor_offset)

        return 1

    def handle_read(self, c):
        if c  == curses.KEY_PPAGE:
            self.value+=self.inc_amt
            self.__limit_value()
            self.valstr = self.fmt%self.value
            self.cursor = len(self.valstr)
        elif c == curses.KEY_NPAGE:
            self.value-=self.inc_amt
            self.__limit_value()
            self.valstr = self.fmt%self.value
            self.cursor = len(self.valstr)
        elif c == curses.KEY_LEFT:
            self.cursor = max(0,self.cursor-1)
        elif c == curses.KEY_RIGHT:
            self.cursor = min(len(self.valstr),self.cursor+1)
        elif c == curses.KEY_HOME:
            self.cursor = 0
        elif c == curses.KEY_END:
            self.cursor = len(self.valstr)
        elif c == curses.KEY_BACKSPACE or c == 127:
            if self.valstr and  self.cursor > 0:
                self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
                self.cursor-=1
                self.need_update = True
        elif c == curses.KEY_DC:
            if self.valstr and self.cursor < len(self.valstr):
                self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:]
                self.need_update = True
        elif c == 45 and self.cursor == 0 and self.min_val < 0:
            minus_place = self.valstr.find('-')
            if minus_place >= 0: return
            self.valstr = chr(c)+self.valstr
            self.cursor += 1
            self.need_update = True
        elif c == 46:
            minus_place = self.valstr.find('-')
            if self.cursor <= minus_place: return
            point_place = self.valstr.find('.')
            if point_place >= 0: return
            if self.cursor == len(self.valstr):
                self.valstr += chr(c)
            else:
                # Insert into string
                self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
            self.need_update = True
            # Move the cursor forward
            self.cursor+=1
        elif (c > 47 and c < 58):
            minus_place = self.valstr.find('-')
            if self.cursor <= minus_place: return
            if self.cursor == len(self.valstr):
                self.valstr += chr(c)
            else:
                # Insert into string
                self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
            self.need_update = True
            # Move the cursor forward
            self.cursor+=1
            

    def get_value(self):
        return self.value

    def set_value(self, val):
        self.value = round(float(val),self.precision)
        self.valstr = self.fmt%self.value
        self.cursor = len(self.valstr)


class SelectInput(InputField):
    def __init__(self, parent, message, name, opts, vals, selidx):
        self.parent = parent
        self.message = message
        self.name = name
        self.opts = opts
        self.vals = vals
        self.selidx = selidx

    def render(self, screen, row, width, selected, col=1):
        if self.message:
            self.parent.add_string(row,self.message,screen,col,False,True)
            row += 1
        off = col+1
        for i,opt in enumerate(self.opts):
            if selected and i == self.selidx:
                self.parent.add_string(row,"{!black,white,bold!}[%s]"%opt,screen,off,False,True)
            elif i == self.selidx:
                self.parent.add_string(row,"[{!white,black,underline!}%s{!white,black!}]"%opt,screen,off,False,True)
            else:
                self.parent.add_string(row,"[%s]"%opt,screen,off,False,True)
            off += len(opt)+3
        if self.message:
            return 2
        else:
            return 1

    def handle_read(self, c):
        if c == curses.KEY_LEFT:
            self.selidx = max(0,self.selidx-1)
        if c == curses.KEY_RIGHT:
            self.selidx = min(len(self.opts)-1,self.selidx+1)

    def get_value(self):
        return self.vals[self.selidx]

    def set_value(self, nv):
        for i,val in enumerate(self.vals):
            if nv == val:
                self.selidx = i
                return
        raise Exception("Invalid value for SelectInput")

class TextInput(InputField):
    def __init__(self, parent, move_func, width, message, name, value, docmp):
        self.parent = parent
        self.move_func = move_func
        self.width = width

        self.message = message
        self.name = name
        self.value = value
        self.docmp = docmp

        self.tab_count = 0
        self.cursor = len(self.value)
        self.opts = None
        self.opt_off = 0

    def render(self,screen,row,width,selected,col=1,cursor_offset=0):
        if self.message:
            self.parent.add_string(row,self.message,screen,col,False,True)
            row += 1
        if selected:
            if self.opts:
                self.parent.add_string(row+1,self.opts[self.opt_off:],screen,col,False,True)
            if self.cursor > (width-3):
                self.move_func(row,width-2)
            else:
                self.move_func(row,self.cursor+1+cursor_offset)
        slen = len(self.value)+3
        if slen > width:
            vstr = self.value[(slen-width):]
        else:
            vstr = self.value.ljust(width-2)
        self.parent.add_string(row,"{!black,white,bold!}%s"%vstr,screen,col,False,False)

        if self.message:
            return 3
        else:
            return 2

    def get_value(self):
        return self.value

    def set_value(self,val):
        self.value = val
        self.cursor = len(self.value)

    # most of the cursor,input stuff here taken from ui/console/screen.py
    def handle_read(self,c):
        if c == 9 and self.docmp:
            # Keep track of tab hit count to know when it's double-hit
            self.tab_count += 1
            if self.tab_count > 1:
                second_hit = True
                self.tab_count = 0
            else:
                second_hit = False

            # We only call the tab completer function if we're at the end of
            # the input string on the cursor is on a space
            if self.cursor == len(self.value) or self.value[self.cursor] == " ":
                if self.opts:
                    prev = self.opt_off
                    self.opt_off += self.width-3
                    # now find previous double space, best guess at a split point
                    # in future could keep opts unjoined to get this really right
                    self.opt_off = self.opts.rfind("  ",0,self.opt_off)+2
                    if second_hit and self.opt_off == prev: # double tap and we're at the end
                        self.opt_off = 0
                else:
                    opts = self.complete(self.value)
                    if len(opts) == 1: # only one option, just complete it
                        self.value = opts[0]
                        self.cursor = len(opts[0])
                        self.tab_count = 0
                    elif len(opts) > 1:
                        prefix = os.path.commonprefix(opts)
                        if prefix:
                            self.value = prefix
                            self.cursor = len(prefix)

                    if len(opts) > 1 and second_hit: # display multiple options on second tab hit
                        sp = self.value.rfind(os.sep)+1
                        self.opts = "  ".join([o[sp:] for o in opts])

        # Cursor movement
        elif c == curses.KEY_LEFT:
            self.cursor = max(0,self.cursor-1)
        elif c == curses.KEY_RIGHT:
            self.cursor = min(len(self.value),self.cursor+1)
        elif c == curses.KEY_HOME:
            self.cursor = 0
        elif c == curses.KEY_END:
            self.cursor = len(self.value)

        if c != 9:
            self.opts = None
            self.opt_off = 0
            self.tab_count = 0

        # Delete a character in the input string based on cursor position
        if c == curses.KEY_BACKSPACE or c == 127:
            if self.value and  self.cursor > 0:
                self.value = self.value[:self.cursor - 1] + self.value[self.cursor:]
                self.cursor-=1
        elif c == curses.KEY_DC:
            if self.value and self.cursor < len(self.value):
                self.value = self.value[:self.cursor] + self.value[self.cursor+1:]
        elif c > 31 and c < 256:
            # Emulate getwch
            stroke = chr(c)
            uchar = ""
            while not uchar:
                try:
                    uchar = stroke.decode(self.parent.encoding)
                except UnicodeDecodeError:
                    c = self.parent.stdscr.getch()
                    stroke += chr(c)
            if uchar:
                if self.cursor == len(self.value):
                    self.value += uchar
                else:
                    # Insert into string
                    self.value = self.value[:self.cursor] + uchar + self.value[self.cursor:]
                # Move the cursor forward
                self.cursor+=1
        

    def complete(self,line):
        line = os.path.abspath(os.path.expanduser(line))
        ret = []
        if os.path.exists(line):
            # This is a correct path, check to see if it's a directory
            if os.path.isdir(line):
                # Directory, so we need to show contents of directory
                #ret.extend(os.listdir(line))
                for f in os.listdir(line):
                    # Skip hidden
                    if f.startswith("."):
                        continue
                    f = os.path.join(line, f)
                    if os.path.isdir(f):
                        f += os.sep
                    ret.append(f)
            else:
                # This is a file, but we could be looking for another file that
                # shares a common prefix.
                for f in os.listdir(os.path.dirname(line)):
                    if f.startswith(os.path.split(line)[1]):
                        ret.append(os.path.join( os.path.dirname(line), f))
        else:
            # This path does not exist, so lets do a listdir on it's parent
            # and find any matches.
            ret = []
            if os.path.isdir(os.path.dirname(line)):
                for f in os.listdir(os.path.dirname(line)):
                    if f.startswith(os.path.split(line)[1]):
                        p = os.path.join(os.path.dirname(line), f)

                        if os.path.isdir(p):
                            p += os.sep
                        ret.append(p)
        return ret


class InputPopup(Popup):
    def __init__(self,parent_mode,title,width_req=-1,height_req=-1,close_cb=None):
        Popup.__init__(self,parent_mode,title,width_req,height_req,close_cb)
        self.inputs = []
        self.spaces = []
        self.current_input = 0

    def move(self,r,c):
        self._cursor_row = r
        self._cursor_col = c

    def add_text_input(self, message, name, value="", complete=True):
        """ 
        Add a text input field to the popup.
        
        :param message: string to display above the input field
        :param name: name of the field, for the return callback
        :param value: initial value of the field
        :param complete: should completion be run when tab is hit and this field is active
        """
        self.inputs.append(TextInput(self.parent, self.move, self.width, message,
                                     name, value, complete))

    def add_spaces(self, num):
        self.spaces.append((len(self.inputs)-1,num))

    def add_select_input(self, message, name, opts, vals, default_index=0):
        self.inputs.append(SelectInput(self.parent, message, name, opts, vals, default_index))

    def add_checked_input(self, message, name, checked=False):
        self.inputs.append(CheckedInput(self.parent,message,name,checked))

    def _refresh_lines(self):
        self._cursor_row = -1
        self._cursor_col = -1
        curses.curs_set(0)
        crow = 1
        spos = 0
        for i,ipt in enumerate(self.inputs):
            crow += ipt.render(self.screen,crow,self.width,i==self.current_input)
            if self.spaces and (spos < len(self.spaces)) and (i == self.spaces[spos][0]):
                crow += self.spaces[spos][1]
                spos += 1

        # need to do this last as adding things moves the cursor
        if self._cursor_row >= 0:
            curses.curs_set(2)
            self.screen.move(self._cursor_row,self._cursor_col)

    def handle_read(self, c):
        if c == curses.KEY_UP:
            self.current_input = max(0,self.current_input-1)
        elif c == curses.KEY_DOWN:
            self.current_input = min(len(self.inputs)-1,self.current_input+1)
        elif c == curses.KEY_ENTER or c == 10:
            if self.close_cb:
                vals = {}
                for ipt in self.inputs:
                    vals[ipt.name] = ipt.get_value()
                curses.curs_set(0)
                self.close_cb(vals)
            return True # close the popup
        elif c == 27: # close on esc, no action
            return True
        elif self.inputs:
            self.inputs[self.current_input].handle_read(c)

        self.refresh()
        return False

