# -*- coding: utf-8 -*-
#*****************************************************************************
#       Copyright (C) 2003-2006 Gary Bishop.
#       Copyright (C) 2006  Jorgen Stenarson. <jorgen.stenarson@bostream.nu>
#
#  Distributed under the terms of the BSD License.  The full license is in
#  the file COPYING, distributed as part of this software.
#*****************************************************************************
from __future__ import print_function, unicode_literals, absolute_import
''' an attempt to implement readline for Python in Python using ctypes'''
import sys,os,re,time
from glob import glob

from . import release
from .py3k_compat import callable, execfile

import pyreadline.lineeditor.lineobj as lineobj
import pyreadline.lineeditor.history as history
import pyreadline.clipboard as clipboard
import pyreadline.console as console
import pyreadline.logger as logger

from pyreadline.keysyms.common import make_KeyPress_from_keydescr
from pyreadline.unicode_helper import ensure_unicode, ensure_str
from .logger import log
from .modes import editingmodes
from .error import ReadlineError, GetSetError

in_ironpython = "IronPython" in sys.version
if in_ironpython:#ironpython does not provide a prompt string to readline
    import System
    default_prompt = ">>> "
else:
    default_prompt = ""


class MockConsoleError(Exception):
    pass

class MockConsole(object):
    """object used during refactoring. Should raise errors when someone tries to use it.
    """
    def __setattr__(self, x):
        raise MockConsoleError("Should not try to get attributes from MockConsole")

    def cursor(self, size=50):
        pass

class BaseReadline(object):
    def __init__(self):
        self.allow_ctrl_c = False
        self.ctrl_c_tap_time_interval = 0.3

        self.debug = False
        self.bell_style = 'none'
        self.mark = -1
        self.console=MockConsole()
        self.disable_readline = False
        # this code needs to follow l_buffer and history creation
        self.editingmodes = [mode(self) for mode in editingmodes]
        for mode in self.editingmodes:
            mode.init_editing_mode(None)
        self.mode = self.editingmodes[0]

        self.read_inputrc()
        log("\n".join(self.mode.rl_settings_to_string()))

        self.callback = None

    def parse_and_bind(self, string):
        '''Parse and execute single line of a readline init file.'''
        try:
            log('parse_and_bind("%s")' % string)
            if string.startswith('#'):
                return
            if string.startswith('set'):
                m = re.compile(r'set\s+([-a-zA-Z0-9]+)\s+(.+)\s*$').match(string)
                if m:
                    var_name = m.group(1)
                    val = m.group(2)
                    try:
                        setattr(self.mode, var_name.replace('-','_'), val)
                    except AttributeError:
                        log('unknown var="%s" val="%s"' % (var_name, val))
                else:
                    log('bad set "%s"' % string)
                return
            m = re.compile(r'\s*(.+)\s*:\s*([-a-zA-Z]+)\s*$').match(string)
            if m:
                key = m.group(1)
                func_name = m.group(2)
                py_name = func_name.replace('-', '_')
                try:
                    func = getattr(self.mode, py_name)
                except AttributeError:
                    log('unknown func key="%s" func="%s"' % (key, func_name))
                    if self.debug:
                        print('pyreadline parse_and_bind error, unknown function to bind: "%s"' % func_name)
                    return
                self.mode._bind_key(key, func)
        except:
            log('error')
            raise

    def _set_prompt(self, prompt):
        self.mode.prompt = prompt

    def _get_prompt(self):
        return self.mode.prompt

    prompt = property(_get_prompt, _set_prompt)


    def get_line_buffer(self):
        '''Return the current contents of the line buffer.'''
        return self.mode.l_buffer.get_line_text()

    def insert_text(self, string):
        '''Insert text into the command line.'''
        self.mode.insert_text(string)

    def read_init_file(self, filename=None):
        '''Parse a readline initialization file. The default filename is the last filename used.'''
        log('read_init_file("%s")' % filename)

    #History file book keeping methods (non-bindable)

    def add_history(self, line):
        '''Append a line to the history buffer, as if it was the last line typed.'''
        self.mode._history.add_history(line)

    def get_current_history_length(self ):
        '''Return the number of lines currently in the history.
        (This is different from get_history_length(), which returns
        the maximum number of lines that will be written to a history file.)'''
        return self.mode._history.get_current_history_length()

    def get_history_length(self ):
        '''Return the desired length of the history file.

        Negative values imply unlimited history file size.'''
        return self.mode._history.get_history_length()

    def set_history_length(self, length):
        '''Set the number of lines to save in the history file.

        write_history_file() uses this value to truncate the history file
        when saving. Negative values imply unlimited history file size.
        '''
        self.mode._history.set_history_length(length)

    def get_history_item(self, index):
        '''Return the current contents of history item at index.'''
        return self.mode._history.get_history_item(index)

    def clear_history(self):
        '''Clear readline history'''
        self.mode._history.clear_history()

    def read_history_file(self, filename=None):
        '''Load a readline history file. The default filename is ~/.history.'''
        if filename is None:
            filename = self.mode._history.history_filename
        log("read_history_file from %s"%ensure_unicode(filename))
        self.mode._history.read_history_file(filename)

    def write_history_file(self, filename=None):
        '''Save a readline history file. The default filename is ~/.history.'''
        self.mode._history.write_history_file(filename)

    #Completer functions

    def set_completer(self, function=None):
        '''Set or remove the completer function.

        If function is specified, it will be used as the new completer
        function; if omitted or None, any completer function already
        installed is removed. The completer function is called as
        function(text, state), for state in 0, 1, 2, ..., until it returns a
        non-string value. It should return the next possible completion
        starting with text.
        '''
        log('set_completer')
        self.mode.completer = function

    def get_completer(self):
        '''Get the completer function.
        '''
        log('get_completer')
        return self.mode.completer

    def get_begidx(self):
        '''Get the beginning index of the readline tab-completion scope.'''
        return self.mode.begidx

    def get_endidx(self):
        '''Get the ending index of the readline tab-completion scope.'''
        return self.mode.endidx

    def set_completer_delims(self, string):
        '''Set the readline word delimiters for tab-completion.'''
        self.mode.completer_delims = string

    def get_completer_delims(self):
        '''Get the readline word delimiters for tab-completion.'''
        if sys.version_info[0] < 3:
            return self.mode.completer_delims.encode("ascii")
        else:
            return self.mode.completer_delims

    def set_startup_hook(self, function=None):
        '''Set or remove the startup_hook function.

        If function is specified, it will be used as the new startup_hook
        function; if omitted or None, any hook function already installed is
        removed. The startup_hook function is called with no arguments just
        before readline prints the first prompt.

        '''
        self.mode.startup_hook = function

    def set_pre_input_hook(self, function=None):
        '''Set or remove the pre_input_hook function.

        If function is specified, it will be used as the new pre_input_hook
        function; if omitted or None, any hook function already installed is
        removed. The pre_input_hook function is called with no arguments
        after the first prompt has been printed and just before readline
        starts reading input characters.

        '''
        self.mode.pre_input_hook = function

