#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# ***** BEGIN LICENSE BLOCK *****
# Copyright (C) 2012-2014, Hayaki Saito 
# 
# Permission is hereby granted, free of charge, to any person obtaining a 
# copy of this software and associated documentation files (the "Software"), 
# to deal in the Software without restriction, including without limitation 
# the rights to use, copy, modify, merge, publish, distribute, sublicense, 
# and/or sell copies of the Software, and to permit persons to whom the 
# Software is furnished to do so, subject to the following conditions: 
# 
# The above copyright notice and this permission notice shall be included in 
# all copies or substantial portions of the Software. 
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL 
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
# DEALINGS IN THE SOFTWARE. 
# 
# ***** END LICENSE BLOCK *****

import sys
import os
import termios
import pty
import signal
import fcntl
import struct
import select
import errno
import codecs
import threading
import logging
# terminal filter framework interface
from interface import EventObserver, EventDispatcher, OutputStream, PTY
#from exception import NotHandleException, ParseException

_BUFFER_SIZE = 8192
_ESC_TIMEOUT = 0.5  # sec

try:
    from ctff import DefaultScanner
except:
    from scanner import DefaultScanner

try:
    from ctff import DefaultParser as OutputParser
except:
    from parser import DefaultParser as OutputParser
 
from parser import DefaultParser

###############################################################################
#
# Handler implementation
#
class DefaultHandler(EventObserver):
    ''' default handler, pass through all ESC/CSI/string seqnceses '''
    def __init__(self):
        pass

# EventObserver
    def handle_start(self, context):
        pass

    def handle_end(self, context):
        pass

    def handle_esc(self, context, intermediate, final):
        return False

    def handle_csi(self, context, parameter, intermediate, final):
        return False

    def handle_ss2(self, context, final):
        return False

    def handle_ss3(self, context, final):
        return False

    def handle_control_string(self, context, prefix, value):
        return False

    def handle_char(self, context, c):
        return False

    def handle_invalid(self, context, seq):
        return False

    def handle_draw(self, context):
        pass

    def handle_resize(self, context, row, col):
        pass


###############################################################################
#
# Multiplexer implementation
#
class FilterMultiplexer(EventObserver):

    def __init__(self, lhs, rhs):
        self.__lhs = lhs
        self.__rhs = rhs

    def get_lhs(self):
        return self.__lhs

    def get_rhs(self):
        return self.__rhs

    def handle_start(self, context):
        handled_lhs = self.__lhs.handle_start(context)
        handled_rhs = self.__rhs.handle_start(context)
        return handled_lhs and handled_rhs

    def handle_end(self, context):
        handled_lhs = self.__lhs.handle_end(context)
        handled_rhs = self.__rhs.handle_end(context)
        return handled_lhs and handled_rhs

    def handle_flush(self, context):
        handled_lhs = self.__lhs.handle_flush(context)
        handled_rhs = self.__rhs.handle_flush(context)
        return handled_lhs and handled_rhs

    def handle_csi(self, context, params, intermediate, final):
        handled_lhs = self.__lhs.handle_csi(context, params,
                                            intermediate, final)
        handled_rhs = self.__rhs.handle_csi(context, params,
                                            intermediate, final)
        return handled_lhs and handled_rhs

    def handle_esc(self, context, intermediate, final):
        handled_lhs = self.__lhs.handle_esc(context, intermediate, final)
        handled_rhs = self.__rhs.handle_esc(context, intermediate, final)
        return handled_lhs and handled_rhs

    def handle_control_string(self, context, prefix, value):
        handled_lhs = self.__lhs.handle_control_string(context, prefix, value)
        handled_rhs = self.__rhs.handle_control_string(context, prefix, value)
        return handled_lhs and handled_rhs

    def handle_char(self, context, c):
        handled_lhs = self.__lhs.handle_char(context, c)
        handled_rhs = self.__rhs.handle_char(context, c)
        return handled_lhs and handled_rhs

    def handle_invalid(self, context, seq):
        handled_lhs = self.__lhs.handle_invalid(context, seq)
        handled_rhs = self.__rhs.handle_invalid(context, seq)
        return handled_lhs and handled_rhs

    def handle_draw(self, context):
        handled_lhs = self.__lhs.handle_draw(context)
        handled_rhs = self.__rhs.handle_draw(context)
        return handled_lhs and handled_rhs

    def handle_resize(self, context, row, col):
        handled_lhs = self.__lhs.handle_resize(context, row, col)
        handled_rhs = self.__rhs.handle_resize(context, row, col)
        return handled_lhs and handled_rhs


