import curses
import sys
import threading
from time import sleep

from async import ClientConnection
from chat_window import ChatWindow
from debug_window import DebugWindow
from info_bar import InfoBar
from command_bar import CommandBar

class CLI:
    """
    Command line interface based on the Curses library used for the chat
    application. This chat application is a practical assignment for the
    University of Amsterdam's course Telematica 2011.
    """

    def __init__(self):
        # Initialse the curses library
        self.stdscr = curses.initscr()

        # start_color() initializes eight basic colors (black, red, green,
        # yellow, blue, magenta, cyan, and white), and two global variables in
        # the curses module, COLORS and COLOR_PAIRS, containing the maximum
        # number of colors and color-pairs the terminal can support. It also
        # restores the colors on the terminal to the values they had when the
        # terminal was just turned on.
        self.color_count = curses.start_color()
        curses.use_default_colors()

        # Usually curses applications turn off automatic echoing of keys to the
        # screen, in order to be able to read keys and only display them under
        # certain circumstances.
        curses.noecho()

        # Applications will also commonly need to react to keys instantly,
        # without requiring the Enter key to be pressed; this is called cbreak
        # mode, as opposed to the usual buffered input mode.
        curses.cbreak()

        # Terminals usually return special keys, such as the cursor keys or
        # navigation keys such as Page Up and Home, as a multibyte escape
        # sequence. While you could write your application to expect such
        # sequences and process them accordingly, curses can do it for you,
        # returning a special value such as curses.KEY_LEFT. To get curses to do
        # the job, you'll have to enable keypad mode.
        self.stdscr.keypad(1)

        # After setting nodelay(1), getch() for the window becomes non-blocking
        # and returns curses.ERR (a value of -1) when no input is ready.
        self.stdscr.nodelay(1)

        # Initialise color scheme.
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)

        # No default connection
        self.connection = None

        # Initialise command handler
        self.init_command_handler()

        # Window composition.
        self.init_windows()

        # Enter the user input handler loop.
        self.init_input_handler()

    def init_windows(self):
        """
        Initialise the window of this command line interface. The user interface
        of this chat application consists of a main chat window, a debug window,
        an info bar and at the bottom a command bar. The chat window does also
        view the help screen, when the help screen is requested by the user.
        """

        self.max_y, self.max_x = self.stdscr.getmaxyx()
        self.stdscr.refresh()

        # Initialise chat window.
        self.chat_window = ChatWindow(self, 0, 0, self.max_y - 8, self.max_x)

        # Debug bar between chat window and info bar
        self.debug_window = DebugWindow(self, self.max_y - 7, 0, 5, self.max_x)

        # Info bar between debug window and command bar
        self.info_bar = InfoBar(self, self.max_y - 2, 0, self.max_x)

        # Command bar at the bottom of the screen.
        self.command_bar = CommandBar(self, self.max_y - 1, 0, self.max_x)

        # set cursor position to bottom of screen.
        self.stdscr.move(self.max_y - 1, 0)
        self.stdscr.refresh()

    def init_input_handler(self):
        # KeyboardInterrupt is raised when an user presses ^C. This
        # try/except block will catch the exception and shutdown the
        # application gracefully.
        try:
            while True:
                c = self.stdscr.getch()
                if c != curses.ERR and self.command_bar.handle_input(c):
                    break
                # wait 1 milliseconds
                sleep(.001)
        except KeyboardInterrupt:
            self.execute('quit')

    def init_command_handler(self):

        def close(main):
            # If the help screen is currently displayed, hide it.
            if main.chat_window.displayed_help:
                main.chat_window.displayed_help = False
                main.chat_window.redraw()
            else:
                # Stop user input handler (which will exit the application).
                main.execute('quit')

        def connect(main, host, port=16897):

            if not isinstance(port, int):
                port = int(port)

            # Disconnect an active connection.
            if main.connection:
                main.execute('disconnect')

            main.display_info('Connecting to %s:%d...' % (host, port))

            # Initialise the connection and bind event handlers.
            main.connection = ClientConnection()
            main.connection.main = main
            main.connection.event_list['debug'] = main.debug_event
            main.connection.event_list['connect'] = main.connect_event
            main.connection.event_list['info'] = main.info_event
            main.connection.event_list['message'] = main.message_event
            main.connection.event_list['notify'] = main.notify_event
            main.connection.event_list['authenticated'] = \
                    main.authenticated_event
            main.connection.connect((host, port))

            # The chat connection is ran in a separate thread.
            self.connection_thread = threading.Thread()
            self.connection_thread.daemon = True
            self.connection_thread.run = main.connection.init_loop
            self.connection_thread.start()

        def disconnect(main):
            """
            Disconnect from chat server. When there is no active connection,
            this operating will only set the 'Offline' message in the
            information bar. Otherwise, the active connection is set to None
            and the 'Offline' message is set to the info bar.
            """

            self.connection.close()
            del self.connection_thread
            self.connection = None
            self.display_info('Offline. Type "/connect HOST" to connect' \
                    + ' to another chat server.')
            self.info_bar.prefix('')
            self.refresh()

        def help(main):
            if main.chat_window.displayed_help:
                return

            main.chat_window.displayed_help = True

            main.chat_window.clear()
            help_text = """Help screen

All commands listed below should be preceded by a slash:

  Command     Description

  close       Close visible chat window (or this help screen).

  connect     Connect to a chat server using HOSTNAME and optionally a PORT.
              If no PORT is specified, the default port 16897 is used.

              Synopsis: /connect HOSTNAME [PORT]

              For example, "/connect ow150" will connect to the provided test
              server of this assignment (when you're using the UvA network).

  disconnect  Disconnect from the currently connected chat server.

  exec        Execute python code. For example. to list all variables and
              function of the CLI, type: "/exec print dir(main)".

  help        This help page.

  names       Retrieve and display a list of usernames currently logged in on
              the chat server.

  quit        Quit this chat application (shortcut: ^c).

  raw         Send a raw command to the server. This is useful during debugging
              the client and/or server since you do not have to restart the
              application to send an unimplemented message to the server/client.

  user        Change your username to USER.
"""

            for i, line in enumerate(help_text.split('\n')):
                main.chat_window.window.addstr(i, 0, line)
            main.chat_window.window.refresh()

        def quit(main):
            # Reverse the curses-friendly terminal settings.
            curses.nocbreak();
            self.stdscr.keypad(0);
            curses.echo()

            # Restore the terminal to its original operating mode.
            curses.endwin()

            # Disconnect the connection
            if hasattr(self, 'connection_thread'):
                del self.connection_thread
            self.connection = None
            sys.exit(0)

        def raw(main, cmd):
            # Skip "/raw " and send the remaining command to the server.
            if main.connection:
                main.connection.send_raw(cmd[5:])

        def names(main):
            if main.connection:
                main.connection.retrieve_users()

        def user(main, name):
            if main.connection:
                main.connection.username(name)

        def _exec(main, cmd):
            # Skip the leading "/exec " of the command
            exec cmd[6:]

        # List of builtin command
        self.cmd_list = {
            'close': close,
            'connect': connect,
            'disconnect': disconnect,
            'exec': _exec,
            'help': help,
            'names': names,
            'quit': quit,
            'raw': raw,
            'user': user,
        }

    def say(self, msg):
        self.connection.send_raw('SAY %s' % msg)

    def refresh(self):
        self.chat_window.refresh()
        self.debug_window.refresh()
        self.info_bar.refresh()
        self.command_bar.refresh()
        self.command_bar.focus()
        self.stdscr.refresh()

    def authenticated_event(self, conn, username):
        self.info_bar.prefix('[%s @ %s]' % (username, conn.host))
        self.refresh()

    def connect_event(self, conn):
        self.info_bar.prefix('[%s]' % conn.host)
        self.display_info('')
        self.refresh()

    def debug_event(self, conn, msg):
        self.debug_window.append(msg)
        self.refresh()

    def message_event(self, conn, user, msg):
        self.chat_window.append('%s: %s' % (user, msg))
        self.refresh()

    def info_event(self, conn, msg):
        self.chat_window.append('i %s' % msg)
        self.refresh()

    def notify_event(self, conn, msg):
        self.chat_window.append('~ %s' % msg)
        self.refresh()

    def display_info(self, msg):
        """
        Display a message on the information bar (info_bar). This will erase the
        existing message on the info bar.
        """

        self.info_bar.display(msg)
        self.refresh()

    def execute(self, cmd, args=[], raw_command=''):

        if not raw_command:
            raw_command = cmd

        # Select command from command list.
        try:
            cmd_fn = self.cmd_list[cmd]
        except KeyError:
            self.display_info('Unknown command "/%s".' % cmd)
            return

        # Call command handler.
        try:
            if cmd == 'exec' or cmd == 'raw':
                cmd_fn(self, raw_command)
            else:
                cmd_fn(self, *args)
        except Exception, e:
            self.display_info(str(e))
            return

    def __del__(self):
        import curses

        # Reverse the curses-friendly terminal settings.
        curses.nocbreak();
        self.stdscr.keypad(0);
        curses.echo()

        # Restore the terminal to its original operating mode.
        curses.endwin()

