# *****************************************************************************
#
# 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 2 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, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import sys
import curses
import threading
from select import select


class KEY:
    """helper class for keys

    converts all keys used into an instance of Key
    """
    # unfortunately, we can not use some ctrl keys, as they already have a meaning:
    #   ^H: backspace, ^I: tab, ^M: ret, ^S/^Q: flow control
    ESC = 27
    TAB = 9
    DEL = 127
    RETURN = 13
    QUIT = '^q'
    BEG_LINE = '^a'
    END_LINE = '^e'
    MENU = '^x'
    CUT = '^k'
    PASTE = '^v'
    HELP = '^g'
    UNHANDLED = -1
    GOTO_MAIN = -2
    GO_UP = -3
    UP = None  # get curses.KEY_UP
    DOWN = None
    LEFT = None
    RIGHT = None
    ENTER = None
    bynumber = {k: chr(k) for k in range(32, 127)}
    byname = {}

    @classmethod
    def init(cls):
        for name in dir(cls):
            if name.isupper():
                cls.add_key(name, getattr(cls, name))
        for base in cls.__mro__:
            for name in getattr(base, 'from_curses', ()):
                cls.add_key(name,  getattr(curses, f'KEY_{name}'))

    @classmethod
    def add_key(cls, name, nr):
        if isinstance(nr, str):
            assert nr[0] == '^'
            nr = ord(nr[1]) & 0x1f
        elif nr is None:
            nr = getattr(curses, f'KEY_{name}')
        cls.byname[name] = cls.bynumber[nr] = key = Key(name, nr)
        setattr(cls, name, key)

    @classmethod
    def add(cls, **kwds):
        for name, nr in kwds.items():
            cls.add_key(name, nr)


class Key(int):
    def __new__(cls, name, nr):
        if isinstance(nr, str):
            if nr.startswith('^'):
                nr = ord(nr[1]) & 0x1f
        key = super().__new__(cls, nr)
        key.name = name
        return key

    def short(self):
        """as __repr__, but ctrl keys are translated to ^<letter>"""
        if 0 <= self < 32:
            if 0 < self <= 26:
                return f'^{chr(96 + self)}'
            return f'^{chr(64 + self)}'
        return self.name

    def __repr__(self):
        """name by function"""
        return self.name


class Screen:
    def __init__(self):
        self.pairs = {}
        self.colors = {}
        self.nextcolor = 16
        self.lock = threading.RLock()

    def init(self):
        self.scr = curses.initscr()
        try:
            self.init_colors(self.scr)
            self.hascolors = True
        except Exception:
            self.hascolors = False
        curses.noecho()
        curses.raw()  # disable ctrl-C interrupt and ctrl-Q/S flow control
        curses.nonl()  # accept ctrl-J
        self.scr.keypad(True)
        KEY.init()

    def finish(self):
        self.scr.keypad(False)
        curses.echo()
        curses.nocbreak()
        curses.endwin()

    def make_color(self, rgb):
        if isinstance(rgb, int):
            # a color number was given
            return rgb
        idx = self.colors.get(rgb)
        if idx is None:
            idx = self.nextcolor
            self.nextcolor += 1
            self.colors[idx] = rgb
            curses.init_color(idx, *rgb)
        return idx

    def make_pair(self, fg, bg):
        fg = self.make_color(fg)
        bg = self.make_color(bg)
        pair = self.pairs.get((fg, bg))
        if pair is not None:
            return pair
        idx = len(self.pairs) + 1
        curses.init_pair(idx, fg, bg)
        self.pairs[(fg, bg)] = pair = curses.color_pair(idx)
        return pair

    def init_colors(self, stdscr):
        curses.start_color()
        for nr in range(self.nextcolor):
            rgb = curses.color_content(nr)
            self.colors.setdefault(rgb, nr)
        black = self.make_color((0, 0, 0))
        dim_white = (680, 680, 680)
        stdscr.bkgd(' ', self.make_pair(black, dim_white))
        bright_white = 1000, 1000, 1000
        self.menustyle = self.brightstyle = self.make_pair(black, bright_white)
        red = self.make_color((680, 0, 0))
        self.errorstyle = self.make_pair(red, dim_white)
        self.errorflag = ''
        very_light_blue = (800, 900, 1000)
        self.highstyle = self.make_pair(black, very_light_blue)
        light_white = (800, 800, 800)
        self.buttonstyle = self.make_pair(black, light_white)
        light_green = 0, 1000, 0
        self.querystyle = self.make_pair(black, light_green)
        yellow = 1000, 1000, 0
        self.warnstyle = self.make_pair(black, yellow)
        grey = 400, 400, 400
        self.dimstyle = self.make_pair(grey, dim_white)

    def get_key(self, timeout=None):
        if timeout is None:
            key = self.scr.getch()
        else:
            self.scr.refresh()
            if select([sys.stdin], [], [], timeout)[0]:
                key = self.scr.getch()
            else:
                return None
        key = KEY.bynumber.get(key, key)
        return key

    def set_cursor(self, flag, *cursor_pos):
        if flag:
            self.scr.move(*cursor_pos)
            curses.curs_set(1)
        else:
            curses.curs_set(0)