#Functions that are not relevant for all Readlines but should at least have a NOP

    def _bell(self):
        pass

#
# Standard call, not available for all implementations
#

    def readline(self, prompt=''):
        raise NotImplementedError

    def redisplay(self):
        raise NotImplementedError

#
# Callback interface
#
    def process_keyevent(self, keyinfo):
        return self.mode.process_keyevent(keyinfo)

    def readline_setup(self, prompt=""):
        return self.mode.readline_setup(prompt)

    def keyboard_poll(self):
        return self.mode._readline_from_keyboard_poll()

    def callback_handler_install(self, prompt, callback):
        '''bool readline_callback_handler_install ( string prompt, callback callback)
        Initializes the readline callback interface and terminal, prints the prompt and returns immediately
        '''
        self.callback = callback
        self.readline_setup(prompt)

    def callback_handler_remove(self):
        '''Removes a previously installed callback handler and restores terminal settings'''
        self.callback = None

    def callback_read_char(self):
        '''Reads a character and informs the readline callback interface when a line is received'''
        if self.keyboard_poll():
            line = self.get_line_buffer() + '\n'
            # however there is another newline added by
            # self.mode.readline_setup(prompt) which is called by callback_handler_install
            # this differs from GNU readline
            self.add_history(self.mode.l_buffer)
            # TADA:
            self.callback(line)

    def read_inputrc(self, #in 2.4 we cannot call expanduser with unicode string
                     inputrcpath=os.path.expanduser(ensure_str("~/pyreadlineconfig.ini"))):
        modes = dict([(x.mode,x) for x in self.editingmodes])
        mode = self.editingmodes[0].mode

        def setmode(name):
            self.mode = modes[name]

        def bind_key(key, name):
            import types
            if callable(name):
                modes[mode]._bind_key(key, types.MethodType(name, modes[mode]))
            elif hasattr(modes[mode], name):
                modes[mode]._bind_key(key, getattr(modes[mode], name))
            else:
                print("Trying to bind unknown command '%s' to key '%s'"%(name, key))

        def un_bind_key(key):
            keyinfo = make_KeyPress_from_keydescr(key).tuple()
            if keyinfo in modes[mode].key_dispatch:
                del modes[mode].key_dispatch[keyinfo]

        def bind_exit_key(key):
            modes[mode]._bind_exit_key(key)

        def un_bind_exit_key(key):
            keyinfo = make_KeyPress_from_keydescr(key).tuple()
            if keyinfo in modes[mode].exit_dispatch:
                del modes[mode].exit_dispatch[keyinfo]

        def setkill_ring_to_clipboard(killring):
            import pyreadline.lineeditor.lineobj
            pyreadline.lineeditor.lineobj.kill_ring_to_clipboard = killring

        def sethistoryfilename(filename):
            self.mode._history.history_filename = os.path.expanduser(ensure_str(filename))

        def setbellstyle(mode):
            self.bell_style = mode

        def disable_readline(mode):
            self.disable_readline = mode

        def sethistorylength(length):
            self.mode._history.history_length = int(length)

        def allow_ctrl_c(mode):
            log("allow_ctrl_c:%s:%s"%(self.allow_ctrl_c, mode))
            self.allow_ctrl_c = mode

        def setbellstyle(mode):
            self.bell_style = mode

        def show_all_if_ambiguous(mode):
            self.mode.show_all_if_ambiguous = mode

        def ctrl_c_tap_time_interval(mode):
            self.ctrl_c_tap_time_interval = mode

        def mark_directories(mode):
            self.mode.mark_directories = mode

        def completer_delims(delims):
            self.mode.completer_delims = delims

        def complete_filesystem(delims):
            self.mode.complete_filesystem = delims.lower()

        def enable_ipython_paste_for_paths(boolean):
            self.mode.enable_ipython_paste_for_paths = boolean

        def debug_output(on, filename="pyreadline_debug_log.txt"): #Not implemented yet
            if on in ["on", "on_nologfile"]:
                self.debug=True

            if on == "on":
                logger.start_file_log(filename)
                logger.start_socket_log()
                logger.log("STARTING LOG")
            elif on == "on_nologfile":
                logger.start_socket_log()
                logger.log("STARTING LOG")
            else:
                logger.log("STOPING LOG")
                logger.stop_file_log()
                logger.stop_socket_log()

        _color_trtable={"black":0,      "darkred":4,  "darkgreen":2,
                        "darkyellow":6, "darkblue":1, "darkmagenta":5,
                        "darkcyan":3,   "gray":7,     "red":4+8,
                        "green":2+8,    "yellow":6+8, "blue":1+8,
                        "magenta":5+8,  "cyan":3+8,   "white":7+8}

        def set_prompt_color(color):
            self.prompt_color = self._color_trtable.get(color.lower(),7)

        def set_input_color(color):
            self.command_color=self._color_trtable.get(color.lower(),7)

        loc = {"branch":release.branch,
               "version":release.version,
               "mode":mode,
               "modes":modes,
               "set_mode":setmode,
               "bind_key":bind_key,
               "disable_readline":disable_readline,
               "bind_exit_key":bind_exit_key,
               "un_bind_key":un_bind_key,
               "un_bind_exit_key":un_bind_exit_key,
               "bell_style":setbellstyle,
               "mark_directories":mark_directories,
               "show_all_if_ambiguous":show_all_if_ambiguous,
               "completer_delims":completer_delims,
               "complete_filesystem":complete_filesystem,
               "debug_output":debug_output,
               "history_filename":sethistoryfilename,
               "history_length":sethistorylength,
               "set_prompt_color":set_prompt_color,
               "set_input_color":set_input_color,
               "allow_ctrl_c":allow_ctrl_c,
               "ctrl_c_tap_time_interval":ctrl_c_tap_time_interval,
               "kill_ring_to_clipboard":setkill_ring_to_clipboard,
               "enable_ipython_paste_for_paths":enable_ipython_paste_for_paths,
              }
        if os.path.isfile(inputrcpath):
            try:
                execfile(inputrcpath, loc, loc)
            except Exception as x:
                raise
                import traceback
                print("Error reading .pyinputrc", file=sys.stderr)
                filepath, lineno = traceback.extract_tb(sys.exc_traceback)[1][:2]
                print("Line: %s in file %s"%(lineno, filepath), file=sys.stderr)
                print(x, file=sys.stderr)
                raise ReadlineError("Error reading .pyinputrc")



