# -*- coding: utf-8 -*-

# Copyright 2016 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
try:
    unicode
except NameError:
    unicode = str
    unichr = chr

import curses
import curses.ascii
import fcntl
import os
import signal
import struct
import sys
import termios
import unicodedata

import app.config

# Strings are found using the cursesKeyName() function.
# Constants are found using the curses.getch() function.

# Tuple events are preceded by an escape (27).
BRACKETED_PASTE_BEGIN = (91, 50, 48, 48, 126)  # i.e. "[200~"
BRACKETED_PASTE_END = (91, 50, 48, 49, 126)  # i.e. "[201~"
BRACKETED_PASTE = (b'terminal_paste',)  # Pseudo event type.

UNICODE_INPUT = (b'unicode_input',)  # Pseudo event type.

CTRL_AT = b'^@'  # 0x00
CTRL_SPACE = b'^@'  # 0x00
CTRL_A = b'^A'  # 0x01
CTRL_B = b'^B'  # 0x02
CTRL_C = b'^C'  # 0x03
CTRL_D = b'^D'  # 0x04
CTRL_E = b'^E'  # 0x05
CTRL_F = b'^F'  # 0x06
CTRL_G = b'^G'  # 0x07
CTRL_H = b'^H'  # 0x08
CTRL_I = b'^I'  # 0x09
CTRL_J = b'^J'  # 0x0a
CTRL_K = b'^K'  # 0x0b
CTRL_L = b'^L'  # 0x0c
CTRL_M = b'^M'  # 0x0d
CTRL_N = b'^N'  # 0x0e
CTRL_O = b'^O'  # 0x0f
CTRL_P = b'^P'  # 0x10
CTRL_Q = b'^Q'  # 0x11
CTRL_R = b'^R'  # 0x12
CTRL_S = b'^S'  # 0x13
CTRL_T = b'^T'  # 0x14
CTRL_U = b'^U'  # 0x15
CTRL_V = b'^V'  # 0x16
CTRL_W = b'^W'  # 0x17
CTRL_X = b'^X'  # 0x18
CTRL_Y = b'^Y'  # 0x19
CTRL_Z = b'^Z'  # 0x1a
CTRL_OPEN_BRACKET = b'^['  # 0x1b
CTRL_BACKSLASH = b'^\\'  # 0x1c
CTRL_CLOSE_BRACKET = b'^]'  # 0x1d
CTRL_CARROT = b'^^'  # 0x1e
CTRL_UNDERBAR = b'^_'  # 0x1f
CTRL_BACKSPACE = b'^BACKSPACE'

KEY_ALT_A = 165
KEY_ALT_B = 171
KEY_ALT_C = 167
KEY_ALT_S = 159
KEY_ALT_SHIFT_PAGE_DOWN = b'kNXT4'
KEY_ALT_SHIFT_PAGE_UP = b'kPRV4'
KEY_BACKSPACE1 = curses.ascii.BS  # 8
KEY_BACKSPACE2 = curses.ascii.DEL  # 127
KEY_BACKSPACE3 = curses.KEY_BACKSPACE  # 263
KEY_BTAB = curses.KEY_BTAB
KEY_DELETE = curses.KEY_DC
KEY_END = curses.KEY_END
KEY_ESCAPE = curses.ascii.ESC
KEY_HOME = curses.KEY_HOME
KEY_PAGE_DOWN = curses.KEY_NPAGE
KEY_PAGE_UP = curses.KEY_PPAGE
KEY_SEND = curses.KEY_SEND
KEY_SHIFT_PAGE_DOWN = curses.KEY_SNEXT
KEY_SHIFT_PAGE_UP = curses.KEY_SPREVIOUS
KEY_SHOME = curses.KEY_SHOME

if sys.platform == u"darwin":
    KEY_ALT_LEFT = (91, 49, 59, 57, 68)
    KEY_ALT_RIGHT = (91, 49, 59, 57, 67)
    KEY_ALT_SHIFT_LEFT = (
        91,
        49,
        59,
        49,
        48,
        68,
    )
    KEY_ALT_SHIFT_RIGHT = (
        91,
        49,
        59,
        49,
        48,
        67,
    )
else:
    KEY_ALT_LEFT = b'kLFT3'
    KEY_ALT_RIGHT = b'kRIT3'
    KEY_ALT_SHIFT_LEFT = b'kLFT4'
    KEY_ALT_SHIFT_RIGHT = b'kRIT4'

if u"SSH_CLIENT" in os.environ:
    KEY_ALT_LEFT = (98,)  # Need a better way to sort this out.
    KEY_ALT_RIGHT = (102,)  # ditto