###############################################################################
#
# Dispatcher implementation
#
class ParseContext(OutputStream, EventDispatcher):

    def __init__(self,
                 output,
                 termenc='UTF-8',
                 scanner=DefaultScanner(),
                 handler=DefaultHandler(),
                 buffering=False):
        self.__termenc = termenc
        self.__scanner = scanner
        self.__handler = handler
        self._c1 = 0

        if buffering:
            try:
                from cStringIO import StringIO
                self._output = codecs.getwriter(termenc)(StringIO())
            except ImportError:
                try:
                    from StringIO import StringIO
                    self._output = codecs.getwriter(termenc)(StringIO())
                except ImportError:
                    from io import StringIO
                    self._output = codecs.getwriter(termenc)(StringIO())
        else:
            self._output = codecs.getwriter(termenc)(output)
        self._target_output = output
        self._buffering = buffering

    def __iter__(self):
        return self.__scanner.__iter__()

    def assign(self, data):
        self.__scanner.assign(data, self.__termenc)
        if self._buffering:
            self._output.truncate(0)

    def sethandler(self, handler):
        self.__handler = handler

    def putu(self, data):
        self._output.write(data)

    def puts(self, data):
        self._target_output.write(data)

    def put(self, c):
        if c < 0x80:
            self._output.write(chr(c))
        elif c < 0xd800:
            self._output.write(unichr(c))
        elif c < 0xdc00:
            self._c1 = c
        elif c < 0xe000:
            self._output.write(unichr(self._c1) + unichr(c))
        elif c < 0x10000:
            self._output.write(unichr(c))
        else:  # c > 0x10000
            c -= 0x10000
            c1 = (c >> 10) + 0xd800
            c2 = (c & 0x3ff) + 0xdc00
            self._output.write(unichr(c1) + unichr(c2))

    # obsoluted!!
    def writestring(self, data):
        try:
            self._target_output.write(data)
        except Exception:
            self._output.write(data)

# OutputStream
    # obsoluted!!
    def write(self, c):
        self.put(c)

    def flush(self):
        if self._buffering:
            self._target_output.write(self._output)
        try:
            self._target_output.flush()
        except IOError:
            pass

# EventDispatcher
    def dispatch_esc(self, intermediate, final):
        if not self.__handler.handle_esc(self, intermediate, final):
            self.put(0x1b)  # ESC
            for c in intermediate:
                self.put(c)
            self.put(final)

    def dispatch_csi(self, parameter, intermediate, final):
        if not self.__handler.handle_csi(self, parameter, intermediate, final):
            self.put(0x1b)  # ESC
            self.put(0x5b)  # [
            for c in parameter:
                self.put(c)
            for c in intermediate:
                self.put(c)
            self.put(final)

    def dispatch_ss2(self, final):
        if not self.__handler.handle_ss2(self, final):
            self.put(0x1b)  # ESC
            self.put(0x4e)  # N
            self.put(final)

    def dispatch_ss3(self, final):
        if not self.__handler.handle_ss3(self, final):
            self.put(0x1b)  # ESC
            self.put(0x4f)  # O
            self.put(final)

    def dispatch_control_string(self, prefix, value):
        if not self.__handler.handle_control_string(self, prefix, value):
            self.put(0x1b)  # ESC
            self.put(prefix)
            for c in value:
                self.put(c)
            self.put(0x1b)  # ESC
            self.put(0x5c)  # \

    def dispatch_char(self, c):
        if not self.__handler.handle_char(self, c):
            self.put(c)

    def dispatch_invalid(self, seq):
        if not self.__handler.handle_invalid(self, seq):
            for c in seq:
                self.put(c)