class Readline(BaseReadline):
    """Baseclass for readline based on a console
    """
    def __init__(self):
        BaseReadline.__init__(self)
        self.console = console.Console()
        self.selection_color = self.console.saveattr<<4
        self.command_color = None
        self.prompt_color = None
        self.size = self.console.size()

        # variables you can control with parse_and_bind

#  To export as readline interface


##  Internal functions

    def _bell(self):
        '''ring the bell if requested.'''
        if self.bell_style == 'none':
            pass
        elif self.bell_style == 'visible':
            raise NotImplementedError("Bellstyle visible is not implemented yet.")
        elif self.bell_style == 'audible':
            self.console.bell()
        else:
            raise ReadlineError("Bellstyle %s unknown."%self.bell_style)

    def _clear_after(self):
        c = self.console
        x, y = c.pos()
        w, h = c.size()
        c.rectangle((x, y, w+1, y+1))
        c.rectangle((0, y+1, w, min(y+3,h)))

    def _set_cursor(self):
        c = self.console
        xc, yc = self.prompt_end_pos
        w, h = c.size()
        xc += self.mode.l_buffer.visible_line_width()
        while(xc >= w):
            xc -= w
            yc += 1
        c.pos(xc, yc)

    def _print_prompt(self):
        c = self.console
        x, y = c.pos()

        n = c.write_scrolling(self.prompt, self.prompt_color)
        self.prompt_begin_pos = (x, y - n)
        self.prompt_end_pos = c.pos()
        self.size = c.size()

    def _update_prompt_pos(self, n):
        if n != 0:
            bx, by = self.prompt_begin_pos
            ex, ey = self.prompt_end_pos
            self.prompt_begin_pos = (bx, by - n)
            self.prompt_end_pos = (ex, ey - n)

    def _update_line(self):
        c = self.console
        l_buffer = self.mode.l_buffer
        c.cursor(0)         #Hide cursor avoiding flicking
        c.pos(*self.prompt_begin_pos)
        self._print_prompt()
        ltext = l_buffer.quoted_text()
        if l_buffer.enable_selection and (l_buffer.selection_mark >= 0):
            start = len(l_buffer[:l_buffer.selection_mark].quoted_text())
            stop  = len(l_buffer[:l_buffer.point].quoted_text())
            if start > stop:
                stop,start = start,stop
            n = c.write_scrolling(ltext[:start], self.command_color)
            n = c.write_scrolling(ltext[start:stop], self.selection_color)
            n = c.write_scrolling(ltext[stop:], self.command_color)
        else:
            n = c.write_scrolling(ltext, self.command_color)

        x, y = c.pos()       #Preserve one line for Asian IME(Input Method Editor) statusbar
        w, h = c.size()
        if (y >= h - 1) or (n > 0):
            c.scroll_window(-1)
            c.scroll((0, 0, w, h), 0, -1)
            n += 1

        self._update_prompt_pos(n)
        if hasattr(c, "clear_to_end_of_window"): #Work around function for ironpython due
            c.clear_to_end_of_window()          #to System.Console's lack of FillFunction
        else:
            self._clear_after()

        #Show cursor, set size vi mode changes size in insert/overwrite mode
        c.cursor(1, size=self.mode.cursor_size)
        self._set_cursor()


    def callback_read_char(self):
        #Override base to get automatic newline
        '''Reads a character and informs the readline callback interface when a line is received'''
        if self.keyboard_poll():
            line = self.get_line_buffer() + '\n'
            self.console.write("\r\n")
            # however there is another newline added by
            # self.mode.readline_setup(prompt) which is called by callback_handler_install
            # this differs from GNU readline
            self.add_history(self.mode.l_buffer)
            # TADA:
            self.callback(line)


    def event_available(self):
        return self.console.peek() or (len(self.paste_line_buffer) > 0)


    def _readline_from_keyboard(self):
        while 1:
            if self._readline_from_keyboard_poll():
                break

    def _readline_from_keyboard_poll(self):
        pastebuffer = self.mode.paste_line_buffer
        if len(pastebuffer) > 0:
            #paste first line in multiline paste buffer
            self.l_buffer = lineobj.ReadLineTextBuffer(pastebuffer[0])
            self._update_line()
            self.mode.paste_line_buffer = pastebuffer[1:]
            return True

        c = self.console
        def nop(e):
            pass
        try:
            event = c.getkeypress()
        except KeyboardInterrupt:
            event = self.handle_ctrl_c()
        try:
            result = self.mode.process_keyevent(event.keyinfo)
        except EOFError:
            logger.stop_logging()
            raise
        self._update_line()
        return result

    def readline_setup(self, prompt=''):
        BaseReadline.readline_setup(self, prompt)
        self._print_prompt()
        self._update_line()

    def readline(self, prompt=''):
        self.readline_setup(prompt)
        self.ctrl_c_timeout = time.time()
        self._readline_from_keyboard()
        self.console.write('\r\n')
        log('returning(%s)' % self.get_line_buffer())
        return self.get_line_buffer() + '\n'

    def handle_ctrl_c(self):
        from pyreadline.keysyms.common import KeyPress
        from pyreadline.console.event import Event
        log("KBDIRQ")
        event = Event(0,0)
        event.char = "c"
        event.keyinfo = KeyPress("c", shift=False, control=True,
                                 meta=False, keyname=None)
        if self.allow_ctrl_c:
            now = time.time()
            if (now - self.ctrl_c_timeout) < self.ctrl_c_tap_time_interval:
                log("Raise KeyboardInterrupt")
                raise KeyboardInterrupt
            else:
                self.ctrl_c_timeout = now
        else:
            raise KeyboardInterrupt
        return event

    def redisplay(self):
        self._update_line()