KEY_CTRL_DOWN = b'kDN5'
KEY_CTRL_SHIFT_DOWN = b'kDN6'
KEY_CTRL_LEFT = b'kLFT5'
KEY_CTRL_SHIFT_LEFT = b'kLFT6'
KEY_CTRL_RIGHT = b'kRIT5'
KEY_CTRL_SHIFT_RIGHT = b'kRIT6'
KEY_CTRL_UP = b'kUP5'
KEY_CTRL_SHIFT_UP = b'kUP6'

KEY_F1 = curses.KEY_F1
KEY_F2 = curses.KEY_F2
KEY_F3 = curses.KEY_F3
KEY_F4 = curses.KEY_F4
KEY_F5 = curses.KEY_F5
KEY_F6 = curses.KEY_F6
KEY_F7 = curses.KEY_F7
KEY_F8 = curses.KEY_F8
KEY_F9 = curses.KEY_F9
KEY_F10 = curses.KEY_F10
KEY_SHIFT_F1 = curses.KEY_F13
KEY_SHIFT_F2 = curses.KEY_F14
KEY_SHIFT_F3 = curses.KEY_F15
KEY_SHIFT_F4 = curses.KEY_F16
KEY_SHIFT_F5 = curses.KEY_F17
KEY_SHIFT_F6 = curses.KEY_F18
KEY_SHIFT_F7 = curses.KEY_F19
KEY_SHIFT_F8 = curses.KEY_F20
KEY_SHIFT_F9 = curses.KEY_F21
KEY_SHIFT_F10 = curses.KEY_F22

KEY_SHIFT_DOWN = curses.KEY_SF
KEY_DOWN = curses.KEY_DOWN
KEY_SHIFT_UP = curses.KEY_SR
KEY_UP = curses.KEY_UP
KEY_LEFT = curses.KEY_LEFT
KEY_SHIFT_LEFT = curses.KEY_SLEFT
KEY_RIGHT = curses.KEY_RIGHT
KEY_SHIFT_RIGHT = curses.KEY_SRIGHT

KEY_MOUSE = curses.KEY_MOUSE
KEY_RESIZE = curses.KEY_RESIZE


def mouseButtonName(buttonState):
    """Curses debugging. Prints readable name for state of mouse buttons."""
    result = u""
    if buttonState & curses.BUTTON1_RELEASED:
        result += u'BUTTON1_RELEASED'
    if buttonState & curses.BUTTON1_PRESSED:
        result += u'BUTTON1_PRESSED'
    if buttonState & curses.BUTTON1_CLICKED:
        result += u'BUTTON1_CLICKED'
    if buttonState & curses.BUTTON1_DOUBLE_CLICKED:
        result += u'BUTTON1_DOUBLE_CLICKED'

    if buttonState & curses.BUTTON2_RELEASED:
        result += u'BUTTON2_RELEASED'
    if buttonState & curses.BUTTON2_PRESSED:
        result += u'BUTTON2_PRESSED'
    if buttonState & curses.BUTTON2_CLICKED:
        result += u'BUTTON2_CLICKED'
    if buttonState & curses.BUTTON2_DOUBLE_CLICKED:
        result += u'BUTTON2_DOUBLE_CLICKED'

    if buttonState & curses.BUTTON3_RELEASED:
        result += u'BUTTON3_RELEASED'
    if buttonState & curses.BUTTON3_PRESSED:
        result += u'BUTTON3_PRESSED'
    if buttonState & curses.BUTTON3_CLICKED:
        result += u'BUTTON3_CLICKED'
    if buttonState & curses.BUTTON3_DOUBLE_CLICKED:
        result += u'BUTTON3_DOUBLE_CLICKED'

    if buttonState & curses.BUTTON4_RELEASED:
        result += u'BUTTON4_RELEASED'
    if buttonState & curses.BUTTON4_PRESSED:
        result += u'BUTTON4_PRESSED'
    if buttonState & curses.BUTTON4_CLICKED:
        result += u'BUTTON4_CLICKED'
    if buttonState & curses.BUTTON4_DOUBLE_CLICKED:
        result += u'BUTTON4_DOUBLE_CLICKED'

    if buttonState & curses.REPORT_MOUSE_POSITION:
        result += u'REPORT_MOUSE_POSITION'

    if buttonState & curses.BUTTON_SHIFT:
        result += u' SHIFT'
    if buttonState & curses.BUTTON_CTRL:
        result += u' CTRL'
    if buttonState & curses.BUTTON_ALT:
        result += u' ALT'
    return result


