#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# ***** BEGIN LICENSE BLOCK *****
# Copyright (C) 2012  Hayaki Saito <user@zuse.jp>
#
# This program is free software: you can 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
# ***** END LICENSE BLOCK *****


import tff
from mouse import IFocusListener, IMouseListener, MouseDecoder
from interface import IInnerFrame, IInnerFrameListener
from output import Canossa
from screen import Screen

_HITTEST_NONE              = 0
_HITTEST_CLIENTAREA        = 1
_HITTEST_TITLEBAR          = 2
_HITTEST_FRAME_LEFT        = 3
_HITTEST_FRAME_TOP         = 4
_HITTEST_FRAME_RIGHT       = 5
_HITTEST_FRAME_BOTTOM      = 6
_HITTEST_FRAME_TOPLEFT     = 7
_HITTEST_FRAME_TOPRIGHT    = 8
_HITTEST_FRAME_BOTTOMLEFT  = 9
_HITTEST_FRAME_BOTTOMRIGHT = 10
_HITTEST_BUTTON_CLOSE      = 11

_TITLESTYLE_INACTIVE       = '\x1b[30;47m'
_TITLESTYLE_ACTIVE         = '\x1b[30;42m'
_TITLESTYLE_HOVER          = '\x1b[30;46m'
_TITLESTYLE_DRAG           = '\x1b[30;43m'

_DRAGTYPE_NONE             = 0
_DRAGTYPE_TITLEBAR         = 1
_DRAGTYPE_BOTTOMRIGHT      = 2
_DRAGTYPE_BOTTOMLEFT       = 3
_DRAGTYPE_BOTTOM           = 4
_DRAGTYPE_LEFT             = 5
_DRAGTYPE_RIGHT            = 6

_HOVERTYPE_NONE            = 0
_HOVERTYPE_TITLEBAR        = 1
_HOVERTYPE_BOTTOMRIGHT     = 2
_HOVERTYPE_BOTTOMLEFT      = 3
_HOVERTYPE_BOTTOM          = 4
_HOVERTYPE_LEFT            = 5
_HOVERTYPE_RIGHT           = 6


class IFocusListenerImpl(IFocusListener):

    def __init__(self):
        pass

    """ IFocusListener implementation """
    def onfocusin(self):
        self._titlestyle = _TITLESTYLE_ACTIVE

    def onfocusout(self):
        self._titlestyle = _TITLESTYLE_INACTIVE


