#!/usr/bin/env python3

"""A curses+pad experiment."""

import curses
import math
import typing

# For debugging:
# from pudb.remote import set_trace
# set_trace(term_size=(140, 32))

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from _curses import _CursesWindow as Window
else:
    from typing import Any as Window


def row_col_to_char(pad_max_row: int, pad_max_column: int, row: int, column: int) -> str:
    """
    Convert a row, column into a magnitude, and from there convert it to character in a..z.

    Why?  Because we want to put some simple ASCII art in the pad for testing.  This gives
    us something to scroll and search.
    """
    pad_max_row_over_2 = pad_max_row / 2.0
    pad_max_column_over_2 = pad_max_column / 2.0
    sqrt_of_2 = math.sqrt(2.0)
    # assert side_over_2 == 50.0

    shifted_row = row - pad_max_row_over_2
    # assert -50.0 <= shifted_row <= 50
    shifted_column = column - pad_max_column_over_2
    # assert -50.0 <= shifted_column <= 50

    scaled_row = shifted_row / pad_max_row_over_2
    # assert -1.0 <= scaled_row <= 1.0
    scaled_column = shifted_column / pad_max_column_over_2
    # assert -1.0 <= scaled_column <= 1.0

    magnitude = math.sqrt(scaled_row ** 2.0 + scaled_column ** 2.0)
    # assert 0.0 <= magnitude <= sqrt_of_2

    scaled_magnitude = magnitude / sqrt_of_2
    # assert 0.0 <= scaled_magnitude <= 1.0

    color_char = chr(int(scaled_magnitude * 25.0) + ord('a'))
    # assert 'a' <= color_char <= 'z'

    return color_char


def setup() -> Window:
    """Set up curses."""
    stdscr = curses.initscr()
    curses.noecho()
    curses.raw()
    stdscr.keypad(True)

    # This refresh is a little weird, but it's needed to get the pad to be displayed.
    stdscr.refresh()

    return stdscr


def teardown() -> None:
    """Tear down curses."""
    curses.noraw()
    curses.echo()
    curses.endwin()


def circle(pad_max_row: int, pad_max_column: int) -> typing.List[str]:
    """Generate a weird sort of sphere using ASCII characters."""
    text_list = []

    # These loops fill the pad with letters
    for row in range(0, pad_max_row):
        line_list = []
        for column in range(0, pad_max_column):
            if row == pad_max_row - 1 and column == pad_max_column - 1:
                # Curses doesn't want to put a character in the lower right corner
                continue
            color_char = row_col_to_char(pad_max_row, pad_max_column, row, column)
            line_list.append(color_char)
        text_list.append(''.join(line_list))

    return text_list


def put_stuff_on_pad(pad_max_row: int, pad_max_column, text: typing.List[str]):
    """Display a weird sort of sphere using ASCII characters."""
    pad = curses.newpad(pad_max_row, pad_max_column)

    # These loops fill the pad with letters
    for string_index, string in enumerate(text):
        for color_char_index, color_char in enumerate(string):
            if string_index == pad_max_row - 1 and color_char_index == pad_max_column - 1:
                # Curses doesn't want to put a character in the lower right corner
                continue
            pad.addch(string_index, color_char_index, color_char)

    return pad


def control(char: str) -> int:
    """Convert a letter to a control-key-esque version of itself."""
    if 'a' <= char <= 'z':
        return ord(char) - ord('a') + 1
    if 'A' <= char <= 'Z':
        return ord(char) - ord('A') + 1
    raise ValueError('char not a letter')


def linenos_to_search(current: int, maximum: int):
    """Generate line numbers for a search of text."""
    yield from range(current + 1, maximum)
    yield from range(0, current)