###############################################################################
#
# DefaultPTY
#
class DefaultPTY(PTY):

    def __init__(self, term, lang, command, stdin):
        self._stdin_fileno = stdin.fileno()
        backup = termios.tcgetattr(self._stdin_fileno)
        self._backup_termios = backup
        pid, master = pty.fork()
        if not pid:
            os.environ['TERM'] = term
            os.environ['LANG'] = lang
            os.execlp('/bin/sh',
                      '/bin/sh', '-c',
                      'exec %s' % command)

        self.__setupterm(self._stdin_fileno)
        self.pid = pid
        self._master = master

    def close(self):
        #self.restore_term()
        try:
            os.close(self._master)
        except OSError, e:
            logging.exception(e)
            logging.info("DefaultPTY.close: master=%d" % self._master)

    def restore_term(self):
        termios.tcsetattr(self._stdin_fileno,
                          termios.TCSANOW,
                          self._backup_termios)

    def __setupterm(self, fd):
        term = termios.tcgetattr(fd)

        ## c_iflag
        #IUTF8 = 16384
        term[0] &= ~(termios.IGNBRK
                     | termios.BRKINT
                     | termios.PARMRK
                     | termios.ISTRIP
                     | termios.INLCR
                     | termios.IGNCR
                     | termios.ICRNL
                     | termios.IXON)

        term[1] &= ~(termios.OPOST
                     | termios.ONLCR)

        # c_cflag
        c_cflag = term[2]
        c_cflag &= ~(termios.CSIZE | termios.PARENB)
        c_cflag |= termios.CS8
        term[2] = c_cflag

        ## c_lflag
        c_lflag = term[3]
        c_lflag &= ~(termios.ECHO
                     | termios.ECHONL
                     | termios.ICANON
                     | termios.ISIG
                     | termios.IEXTEN)
        term[3] = c_lflag

        # c_cc
        # this PTY is jast a filter, so it must not fire signals
        vdisable = os.fpathconf(self._stdin_fileno, 'PC_VDISABLE')
        VDSUSP = 11
        c_cc = term[6]
        c_cc[termios.VEOF] = vdisable  # Ctrl-D
        c_cc[termios.VINTR] = vdisable  # Ctrl-C
        c_cc[termios.VREPRINT] = vdisable  # Ctrl-R
        c_cc[termios.VSTART] = vdisable  # Ctrl-Q
        c_cc[termios.VSTOP] = vdisable  # Ctrl-S
        c_cc[termios.VLNEXT] = vdisable  # Ctrl-V
        c_cc[termios.VWERASE] = vdisable  # Ctrl-W
        c_cc[termios.VKILL] = vdisable  # Ctrl-X
        c_cc[termios.VSUSP] = vdisable  # Ctrl-Z
        c_cc[termios.VQUIT] = vdisable  # Ctrl-\
        c_cc[VDSUSP] = vdisable  # Ctrl-Y

        termios.tcsetattr(fd, termios.TCSANOW, term)

    def __resize_impl(self, winsize):
        fcntl.ioctl(self._master, termios.TIOCSWINSZ, winsize)
        # notify Application process that terminal size has been changed.
        os.kill(self.pid, signal.SIGWINCH)

    def fitsize(self):
        winsize = fcntl.ioctl(self._stdin_fileno, termios.TIOCGWINSZ, 'hhhh')
        height, width = struct.unpack('hh', winsize)
        self.__resize_impl(winsize)
        return height, width

    def resize(self, height, width):
        winsize = struct.pack('HHHH', height, width, 0, 0)
        self.__resize_impl(winsize)
        return height, width

    def fileno(self):
        return self._master

    def stdin_fileno(self):
        return self._stdin_fileno

    def read(self):
        return os.read(self._master, _BUFFER_SIZE)

    def write(self, data):
        os.write(self._master, data)

    def flush(self):
        pass
        #os.fsync(self._master)

    def xoff(self):
        #fcntl.ioctl(self._master, termios.TIOCSTOP, 0)
        termios.tcflow(self._master, termios.TCOOFF)

    def xon(self):
        #fcntl.ioctl(self._master, termios.TIOCSTART, 0)
        termios.tcflow(self._master, termios.TCOON)

class MockParseContext(ParseContext):

    def __init__(self):
        from StringIO import StringIO
        output = StringIO()
        ParseContext.__init__(self, output)