def cursesKeyName(keyCode):
    try:
        return curses.keyname(keyCode)
    except Exception:
        pass
    return None


def columnToIndex(column, string):
    """If the visual cursor is on |column|, which index of the string is the
    cursor on?"""
    if app.config.strict_debug:
        assert isinstance(column, int)
        assert isinstance(string, unicode)
    if not string:
        return None
    indexLimit = len(string) - 1
    colCursor = 0
    index = 0
    for ch in string:
        colCursor += charWidth(ch, colCursor)
        if colCursor > column:
            break
        index += 1
        if index > indexLimit:
            return None
    return index


def charAtColumn(column, string):
    """If the visual cursor is on |column|, which index of the string is the
    cursor on?"""
    if app.config.strict_debug:
        assert isinstance(column, int)
        assert isinstance(string, unicode)
    index = columnToIndex(column, string)
    if index is not None:
        return string[index]
    return None


def fitToRenderedWidth(column, width, string):
    """With |width| character cells (columns) available, how much of |string|
    can I render? The start |column| is required to calculate tab stops.

    The result can vary for double-wide characters, zero-width characters, and
    tabs. For plain, printable ASCII, the result will always be the lesser of
    |width| or len(string).
    """
    if app.config.strict_debug:
        assert isinstance(width, int)
        assert isinstance(string, unicode)
    indexLimit = len(string)
    index = 0
    for i in string:
        cols = charWidth(i, column)
        width -= cols
        column += cols
        if width < 0 or index >= indexLimit:
            break
        index += 1
    return index


def renderedFindIter(string, beginCol, endCol, charGroups, numbers, eolSpaces):
    """Get a slice (similar to `string[beginCol:endCol]`) based on the rendered
    width of the string.

    Note: charGroups cannot (currently) contain double width characters.

    Returns:
      tuple of (subStr, column, index, id)
    """
    if app.config.strict_debug:
        assert isinstance(string, unicode)
        assert isinstance(beginCol, int)
        assert isinstance(endCol, int)
    column = 0
    index = 0
    limit = len(string)
    while index < limit:
        if column >= endCol:
            break
        c = string[index]
        if column >= beginCol:
            if numbers and c in '0123456789':
                sre = app.regex.kReNumbers.match(string[index:])
                begin = index
                length = min(sre.regs[0][1], endCol - column)
                index += length
                yield string[begin:index], column, length, len(charGroups)
                column += length
            else:
                for id, group in enumerate(charGroups):
                    if c in group:
                        begin = index
                        while index < limit and string[index] in group:
                            index += 1
                        #if
                        yield string[begin:index], column, index - begin, id
                        column += index - begin
                        break
                else:
                    column += charWidth(c, column)
                    index += 1
        else:
            column += charWidth(c, column)
            index += 1
    if eolSpaces and limit and string[-1] == ' ':
        index = limit - 1
        while index and string[index - 1] == ' ':
            index -= 1
        yield string[index:], index, index, len(charGroups) + 1


def renderedSubStr(string, beginCol, endCol=None):
    """
    Get a slice (similar to `string[beginCol:endCol]`) based on the rendered
    width of the string. If columns beginCol or endCol land in the middle of a
    double-wide character, a space is used to pad the result.

    Negative columns are not supported. (Just haven't implemented it).

    Args:
      string: The string to slice.
      beginCol: The first column of text (inclusive).
      endCol: The last column of text (exclusive). Omit parameter for
              end-of-line (similar to `string[beginCol:]`).

    Returns:
      unicode string
    """
    if endCol is None:
        endCol = sys.maxsize
    if app.config.strict_debug:
        assert isinstance(string, unicode)
        assert isinstance(beginCol, int)
        assert isinstance(endCol, int)
    column = 0
    i = 0
    limit = len(string)
    output = []
    while column < beginCol:
        if i >= limit:
            # The |string| is entirely before |beginCol|.
            return u""
        ch = string[i]
        column += charWidth(ch, column)
        i += 1
        if column > beginCol:
            # Split the leading character.
            paddingWidth = column - beginCol
            output.append(u" " * paddingWidth)
    while i < limit and column < endCol:
        ch = string[i]
        lastCharWidth = charWidth(ch, column)
        column += lastCharWidth
        i += 1
        if column > endCol:
            # Split the trailing character.
            paddingWidth = min(endCol - (column - lastCharWidth),
                               lastCharWidth - 1)
            output.append(u" " * paddingWidth)
        else:
            if ch == u"\t":
                output.append(u" " * lastCharWidth)
            else:
                output.append(ch)
    return u"".join(output)