class BaseWriter:
    """base for writer. does nothing else than keeping track of position"""
    errorflag = '! '

    def __init__(self, screen):
        self.width = None
        self.screen = screen
        self.nextrow = 0
        self.left = 0

    def move(self, row, col):
        self.row = self.nextrow = row
        self.col = self.left = col

    def startrow(self):
        self.row = self.nextrow
        self.col = self.left
        self.nextrow = self.row + 1

    def norm(self, text, width=0):
        self.wr(text, extend_to=width)

    def dim(self, text, width=0):
        self.wr(text, self.screen.dimstyle, extend_to=width)

    def edit(self, text, width, pos):
        self.bright(text, width)

    def bright(self, text, width=0):
        self.wr(text.ljust(width), self.screen.brightstyle, extend_to=width)

    def high(self, text, width=0):
        self.wr(text.ljust(width), self.screen.highstyle, extend_to=width)

    def menu(self, text, width=0):
        self.wr(text, self.screen.menustyle, extend_to=width)

    def button(self, text, width=0):
        self.wr(text, self.screen.buttonstyle, extend_to=width)

    def error(self, text, width=0):
        self.wr(f'{self.errorflag}{text}', self.screen.errorstyle, extend_to=width)

    def write_raw(self, row, text, *attr):
        self.col += len(text)

    def wr(self, text, *attr, extend_to=0):
        """write text on screen

        :param text: the text
        :param attr: attributes
        :param extend_to: extend_to >= 0: fill up to given value
                          extend_to < 0: extend to right margin + extend_to
        """
        if self.width:
            limit = self.width - self.col
            if limit <= 0:
                return
            if extend_to < 0:
                limit += extend_to
                text = text.ljust(limit)
            else:  # elif extend_to >= limit
                text = text.ljust(extend_to)[:limit]
        else:
            text = text.ljust(extend_to)
        self.write_raw(self.row, text, *attr)


class Writer(BaseWriter):
    highstyle = curses.A_REVERSE
    buttonstyle = curses.A_BOLD
    barstyle = curses.A_REVERSE
    brightstyle = menustyle = 0
    errorstyle = 0
    errorflag = '! '
    newoffset = None
    querystyle = curses.A_BOLD
    warnstyle = curses.A_REVERSE
    dimstyle = 0
    leftwidth = 8  # minimum left column width

    def __init__(self, screen, main):
        super().__init__(screen)
        self.scr = screen.scr
        self.main = main
        self.scr.clear()
        self.height, self.width = self.scr.getmaxyx()
        self.popup = None
        self.top = 0
        self.bot = self.height
        self.adjust_offset_to = None
        self.preferred_window_row = None
        self.offset = 0

    def set_leftwidth(self, leftwidth):
        if leftwidth < 1:
            self.leftwidth = int(self.width * leftwidth + 1)
        else:
            self.leftwidth = leftwidth

    def write_raw(self, row, text, *attr):
        row = self.row - self.offset  # screen row
        if self.top <= row < self.bot and self.col < self.width:
            try:
                self.scr.addstr(row, self.col, text, *attr)
                self.col += len(text)
            except curses.error:
                self.col += len(text)
                if self.col < self.width or row < self.height - 1:
                    raise

    def edit(self, text, width, pos):
        """write text and set cursor at given pos

        also scroll horizontally if cursor would be outside screen
        """
        if pos is not None:
            maxwidth = self.width - self.col
            scrollrange = len(text) - maxwidth + 1
            if scrollrange <= 0 or pos < scrollrange:
                offset = 0
            else:
                offset = min(pos - scrollrange, scrollrange)
                text = text[offset:]
                if len(text) - offset < maxwidth:
                    text += ' '
            self.set_cursor_pos(pos - offset, True)
        self.bright(text, width)

    def set_cursor_pos(self, pos=0, visible=False):
        self.cursor_visible = visible
        self.main.cursor_pos = self.row - self.offset, self.col + pos

    def vline(self, row, col, length, top, bottom, left, right, *attr):
        """draw a vertical line

        :param row, col: upper start point
        :param length: length without clipping
        :param top, bottom, left, right: clipping
        :return: <start row or None>, <end row or None>  (None is returned, when clipped on this corner)
        """
        beg = None
        end = None
        if left <= col < right and row < bottom:
            end = row + length
            if end >= bottom:
                length -= end - bottom
                end = None
            beg = row
            if beg < top:
                row = top
                length -= top - beg
                beg = None
            if length > 0:
                self.scr.vline(row, col, curses.ACS_VLINE, length, *attr)
        return beg, end

    def hline(self, row, col, length, top, bottom, left, right, *attr):
        """draw a horizontal line

        :param row, col: left start point
        :param length: length without clipping
        :param top, bottom, left, right: clipping
        :return: <start row or None>, <end row or None>  (None is returned, when clipped on this corner)
        """
        beg = None
        end = None
        if top <= row < bottom and col < right:
            end = col + length
            if end >= right:
                length -= end - right
                end = None
            beg = col
            if beg < left:
                col = left
                length -= left - beg
                beg = None
            if length > 0:
                self.scr.hline(row, col, curses.ACS_HLINE, length, *attr)
            else:
                return None, None
        return beg, end

    def rectangle(self, row, col, height, width, *attr, **clip):
        """clipped rectangle"""
        row = row - self.offset
        args = (
            max(0, clip.get('top', 0) - self.offset),
            min(self.height, clip.get('bottom', self.height + self.offset) - self.offset),
            max(0, clip.get('left', 0)),
            min(self.width, clip.get('right', self.width))
        ) + attr
        self.vline(row, col, height, *args)
        self.vline(row, col + width, height, *args)
        left, right = self.hline(row, col, width, *args)
        if left is not None:
            self.scr.addch(row, left, curses.ACS_ULCORNER, *attr)
        if right is not None:
            self.scr.addch(row, right, curses.ACS_URCORNER, *attr)
        row += height
        left, right = self.hline(row, col, width, *args)
        if left is not None:
            self.scr.addch(row, left, curses.ACS_LLCORNER, *attr)
        if right is not None:
            self.scr.addch(row, right, curses.ACS_LRCORNER, *attr)