###############################################################################
#
# Process
#
class Process:

    _tty = None
    _esc_timer = None

    def __init__(self, tty):
        self._tty = tty

    def start(self, termenc,
              inputhandler, outputhandler,
              inputparser, outputparser,
              inputscanner, outputscanner,
              buffering=False,
              stdout=sys.stdout):

        inputcontext = ParseContext(output=self._tty,
                                    termenc=termenc,
                                    scanner=inputscanner,
                                    handler=inputhandler,
                                    buffering=buffering)
        outputcontext = ParseContext(output=stdout,
                                     termenc=termenc,
                                     scanner=outputscanner,
                                     handler=outputhandler,
                                     buffering=buffering)

        inputparser.init(inputcontext)
        outputparser.init(outputcontext)

        self._inputhandler = inputhandler
        self._outputhandler = outputhandler
        self._inputparser = inputparser
        self._outputparser = outputparser
        self._inputcontext = inputcontext
        self._outputcontext = outputcontext
        inputhandler.handle_start(outputcontext)
        outputhandler.handle_start(outputcontext)

    def getpid(self):
        return self._tty.pid

    def is_alive(self):
        return self._tty is not None 

    def fileno(self):
        return self._tty.fileno()

    def stdin_fileno(self):
        return self._tty.stdin_fileno()

    def read(self):
        return self._tty.read()

    def write(self, data):
        self._tty.write(data)

    def close(self):
        if self._tty is not None:
            self._tty.close()
            self._tty = None

    def end(self):
        self._inputhandler.handle_end(self._outputcontext)
        self._outputhandler.handle_end(self._outputcontext)

    def resize(self, row, col):
        self._tty.resize(row, col)

    def fitsize(self):
        return self._tty.fitsize()

    def on_write(self, data):
        self._inputparser.parse(data)

    def process_start(self):
        self._inputhandler.handle_start(self._inputcontext)
        self._outputhandler.handle_start(self._outputcontext)
        self._inputhandler.handle_draw(self._outputcontext)
        self._outputhandler.handle_draw(self._outputcontext)
        #self._inputcontext.flush()
        self._outputcontext.flush()

    def process_end(self):
        self._inputhandler.handle_end(self._inputcontext)
        self._outputhandler.handle_end(self._outputcontext)

    def process_resize(self, row, col):
        try:
            self._inputhandler.handle_resize(self._inputcontext, row, col)
            self._outputhandler.handle_resize(self._outputcontext, row, col)
        finally:
            self._resized = False

    def process_input(self, data):
        if not self._esc_timer is None:
            self._esc_timer.cancel()
            self._esc_timer = None

        self._inputparser.parse(data)

        # set ESC timer
        if not self._inputparser.state_is_esc():
            self._inputhandler.handle_draw(self._outputcontext)
            self._outputhandler.handle_draw(self._outputcontext)
            #self._inputcontext.flush()
            self._outputcontext.flush()
        else:
            def dispatch_esc():
                self._inputparser.flush()
                self._inputparser.reset()
                self._inputhandler.handle_draw(self._outputcontext)
                self._outputhandler.handle_draw(self._outputcontext)
                #self._inputcontext.flush()
                self._outputcontext.flush()
            self._esc_timer = threading.Timer(_ESC_TIMEOUT, dispatch_esc)
            self._esc_timer.start()

    def process_output(self, data):
        self._outputparser.parse(data)
        self._inputhandler.handle_draw(self._outputcontext)
        self._outputhandler.handle_draw(self._outputcontext)
        #self._inputcontext.flush()
        self._outputcontext.flush()

    def on_read(self, data):
        self._outputparser.parse(data)

    def drain(self):
        self._inputparser.reset()
        self._inputcontext.assign('')