class DisplayData:
    """Hold and update information related to our curses screen and pad, including their dimensions."""

    def __init__(self, stdscr: Window, pad: Window, text: typing.List[str]) -> None:
        """Initialize."""
        self.row = 0
        self.column = 0

        self.pad_row = 2
        self.pad_column = 0

        self.text = text
        self.stdscr = stdscr

        (self.pad_max_row, self.pad_max_column) = pad.getmaxyx()

        self.update(self.stdscr, pad)

    def update(self, stdscr: Window, pad: Window) -> None:
        """Update the info."""
        self.pad_width, self.pad_height = stdscr.getmaxyx()
        self.pad_width = self.pad_width - self.pad_row - 2
        self.pad_height = self.pad_height - self.pad_column - 1

        self.half_pad_height = self.pad_height // 2

        pad.refresh(
            self.row,
            self.column,
            self.pad_row,
            self.pad_column,
            self.pad_row + self.pad_width,
            self.pad_column + self.pad_height,
        )
        stdscr.refresh()

    def forward_n_lines(self, n: int) -> None:
        """Advance the 'cursor' n lines forward."""
        self.row = min(self.pad_max_row - 1 - self.pad_width, self.row + n)

    def back_n_lines(self, n: int) -> None:
        """Move the 'cursor' n lines back."""
        self.row = max(0, self.row - n)

    def last_line(self) -> None:
        """Move the 'cursor' to the last line of the buffer."""
        self.row = self.pad_max_row - 1 - self.pad_height

    def last_char(self) -> None:
        """Move the 'cursor' to the last character of the line."""
        self.column = self.pad_max_column - 1 - self.pad_width

    def first_char(self) -> None:
        """Move the 'cursor' to the first character of the line."""
        self.column = 0

    def forward_n_chars(self, n: int) -> None:
        """Move the 'cursor' n characters to the right."""
        self.column = min(self.pad_max_column - 1 - self.pad_height, self.column + n)

    def back_n_chars(self, n: int) -> None:
        """Move the 'cursor' n characters to the left."""
        self.column = max(0, self.column - 1)

    def jump_to_line(self, n: int) -> None:
        """Set the 'cursor' to the nth line."""
        self.row = n


def scroll_pad(stdscr: Window, pad: Window, text: typing.List[str]):
    """Scroll around the pad."""
    stdscr.addstr(0, 0, 'You can navigate with the arrow keys, vi keys or emacs keys. Hit enter to select.')

    dd = DisplayData(stdscr, pad, text)

    iterationno = 0
    while True:
        stdscr.addstr(0, 0, f'At top of while loop: {iterationno}')
        iterationno += 1

        dd.update(stdscr, pad)

        ch = stdscr.getch()

        if ch in (curses.KEY_RESIZE, ):
            dd.update(stdscr, pad)
        elif ch in (ord('h'), curses.KEY_LEFT, control('b')):
            # dd.column = max(0, dd.column - 1)
            dd.back_n_chars(1)
        elif ch in (ord('l'), curses.KEY_RIGHT, control('f')):
            # dd.column = min(pad_max_column - 1 - dd.pad_height, dd.column + 1)
            dd.forward_n_chars(1)
        elif ch in (ord('j'), curses.KEY_DOWN, control('n')):
            dd.forward_n_lines(1)
        elif ch in (ord('k'), curses.KEY_UP, control('p')):
            # dd.row = max(0, dd.row - 1)
            dd.back_n_lines(1)
        elif ch in (ord('0'), control('a'), curses.KEY_HOME):
            # dd.column = 0
            dd.first_char()
        elif ch in (ord('$'), control('e'), curses.KEY_END):
            # dd.column = pad_max_column - 1 - dd.pad_height
            dd.last_char()
        elif ch in (ord('G'), ):
            # There are other common character sequences for this, but I don't think they're a single character.
            dd.last_line()
        elif ch in (ord('q'), 27, control('c')):
            # 27 is escape
            break
        elif ch in (ord(' '), curses.KEY_NPAGE, ord('d')):
            dd.forward_n_lines(dd.pad_height)
        elif ch in (curses.KEY_PPAGE, ord('u')):
            # dd.row = max(dd.row - dd.pad_height, 0)
            dd.back_n_lines(dd.pad_height)
        elif ch in (control('d'), ):
            dd.forward_n_lines(dd.half_pad_height)
        elif ch in (control('u'), ):
            # dd.row = max(dd.row - dd.half_pad_height, 0)
            dd.back_n_lines(dd.half_pad_height)
        elif ch in (ord('/'), control('s')):
            (physical_max_y, physical_max_x) = dd.stdscr.getmaxyx()
            dd.stdscr.move(physical_max_y - 1, 0)
            dd.stdscr.addch('/')
            curses.echo()
            search_string = dd.stdscr.getstr(physical_max_y - 1, 1, physical_max_x - 2).decode('utf-8')
            curses.noecho()

            dd.stdscr.move(physical_max_y - 1, 0)
            dd.stdscr.clrtoeol()

            for lineno in linenos_to_search(dd.row, len(dd.text)):
                line = dd.text[lineno]
                if search_string in line:
                    dd.jump_to_line(lineno)
                    dd.stdscr.move(physical_max_y - 1, 0)
                    break
        else:
            stdscr.addstr(0, 20, f'got mystery character {ch}')

        pass


def main() -> None:
    """Start the ball rolling."""
    pad_max_column = 200
    pad_max_row = 600

    stdscr = setup()

    text = circle(pad_max_row, pad_max_column)

    pad = put_stuff_on_pad(pad_max_row, pad_max_column, text)

    scroll_pad(stdscr, pad, text)

    teardown()


if __name__ == '__main__':
    main()