# Copyright 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import curses
import os
import signal
import sys

import app.curses_util
import app.debug_window
import app.file_manager_window
import app.log
import app.prediction_window
import app.window


class ProgramWindow(app.window.ActiveWindow):
    """The outermost window. This window doesn't draw content itself. It is
    primarily a container the child windows that make up the UI. The program
    window is expected to be a singleton. The program window has no parent (the
    parent is None). Calls that propagate up the window tree stop here or jump
    over to the |program|."""

    def __init__(self, program):
        if app.config.strict_debug:
            assert issubclass(program.__class__, app.ci_program.CiProgram), self
        app.window.ActiveWindow.__init__(self, program, None)
        self.clicks = 0
        self.focusedWindow = None
        self.modalUi = None
        self.program = program
        self.priorClick = 0
        self.savedMouseButton1Down = False
        self.savedMouseWindow = None
        self.savedMouseX = -1
        self.savedMouseY = -1
        self.showLogWindow = self.program.prefs.startup['showLogWindow']
        self.debugWindow = app.debug_window.DebugWindow(self.program, self)
        self.debugUndoWindow = app.debug_window.DebugUndoWindow(
            self.program, self)
        self.logWindow = app.window.LogWindow(self.program, self)
        self.popupWindow = app.window.PopupWindow(self.program, self)
        self.paletteWindow = app.window.PaletteWindow(self.program, self)
        # The input window is the main document window.
        self.inputWindow = app.window.InputWindow(self.program, self)
        self.inputWindow.parent = self
        # Set up file manager.
        self.fileManagerWindow = app.file_manager_window.FileManagerWindow(
            self.program, self, self.inputWindow)
        self.fileManagerWindow.parent = self
        # Set up prediction.
        self.predictionWindow = app.prediction_window.PredictionWindow(
            self.program, self)
        self.predictionWindow.parent = self
        # Put the input window in front on startup.
        self.inputWindow.reattach()

    def changeFocusTo(self, changeTo):
        self.focusedWindow.controller.onChange()
        # Unfocus all the windows from the prior focused window to the common
        # root.
        commonRoot = self.findCommonRoot(self.focusedWindow, changeTo)
        current = self.focusedWindow
        while current != commonRoot:
            if current.isFocusable:
                current.unfocus()
            current = current.parent
        self.setFocusedWindow(changeTo)

    def debugDraw(self, win):
        if self.showLogWindow:
            self.debugWindow.debugDraw(self.program, win)
            self.debugUndoWindow.debugUndoDraw(win)

    def executeCommandList(self, cmdList):
        for cmd, eventInfo in cmdList:
            self.doPreCommand()
            if cmd == curses.KEY_RESIZE:
                self.handleScreenResize(self.focusedWindow)
                continue
            self.focusedWindow.controller.doCommand(cmd, eventInfo)
            if cmd == curses.KEY_MOUSE:
                self.handleMouse(eventInfo)
            self.focusedWindow.controller.onChange()

    def findCommonRoot(self, first, second):
        """Find the Window that is the parent of both |first| and |second|. If
        |first| is a (grand*)parent of |second|, return |first| (or vice versa).
        """
        # assert self.focusedWindow is not changeTo
        if first is second:
            return first
        firstPath = [first]
        while firstPath[-1].parent:
            firstPath.append(firstPath[-1].parent)
            if firstPath[-1] == second:
                return second
        secondPath = [second]
        while secondPath[-1].parent:
            secondPath.append(secondPath[-1].parent)
            if secondPath[-1] == first:
                return first
        # assert firstPath[-1] is secondPath[-1]
        # Assumptions: The first unequal match will never be found at [-1]. A
        # match will always be found before exhausting the lists. It doesn't
        # matter which list is longer.
        for i in range(len(firstPath)):
            if firstPath[-(i + 1)] is not secondPath[-(i + 1)]:
                root = firstPath[-i]
                break
        return root

    def focus(self):
        self.setFocusedWindow(self.zOrder[-1])

    def setFocusedWindow(self, window):
        # Depth-first search for focusable window.
        depth = [window]
        while len(depth):
            possibility = depth.pop()
            if possibility.isFocusable:
                if app.config.strict_debug:
                    assert issubclass(possibility.__class__,
                                      app.window.ActiveWindow)
                    assert possibility.controller
                self.focusedWindow = possibility
                self.focusedWindow.focus()
                self.focusedWindow.textBuffer.compoundChangePush()
                return
            depth += possibility.zOrder
            app.log.info(depth)
        app.log.error("focusable window not found")

    def doPreCommand(self):
        # Reset UI elements that adjust when new commands are issued.
        # E.g. setMessage()
        win = self.focusedWindow
        while win is not None and win is not self:
            win.doPreCommand()
            win = win.parent

    def longTimeSlice(self):
        """returns whether work is finished (no need to call again)."""
        win = self.focusedWindow
        while win is not None and win is not self:
            if not win.longTimeSlice():
                return False
            win = win.parent
        return True

    def shortTimeSlice(self):
        """returns whether work is finished (no need to call again)."""
        win = self.focusedWindow
        while win is not None and win is not self:
            if not win.shortTimeSlice():
                return False
            #assert win is not win.parent
            win = win.parent
        return True

    def clickedNearby(self, row, col):
        y, x = self.priorClickRowCol
        return y - 1 <= row <= y + 1 and x - 1 <= col <= x + 1

    def handleMouse(self, info):
        """Mouse handling is a special case. The getch() curses function will
        signal the existence of a mouse event, but the event must be fetched and
        parsed separately."""
        (_, mouseCol, mouseRow, _, bState) = info[0]
        app.log.mouse()
        eventTime = info[1]
        rapidClickTimeout = .5

        def findWindow(parent, mouseRow, mouseCol):
            for window in reversed(parent.zOrder):
                if window.contains(mouseRow, mouseCol):
                    return findWindow(window, mouseRow, mouseCol)
            return parent

        window = findWindow(self, mouseRow, mouseCol)
        if window == self:
            app.log.mouse('click landed on screen')
            return
        if self.focusedWindow != window and window.isFocusable:
            app.log.debug('before change focus')
            window.changeFocusTo(window)
            app.log.debug('after change focus')
        mouseRow -= window.top
        mouseCol -= window.left
        app.log.mouse(mouseRow, mouseCol)
        app.log.mouse("\n", window)
        button1WasDown = self.savedMouseButton1Down
        self.savedMouseButton1Down = False
        #app.log.info('bState', app.curses_util.mouseButtonName(bState))
        if bState & curses.BUTTON1_RELEASED:
            if button1WasDown:
                app.log.mouse(bState, curses.BUTTON1_RELEASED)
                if self.priorClick + rapidClickTimeout <= eventTime:
                    window.mouseRelease(
                        mouseRow, mouseCol, bState & curses.BUTTON_SHIFT,
                        bState & curses.BUTTON_CTRL, bState & curses.BUTTON_ALT)
                #else:
                #  signal.setitimer(signal.ITIMER_REAL, rapidClickTimeout)
            else:
                # Some terminals (linux?) send BUTTON1_RELEASED after moving the
                # mouse. Specifically if the terminal doesn't use button 4 for
                # mouse movement. Mouse drag or mouse wheel movement done.
                pass
        elif bState & curses.BUTTON1_PRESSED:
            self.savedMouseButton1Down = True
            if (self.priorClick + rapidClickTimeout > eventTime and
                    self.clickedNearby(mouseRow, mouseCol)):
                self.clicks += 1
                self.priorClick = eventTime
                if self.clicks == 2:
                    window.mouseDoubleClick(
                        mouseRow, mouseCol, bState & curses.BUTTON_SHIFT,
                        bState & curses.BUTTON_CTRL, bState & curses.BUTTON_ALT)
                else:
                    window.mouseTripleClick(
                        mouseRow, mouseCol, bState & curses.BUTTON_SHIFT,
                        bState & curses.BUTTON_CTRL, bState & curses.BUTTON_ALT)
                    self.clicks = 1
            else:
                self.clicks = 1
                self.priorClick = eventTime
                self.priorClickRowCol = (mouseRow, mouseCol)
                window.mouseClick(
                    mouseRow, mouseCol, bState & curses.BUTTON_SHIFT,
                    bState & curses.BUTTON_CTRL, bState & curses.BUTTON_ALT)
        elif bState & (curses.BUTTON2_PRESSED | 0x200000):
            window.mouseWheelUp(bState & curses.BUTTON_SHIFT,
                                bState & curses.BUTTON_CTRL,
                                bState & curses.BUTTON_ALT)
        elif bState & (curses.BUTTON4_PRESSED | curses.REPORT_MOUSE_POSITION):
            # Notes from testing:
            # Mac seems to send BUTTON4_PRESSED during mouse move; followed by
            #   BUTTON4_RELEASED.
            # Linux seems to send REPORT_MOUSE_POSITION during mouse move;
            # followed by
            #   BUTTON1_RELEASED.
            if self.savedMouseX == mouseCol and self.savedMouseY == mouseRow:
                if bState & curses.REPORT_MOUSE_POSITION:
                    # This is a hack for dtterm mouse wheel on Mac OS X.
                    window.mouseWheelUp(bState & curses.BUTTON_SHIFT,
                                        bState & curses.BUTTON_CTRL,
                                        bState & curses.BUTTON_ALT)
                else:
                    # This is the normal case:
                    window.mouseWheelDown(bState & curses.BUTTON_SHIFT,
                                          bState & curses.BUTTON_CTRL,
                                          bState & curses.BUTTON_ALT)
            else:
                if (self.savedMouseWindow and
                        self.savedMouseWindow is not window):
                    mouseRow += window.top - self.savedMouseWindow.top
                    mouseCol += window.left - self.savedMouseWindow.left
                    window = self.savedMouseWindow
                window.mouseMoved(
                    mouseRow, mouseCol, bState & curses.BUTTON_SHIFT,
                    bState & curses.BUTTON_CTRL, bState & curses.BUTTON_ALT)
        elif bState & curses.BUTTON4_RELEASED:
            # Mouse drag or mouse wheel movement done.
            app.log.mouse("BUTTON4_RELEASED")
            pass
        else:
            app.log.mouse('got bState', app.curses_util.mouseButtonName(bState),
                          hex(bState))
        self.savedMouseWindow = window
        self.savedMouseX = mouseCol
        self.savedMouseY = mouseRow

    def handleScreenResize(self, window):
        #app.log.debug('handleScreenResize -----------------------')
        if sys.platform == 'darwin':
            # Some terminals seem to resize the terminal and others leave it
            # to the application to resize the curses terminal.
            rows, cols = app.curses_util.terminalSize()
            curses.resizeterm(rows, cols)
        self.top = self.left = 0
        self.rows, self.cols = app.window.mainCursesWindow.getmaxyx()
        self.layout()
        window.controller.onChange()
        self.render()

    def hide(self):
        pass

    def layout(self):
        """Arrange the debug, log, and input windows."""
        rows, cols = self.rows, self.cols
        #app.log.detail('layout', rows, cols)
        if self.showLogWindow:
            inputWidth = min(88, cols)
            debugWidth = max(cols - inputWidth - 1, 0)
            debugRows = 20
            self.debugWindow.reshape(0, inputWidth + 1, debugRows, debugWidth)
            self.debugUndoWindow.reshape(debugRows, inputWidth + 1,
                                         rows - debugRows, debugWidth)
            self.logWindow.reshape(debugRows, 0, rows - debugRows, inputWidth)
            rows = debugRows
        else:
            inputWidth = cols
        if 1:  # Full screen.
            for window in self.zOrder:
                window.reshape(0, 0, rows, inputWidth)
        else:  # Split horizontally.
            count = len(self.zOrder)
            eachRows = rows // count
            for i, window in enumerate(self.zOrder[:-1]):
                window.reshape(eachRows * i, 0, eachRows, inputWidth)
            self.zOrder[-1].reshape(eachRows * (count - 1), 0,
                                    rows - eachRows * (count - 1), inputWidth)

    def nextFocusableWindow(self, start, reverse=False):
        # Keep the tab focus in the child branch. (The child view will call
        # this, tell the child there is nothing to tab to up here).
        return None

    def normalize(self):
        self.presentModal(None)

    def onPrefChanged(self, category, name):
        pass

    def presentModal(self, changeTo, top=0, left=0):
        if self.modalUi is not None:
            #self.modalUi.controller.onChange()
            self.modalUi.hide()
        app.log.info('\n', changeTo)
        self.modalUi = changeTo
        if self.modalUi is not None:
            self.modalUi.moveSizeToFit(top, left)
            self.modalUi.bringToFront()

    def quitNow(self):
        self.program.quitNow()

    def render(self):
        if self.showLogWindow:
            self.logWindow.render()
        app.window.ActiveWindow.render(self)
        window = self.focusedWindow
        self.debugDraw(window)
        penRow = window.textBuffer.penRow
        penCol = window.textBuffer.penCol
        if (window.showCursor and penRow >= window.scrollRow and
                penRow < window.scrollRow + window.rows):
            self.program.backgroundFrame.setCursor(
                (window.top + penRow - window.scrollRow,
                 window.left + penCol - window.scrollCol))
        else:
            self.program.backgroundFrame.setCursor(None)

    def reshape(self, top, left, rows, cols):
        app.window.ActiveWindow.reshape(self, top, left, rows, cols)
        self.layout()

    def bringToFront(self):
        pass

    def unfocus(self):
        pass