class IMouseListenerImpl(IMouseListener):

    def __init__(self):
        self._lasthittest = None
        self._dragtype = _DRAGTYPE_NONE
        self._hovertype = _HOVERTYPE_NONE
        self._dragpos = None
        self._titlestyle = _TITLESTYLE_INACTIVE

    """ IMouseListener implementation """

    def onmousedown(self, context, x, y):
        hittest = self._hittest(x, y)
        self._lasthittest = hittest
        if hittest == _HITTEST_NONE:
            self._window.blur()
            self._session.blur_subprocess(self._tty)
            return False
        if not self._window.is_active():
            self._window.focus()
            self._session.focus_subprocess(self._tty)
            self._titlestyle = _TITLESTYLE_ACTIVE
            return True
        if hittest == _HITTEST_CLIENTAREA:
            x -= self.left + self.offset_left
            y -= self.top + self.offset_top
            x += 33
            y += 33
            if x < 0x80 and y < 0x80:
                context.puts(u'\x1b[M%c%c%c' % (0 + 32, x, y))
        return True

    def onmouseup(self, context, x, y):
        hittest = self._hittest(x, y)
        self._lasthittest = hittest
        if hittest == _HITTEST_NONE:
            return False
        if not self._window.is_active():
            return True
        if hittest == _HITTEST_CLIENTAREA:
            x -= self.left + self.offset_left
            y -= self.top + self.offset_top
            x += 33
            y += 33
            if x < 0x80 and y < 0x80:
                context.puts(u'\x1b[M%c%c%c' % (3 + 32, x, y))
        return True

    def onclick(self, context, x, y):
        hittest = self._hittest(x, y)
        self._lasthittest = hittest
        if hittest == _HITTEST_NONE:
            return False
        return True

    def ondoubleclick(self, context, x, y):
        hittest = self._lasthittest
        if hittest == _HITTEST_NONE:
            return False
        return True

    def onmousehover(self, context, x, y):
        hittest = self._hittest(x, y)
        self._lasthittest = hittest
        if hittest == _HITTEST_NONE:
            self._hovertype = _HOVERTYPE_NONE
            return False
        if hittest == _HITTEST_CLIENTAREA:
            if self._window.is_active():
                x -= self.left + self.offset_left
                y -= self.top + self.offset_top
                x += 33
                y += 33
                if x < 0x80 and y < 0x80:
                    context.puts(u"\x1b[M%c%c%c" % (32 + 32, x, y))
                self._titlestyle = _TITLESTYLE_ACTIVE
            self._hovertype = _HOVERTYPE_NONE
        elif hittest == _HITTEST_TITLEBAR:
            self._titlestyle = _TITLESTYLE_HOVER
            self._hovertype = _HOVERTYPE_TITLEBAR
        elif hittest == _HITTEST_FRAME_BOTTOMLEFT:
            self._titlestyle = _TITLESTYLE_HOVER
            self._hovertype = _HOVERTYPE_BOTTOMLEFT
        elif hittest == _HITTEST_FRAME_BOTTOMRIGHT:
            self._titlestyle = _TITLESTYLE_HOVER
            self._hovertype = _HOVERTYPE_BOTTOMRIGHT
        elif hittest == _HITTEST_FRAME_BOTTOM:
            self._titlestyle = _TITLESTYLE_HOVER
            self._hovertype = _HOVERTYPE_BOTTOM
        elif hittest == _HITTEST_FRAME_LEFT:
            self._titlestyle = _TITLESTYLE_HOVER
            self._hovertype = _HOVERTYPE_LEFT
        elif hittest == _HITTEST_FRAME_RIGHT:
            self._titlestyle = _TITLESTYLE_HOVER
            self._hovertype = _HOVERTYPE_RIGHT
        else:
            self._titlestyle = _TITLESTYLE_ACTIVE
            self._hovertype = _HOVERTYPE_NONE
        return True

    """ scroll """
    def onscrolldown(self, context, x, y):
        hittest = self._lasthittest
        self._lasthittest = hittest
        if hittest == _HITTEST_NONE:
            return False
        if not self._window.is_active():
            return True
        if hittest == _HITTEST_CLIENTAREA:
            x -= self.left + self.offset_left
            y -= self.top + self.offset_top
            x += 33
            y += 33
            if x < 0x80 and y < 0x80:
                context.puts(u'\x1b[M%c%c%c' % (64 + 32, x, y))
        return True

    def onscrollup(self, context, x, y):
        hittest = self._lasthittest
        self._lasthittest = hittest
        if hittest == _HITTEST_NONE:
            return False
        if not self._window.is_active():
            return True
        elif hittest == _HITTEST_CLIENTAREA:
            x -= self.left + self.offset_left
            y -= self.top + self.offset_top
            x += 33
            y += 33
            if x < 0x80 and y < 0x80:
                context.puts(u'\x1b[M%c%c%c' % (65 + 32, x, y))
        return True

    """ drag and drop """
    def ondragstart(self, s, x, y):
        hittest = self._lasthittest
        if hittest == _HITTEST_NONE:
            return False
        if not self._window.is_active():
            return True
        if hittest == _HITTEST_TITLEBAR:
            self._dragtype = _DRAGTYPE_TITLEBAR
            self._dragpos = (x, y)
            self._titlestyle = _TITLESTYLE_DRAG
        elif hittest == _HITTEST_FRAME_BOTTOMLEFT:
            self._dragtype = _DRAGTYPE_BOTTOMLEFT
            self._titlestyle = _TITLESTYLE_DRAG
        elif hittest == _HITTEST_FRAME_BOTTOMRIGHT:
            self._dragtype = _DRAGTYPE_BOTTOMRIGHT
            self._titlestyle = _TITLESTYLE_DRAG
        elif hittest == _HITTEST_FRAME_BOTTOM:
            self._dragtype = _DRAGTYPE_BOTTOM
            self._titlestyle = _TITLESTYLE_DRAG
        elif hittest == _HITTEST_FRAME_LEFT:
            self._dragtype = _DRAGTYPE_LEFT
            self._titlestyle = _TITLESTYLE_DRAG
        elif hittest == _HITTEST_FRAME_RIGHT:
            self._dragtype = _DRAGTYPE_RIGHT
            self._titlestyle = _TITLESTYLE_DRAG
        return True

    def ondragend(self, s, x, y):
        hittest = self._lasthittest
        if hittest == _HITTEST_NONE:
            return False
        if self._dragtype == _DRAGTYPE_NONE:
            return True
        if not self._window.is_active():
            return True
        self.left += self.offset_left
        self.top += self.offset_top
        self.offset_left = 0
        self.offset_top = 0
        self._dragtype = _DRAGTYPE_NONE
        self._dragpos = None
        self._titlestyle = _TITLESTYLE_ACTIVE
        self._dragstype = _DRAGTYPE_NONE
        return True

    def ondragmove(self, context, x, y):
        hittest = self._lasthittest
        if hittest == _HITTEST_NONE:
            return False
        if self._dragtype == _DRAGTYPE_NONE:
            return False
        if not self._window.is_active():
            return True
        if self._dragtype == _DRAGTYPE_TITLEBAR:

            origin_x, origin_y = self._dragpos
            offset_x = x - origin_x
            offset_y = y - origin_y

            screen = self._outerscreen
            innerscreen = self.innerscreen

            width = innerscreen.width + 2
            height = innerscreen.height + 2

            if self.left + width + offset_x < 1:
                offset_x = 1 - self.left - width
            elif self.left + offset_x > screen.width - 1:
                offset_x = screen.width - self.left - 1
            if self.top + height + offset_y < 1:
                offset_y = 1 - self.top - height
            elif self.top + offset_y > screen.height - 1:
                offset_y = screen.height - self.top - 1

            self.offset_left = offset_x
            self.offset_top = offset_y

            left = self.left + self.offset_left - 1
            top = self.top + self.offset_top - 1
            width = innerscreen.width + 2
            height = innerscreen.height + 2

            self._window.realloc(left, top, width, height)

        elif self._dragtype == _DRAGTYPE_BOTTOMRIGHT:

            screen = self.innerscreen
            window = self._window

            left = self.left
            top = self.top
            row = max(y - top, 5)
            col = max(x - left, 8)

            screen.resize(row, col)
            self._tty.resize(row, col)

            left -= 1
            top -= 1
            width = col + 2
            height = row + 2

            window.realloc(left, top, width, height)

        elif self._dragtype == _DRAGTYPE_BOTTOMLEFT:

            screen = self.innerscreen
            window = self._window

            left = min(max(x, 0), self.left + screen.width - 10)
            top = self.top
            row = max(y - top, 5)
            col = self.left + screen.width - left + 1

            screen.resize(row, col)
            self._tty.resize(row, col)

            left -= 2
            top -= 1
            width = col + 2
            height = row + 2

            self.left = left + 1

            window.realloc(left, top, width, height)

        elif self._dragtype == _DRAGTYPE_BOTTOM:

            screen = self.innerscreen
            window = self._window

            left = self.left
            top = self.top
            row = max(y - top, 5)
            col = screen.width

            screen.resize(row, col)
            self._tty.resize(row, col)

            left -= 1
            top -= 1
            width = col + 2
            height = row + 2

            window.realloc(left, top, width, height)

        elif self._dragtype == _DRAGTYPE_LEFT:

            screen = self.innerscreen
            window = self._window

            left = min(max(x, 0), self.left + screen.width - 10)
            top = self.top
            row = screen.height
            col = self.left + screen.width - left + 1

            screen.resize(row, col)
            self._tty.resize(row, col)

            left -= 2
            top -= 1
            width = col + 2
            height = row + 2

            self.left = left + 1

            window.realloc(left, top, width, height)


        elif self._dragtype == _DRAGTYPE_RIGHT:

            screen = self.innerscreen
            window = self._window

            left = self.left
            top = self.top
            row = screen.height
            col = max(x - left, 8)

            screen.resize(row, col)
            self._tty.resize(row, col)

            left -= 1
            top -= 1
            width = col + 2
            height = row + 2

            window.realloc(left, top, width, height)


        else:
            hittest = self._hittest(x, y)
            self._lasthittest = hittest
            if self._dragtype == _DRAGTYPE_NONE:
                return False
            elif hittest == _HITTEST_CLIENTAREA:
                x -= self.left + self.offset_left
                y -= self.top + self.offset_top
                x += 33
                y += 33
                if x < 0x80 and y < 0x80:
                    context.puts("\x1b[M%c%c%c" % (32 + 32, x, y))
        return True

    def _get_left(self):
        return self.left + self.offset_left - 1

    def _get_right(self):
        return self.left + self.offset_left + self.innerscreen.width + 1

    def _get_top(self):
        return self.top + self.offset_top - 1

    def _get_bottom(self):
        return self.top + self.offset_top + self.innerscreen.height + 1

    def _hittest(self, x, y):
        screen = self.innerscreen
        left = self._get_left()
        top = self._get_top()
        right = self._get_right()
        bottom = self._get_bottom()
        if x < left:
            return _HITTEST_NONE
        elif x > right - 1:
            return _HITTEST_NONE
        if y < top:
            return _HITTEST_NONE
        elif y > bottom - 1:
            return _HITTEST_NONE
        elif y == top:
            if x >= left and x <= right:
                return _HITTEST_TITLEBAR
        elif y == bottom - 1:
            if x == left:
                return _HITTEST_FRAME_BOTTOMLEFT
            elif x == right - 1:
                return _HITTEST_FRAME_BOTTOMRIGHT
            else:
                return _HITTEST_FRAME_BOTTOM
        elif x == left:
            return _HITTEST_FRAME_LEFT
        elif x == right - 1:
            return _HITTEST_FRAME_RIGHT
        return _HITTEST_CLIENTAREA