###############################################################################
#
# Session
#
class Session:

    def __init__(self, tty):

        self._alive = True
        self._mainprocess = Process(tty)
        self._input_target = self._mainprocess
        stdin_fileno = self._mainprocess.stdin_fileno()
        self._rfds = [stdin_fileno]
        self._xfds = [stdin_fileno]
        self._resized = False
        self._process_map = {}

    def add_subtty(self, term, lang,
                   command, row, col,
                   termenc,
                   inputhandler=DefaultHandler(),
                   outputhandler=DefaultHandler()):

        tty = DefaultPTY(term, lang, command, sys.stdin)
        tty.resize(row, col)
        process = Process(tty)

        self._init_process(process,
                           termenc,
                           inputhandler, outputhandler,
                           DefaultParser(), DefaultParser(),
                           DefaultScanner(), DefaultScanner(),
                           buffering=False)
        self.focus_process(process)
        return process

    def getactiveprocess(self):
        return self._input_target

    def process_is_active(self, process):
        return self._input_target == process

    def focus_process(self, process):
        if process.is_alive():
            logging.info("Switching focus: fileno=%d" % process.fileno())
            self._input_target.drain()
            self._input_target = process

    def blur_process(self):
        process = self._mainprocess
        if process.is_alive():
            logging.info("Switching focus: fileno=%d (main process)" % process.fileno())
            self._input_target.drain()
            self._input_target = process

    def destruct_process(self, process):
        fd = process.fileno()
        if fd in self._rfds:
            self._rfds.remove(fd)
        if fd in self._xfds:
            self._xfds.remove(fd)
        process.end()
        process.close()
        del self._process_map[fd]

        self.focus_process(self._mainprocess)
        self._mainprocess.process_output("")

    def drive(self):

        def onresize(no, frame):
            ''' handle resize '''
            self._resized = True

        try:
            signal.signal(signal.SIGWINCH, onresize)
        except ValueError:
            pass

        stdin_fileno = self._mainprocess.stdin_fileno()
        try:
            while self._alive:
                try:
                    rfd, wfd, xfd = select.select(self._rfds, [], self._xfds, 0.6)
                    if xfd:
                        for fd in xfd:
                            if fd in self._process_map:
                                process = self._process_map[fd]
                                self.destruct_process(process)
                                continue
                    if self._resized:
                        self._resized = False
                        row, col = self._mainprocess.fitsize()
                        self._mainprocess.process_resize(row, col)
                    if rfd:
                        for fd in rfd:
                            if fd == stdin_fileno:
                                data = os.read(stdin_fileno, _BUFFER_SIZE)
                                if self._input_target.is_alive():
                                    target_fd = self._input_target.fileno()
                                    process = self._process_map[target_fd]
                                    process.process_input(data)
                                    self._mainprocess.process_input("")
                            elif self._process_map:
                                process_map = self._process_map
                                if fd in process_map:
                                    process = process_map[fd]
                                    if self._input_target.is_alive():
                                        if fd == self._input_target.fileno():
                                            data = process.read()
                                            process.on_read(data)
                                            self._mainprocess.process_output("")
                    else:
                        pass
                except select.error, e:
                    no, msg = e
                    if no == errno.EINTR:
                        # The call was interrupted by a signal
                        self._resized = True
                    elif no == errno.EBADF:
                        for fd in self._process_map:
                            process = self._process_map[fd]
                            self.destruct_process(process)
                    else:
                        raise e
                except OSError, e:
                    no, msg = e
                    if no == errno.EINTR:
                        # The call was interrupted by a signal
                        self._resized = True
                    else:
                        raise e
        except OSError, e:
            no, msg = e
            if no == errno.EIO:
                return
            elif no == errno.EBADF:
                return
            else:
                raise e
        finally:
            try:
                self._mainprocess.process_end()
            finally:
                for fd in self._process_map:
                    process = self._process_map[fd]
                    process.end()
                    process.close()

    def start(self,
              termenc,
              stdin=sys.stdin,
              stdout=sys.stdout,
              inputscanner=DefaultScanner(),
              inputparser=DefaultParser(),
              inputhandler=DefaultHandler(),
              outputscanner=DefaultScanner(),
              outputparser=OutputParser(),
              outputhandler=DefaultHandler(),
              buffering=False):

        mainprocess = self._mainprocess

        self._init_process(mainprocess,
                           termenc,
                           inputhandler, outputhandler,
                           inputparser, outputparser,
                           inputscanner, outputscanner,
                           buffering)
        self.focus_process(mainprocess)

        self._resized = False

        def onclose(no, frame):
            pid, status = os.wait()
            if not mainprocess.is_alive():
                self._alive = False
            elif pid == mainprocess.getpid():
                self._alive = False
            else:
                self.focus_process(mainprocess)

        signal.signal(signal.SIGCHLD, onclose)

        self.drive()

    def _init_process(self,
                      process,
                      termenc,
                      inputhandler, outputhandler,
                      inputparser, outputparser,
                      inputscanner, outputscanner,
                      buffering):

        fd = process.fileno()
        self._rfds.append(fd)
        self._xfds.append(fd)
        self._process_map[fd] = process

        process.start(termenc,
                      inputhandler,
                      outputhandler,
                      inputparser,
                      outputparser,
                      inputscanner,
                      outputscanner,
                      buffering)


def _test():
    import doctest
    doctest.testmod()

''' main '''
if __name__ == '__main__':
    _test()