if sys.version_info[0] == 2:

    def charWidth(ch, column, tabWidth=8):
        if ch == u"\t":
            return tabWidth - (column % tabWidth)
        elif ch == u"" or ch < u" ":
            return 0
        elif ch < u"ᄀ":
            # Optimization.
            return 1
        elif unicodedata.east_asian_width(ch) in (u"F", r"W"):
            return 2
        return 1

    def isDoubleWidth(ch):
        if ch == u"" or ch < u"ᄀ":
            # Optimization.
            return False
        width = unicodedata.east_asian_width(ch)
        if width in (u"F", u"W"):
            return True
        return False

    def isZeroWidth(ch):
        return ch == u"" or ch < u" "  #or unicodedata.east_asian_width(ch) == "N"
else:

    def charWidth(ch, column, tabWidth=8):
        if ch == u"\t":
            return tabWidth - (column % tabWidth)
        elif ch == u"" or ch < u" ":
            return 0
        elif ch < u"ᄀ":
            # Optimization.
            return 1
        elif unicodedata.east_asian_width(ch) == u"W":
            return 2
        return 1

    def isDoubleWidth(ch):
        if ch == u"" or ch < u"ᄀ":
            # Optimization.
            return False
        return unicodedata.east_asian_width(ch) == "W"

    def isZeroWidth(ch):
        return ch == u"" or ch < u" "  #or unicodedata.east_asian_width(ch) == "N"


def floorCol(column, line):
    """Round off the column so that it aligns with the start of a character.
    For lines without multi-column characters the result will equal |column|.
    If |column| is midway in a multi-column character the result will be less
    than |column| (i.e. rounding the column number downward).
    """
    if app.config.strict_debug:
        assert isinstance(column, int)
        assert isinstance(line, unicode)
    floorColumn = 0
    for ch in line:
        width = charWidth(ch, floorColumn)
        if floorColumn + width > column:
            return floorColumn
        floorColumn += width
    return floorColumn


def priorCharCol(column, line):
    """Return the start column of the character before |column|.
    """
    if app.config.strict_debug:
        assert isinstance(column, int)
        assert isinstance(line, unicode)
    if column == 0:
        return None
    priorColumn = 0
    for ch in line:
        width = charWidth(ch, priorColumn)
        if priorColumn + width >= column:
            return priorColumn
        priorColumn += width
    return None


def columnWidth(string):
    """When rendering |string| how many character cells will be used? For ASCII
    characters this will equal len(string). For many Chinese characters and
    emoji the value will be greater than len(string), since many of them use two
    cells.
    """
    if app.config.strict_debug:
        assert isinstance(string, unicode)
    width = 0
    for i in string:
        width += charWidth(i, width)
    return width


def wrapLines(lines, indent, width):
    """Word wrap lines of text.

    Args:
      lines (list of unicode): input text.
      indent (unicode): will be added as a prefix to each line of output.
      width (int): is the column limit for the strings. Each double-wide
        character counts as two columns.

    Returns:
      List of strings
    """
    if app.config.strict_debug:
        assert isinstance(lines, tuple), repr(lines)
        assert len(lines) == 0 or isinstance(lines[0], unicode)
        assert isinstance(indent, unicode), repr(path)
        assert isinstance(width, int), repr(int)
    # There is a textwrap library in Python, but I was having trouble getting it
    # to do exactly what I desired. It may be useful to revisit textwrap later.
    words = u" ".join(lines).split()
    output = [indent]
    indentLen = columnWidth(indent)
    index = 0
    while index < len(words):
        lineLen = columnWidth(output[-1])
        word = words[index]
        wordLen = columnWidth(word)
        if lineLen == indentLen and lineLen + wordLen < width:
            output[-1] += word
        elif lineLen + wordLen + 1 < width:
            output[-1] += u" " + word
        else:
            output.append(indent + word)
        index += 1
    return output


# This is built-in in Python 3.
# In Python 2 it's done by hand.
def terminalSize():
    h, w = struct.unpack(
        b'HHHH',
        fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack(b'HHHH', 0, 0, 0,
                                                       0)))[:2]
    return h, w


def hackCursesFixes():
    if sys.platform == u'darwin':

        def windowChangedHandler(signum, frame):
            curses.ungetch(curses.KEY_RESIZE)

        signal.signal(signal.SIGWINCH, windowChangedHandler)

    def wakeGetch(signum, frame):
        curses.ungetch(0)

    signal.signal(signal.SIGUSR1, wakeGetch)