class InnerFrame(tff.DefaultHandler,
                 IInnerFrame,
                 IMouseListenerImpl,
                 IFocusListenerImpl): # aggregate mouse and focus listener

    def __init__(self, session, listener, inputhandler, screen,
                 top, left, row, col,
                 command, termenc, termprop):

        IMouseListenerImpl.__init__(self)
        IFocusListenerImpl.__init__(self)

        self.enabled = True

        innerscreen = Screen(row, col, 0, 0, termenc, termprop)
        canossa = Canossa(innerscreen, visibility=False)

        self._session = session

        window = screen.create_window(self)
        window.alloc(left - 1, top - 1, col + 2, row + 2)

        self._window = window

        self.top = top
        self.left = left
        self.offset_top = 0
        self.offset_left = 0
        self._termprop = termprop
        self.innerscreen = innerscreen
        self._outerscreen = screen
        self._listener = listener
        self._inputhandler = inputhandler

        self._tty = session.add_subtty('xterm', 'ja_JP.UTF-8',
                                       command, row, col, termenc,
                                       self, canossa, self)
        self._title = command

    """ tff.EventObserver override """
    def handle_end(self, context):
        self._window.close()
        self._listener.onclose(self, context)

    def handle_csi(self, context, parameter, intermediate, final):
        if self._inputhandler.handle_csi(context, parameter, intermediate, final):
            return True
        return False

    def handle_char(self, context, c):
        if self._inputhandler.handle_char(context, c):
            return True
        return False

    def moveto(self, row, col):
        if col >= self._outerscreen.width + 1:
            raise Exception("range error col=%s" % col)
        if row >= self._outerscreen.height + 1:
            raise Exception("range error row=%s" % row)
        if row < 1:
            raise Exception("range error row=%s" % row)
        if col < 1:
            raise Exception("range error col=%s" % col)
        self._window.write('\x1b[%d;%dH' % (row, col))

    """ IWidget override """
    def draw(self, region):
        if self.enabled:
            window = self._window
            screen = self.innerscreen
            outerscreen = self._outerscreen
            left = self.left + self.offset_left
            top = self.top + self.offset_top
            width = screen.width
            height = screen.height

            dirtyregion = region.add(left - 1, top - 1, width + 2, height + 2)

            # タイトルの描画
            termprop = self._termprop
            title_length = termprop.wcswidth(self._title)
            width = screen.width + 2
            if title_length < width - 11:
                pad_left = (width - title_length) / 2
                pad_right = width - title_length - pad_left
                title = ' ' * pad_left + self._title + ' ' * (pad_right - 3) + u'[x]'
            elif width > 10:
                title = '  ' + self._title[0:width - 2 - 9] + u'...   [x]'
            else:
                title = ' ' * (width - 3) + '[x]'

            window.write('\x1b[?25l')
            window.write(self._titlestyle)
            dirtyrange = dirtyregion[top - 1]
            if not dirtyrange:
                return

            dirty_left = min(dirtyrange)
            if dirty_left < left - 1:
                dirty_left = left - 1
            if dirty_left < 0:
                dirty_left = 0

            dirty_right = max(dirtyrange) + 1
            if dirty_right > left + screen.width + 1:
                dirty_right = left + screen.width + 1
            if dirty_right > outerscreen.width:
                dirty_right = outerscreen.width

            n = left - 1

            for c in title:
                length = termprop.wcwidth(c)
                if n >= outerscreen.width:
                    break
                if n >= dirty_right:
                    break
                if n == dirty_left:
                    self.moveto(top, n + 1)
                if n >= dirty_left:
                    if n in dirtyrange:
                        window.write(c)
                    else:
                        window.write("\x1b[C")
                n += length

            window.write('\x1b[m')

            # フレーム内容の描画
            for index in xrange(0, height):
                if top + index < outerscreen.height:
                    if top + index >= 0:
                        dirtyrange = dirtyregion[top + index]
                        if dirtyrange:

                            dirty_left = min(dirtyrange)
                            if dirty_left < 0:
                                dirty_left = 0
                            if dirty_left < left:
                                dirty_left = left

                            dirty_right = max(dirtyrange)
                            if dirty_right > outerscreen.width:
                                dirty_right = outerscreen.width
                            if dirty_right > left + screen.width + 1:
                                dirty_right = left + screen.width + 1

                            dirty_width = dirty_right - dirty_left

                            # フレーム左辺の描画
                            if left > 0 and left >= dirty_left:
                                row = top + index + 1
                                col = left
                                self.moveto(row, col)

                                if self._dragtype == _DRAGTYPE_LEFT:
                                    window.write('\x1b[41m')
                                elif self._hovertype == _HOVERTYPE_LEFT:
                                    window.write('\x1b[43m')
                                window.write('|')
                                window.write('\x1b[m')

                            # フレーム内容の描画
                            screen.copyrect(window, dirty_left - left, index, dirty_width, 1,
                                            dirty_left, top + index, lazy=True)

                            # フレーム右辺の描画
                            col = left + screen.width
                            if col <= dirty_right and col < outerscreen.width:
                                row = top + index
                                self.moveto(row + 1, col + 1)

                                if self._dragtype == _DRAGTYPE_RIGHT:
                                    window.write('\x1b[41m')
                                elif self._hovertype == _HOVERTYPE_RIGHT:
                                    window.write('\x1b[43m')
                                window.write('|')
                                window.write('\x1b[m')

            # フレーム下部の描画
            if top + height < outerscreen.height:
                if top + index >= 0:
                    dirtyrange = dirtyregion[top + height]

                    dirty_left = min(dirtyrange)
                    if dirty_left < left - 1:
                        dirty_left = left - 1
                    if dirty_left < 0:
                        dirty_left = 0

                    dirty_right = max(dirtyrange) + 1
                    if dirty_right > left + screen.width + 1:
                        dirty_right = left + screen.width + 1
                    if dirty_right > outerscreen.width:
                        dirty_right = outerscreen.width

                    window.write('\x1b[m')
                    self.moveto(top + height + 1, dirty_left + 1)

                    n = left - 1
                    if n >= 0 and n >= dirty_left:
                        if self._dragtype == _DRAGTYPE_BOTTOMLEFT:
                            window.write('\x1b[41m')
                        elif self._hovertype == _HOVERTYPE_BOTTOMLEFT:
                            window.write('\x1b[43m')
                        else:
                            window.write('\x1b[m')
                        window.write('+')
                        n += 1

                    if self._dragtype == _DRAGTYPE_BOTTOM:
                        window.write('\x1b[41m')
                    elif self._hovertype == _HOVERTYPE_BOTTOM:
                        window.write('\x1b[43m')
                    else:
                        window.write('\x1b[m')
                    while True:
                        if n >= dirty_right - 1:
                            if n == left + screen.width:
                                if self._dragtype == _DRAGTYPE_BOTTOMRIGHT:
                                    window.write('\x1b[41m')
                                elif self._hovertype == _HOVERTYPE_BOTTOMRIGHT:
                                    window.write('\x1b[43m')
                                else:
                                    window.write('\x1b[m')
                                window.write('+')
                            break
                        if n >= outerscreen.width:
                            break
                        n += 1
                        if n < dirty_left + 1:
                            continue
                        window.write('-')

            window.write('\x1b[?25h')
            cursor = screen.cursor

            row = cursor.row + top + 1
            if row < 1:
                return
            elif row > outerscreen.width:
                return

            col = cursor.col + left + 1
            if col < 1:
                return
            elif col > outerscreen.height:
                return

            cursor.draw(window)
            self.moveto(row, col)

    def close(self):
        self._session.destruct_subprocess(self._tty.fileno())


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

if __name__ == "__main__":
    test()

