import curses
from functools import partial
from itertools import islice
from typing import Optional, List, Callable, Dict, Tuple, Any
from loguru import logger
from ..ContextUtils import get_items_list, get_text, get_cursor_index, get_x

def print_empty_line(screen, y):
    pass

def print_line(x, text, screen, y):
    s = str(text).replace("\n", " ").replace("\r", "")
    screen.addstr(y, x, s, curses.A_NORMAL)

def print_highlighted_line(x, text, screen, y):
    s = str(text).replace("\n", " ").replace("\r", "")
    screen.addstr(y, x, s, curses.A_REVERSE)

def print_bold_line(x, text, screen, y):
    _, num_cols = screen.getmaxyx()
    s = str(text).replace("\n", " ").replace("\r", "")
    s = s[: max(0, num_cols - x - 1)]
    screen.addstr(y, x, s, curses.A_BOLD)


def make_horizontal_bar(context, remaining_height) -> List[callable]:
    """Returns a list of printer functions that create a horizontal bar."""
    def print_horizontal_bar(screen, y):
        _, num_cols = screen.getmaxyx()
        screen.addstr(y, 0, "-" * num_cols, curses.A_NORMAL)
    
    return [print_horizontal_bar]

def make_bottom_bar(context, remaining_height):
    def print_bottom_bar(screen, y) -> int:
        items = [text for key, text in context["items"].items()]

        text = " | ".join(items)
        screen.addstr(y, 0, text, curses.A_BOLD)

    return [print_empty_line, print_bottom_bar]

def make_spacer(context, remaining_height):
    """
        Fills up the remaining space with empty lines
        Useful for shoving ui bits to the bottom of the screen
    """
    return [print_empty_line for _ in range(remaining_height)]

def start_stop(index, window_size, list_size):
    """calculates the start and stop indices for visible items in the scrolling list."""

    up = window_size // 2
    down = window_size // 2 - (window_size % 2 == 0)  # Adjust for even window sizes

    # Check if topped out
    if index - up < 0:
        return 0, min(window_size, list_size)

    # Check if bottomed out
    if index + down >= list_size:
        return max(0, list_size - window_size), list_size

    # Normal case
    return max(0, index - up), min(list_size, index + down + 1)

def cut_items_to_window(selected_index, items, window_size):
    """determines which items from the given list should be visible in
    the scrollable list, based on the selected index and the window size."""
    start, stop = start_stop(selected_index, window_size, len(items))
    return start, stop, islice(items, start, stop)

def default_item_printer(screen, y_index, item, mode):
    screen.addstr(y_index, 0, str(item), mode)

def tuple_item_printer(screen, y_index, item, mode):
    screen.addstr(y_index, 0, str(item[0]), mode)

class WrappedScrollList:
    def __init__(self, screen):
        self.screen = screen
        self.wrapped_lines = []
        self.item_line_map = {}
        self.last_items = None
        self.num_cols = None

    def __call__(self, context: Dict, remaining_height: int) -> List[Callable]:
        """
        Creates a scrollable list with wrapped lines and selected item highlighting.

        Args:
            context: The context dictionary containing list items and UI state.
            remaining_height: The available height for the list.

        Returns:
            A list of callable print functions to render the list.
        """
        num_rows, num_cols = self.screen.getmaxyx()
        selected_index = context.get("selected_index", 0)
        is_focused = get_is_focused(context)
        items = context.get("items", [])

        # Check if items or screen width has changed
        if items != self.last_items or self.num_cols != num_cols or not self.wrapped_lines:
            self.last_items = items
            self.num_cols = num_cols
            self.wrapped_lines = []
            self.item_line_map = {}
            line_counter = 0

            for i, item in enumerate(items):
                wrapped_lines = self._wrap_text(str(item), num_cols)
                self.item_line_map[i] = (line_counter, len(wrapped_lines))  # store the line number and length for indexing
                self.wrapped_lines.extend(wrapped_lines)
                line_counter += len(wrapped_lines)

        start, stop, visible_lines = self.cut_wrapped_items_to_window(selected_index, self.wrapped_lines, remaining_height)

        print_items = []
        for line_index, line in zip(range(start, stop), visible_lines):
            item_index = self._find_item_index_for_line(line_index)  # finds the item index from the line number
            if item_index == -1:
                continue

            start_line, num_lines = self.item_line_map[item_index] # finds the starting line and num lines for the selected item.

            # only highlights a line if it's part of the selected item, and the application has focus.
            if item_index == selected_index and is_focused and start_line <= line_index < start_line + num_lines:
                print_items.append(partial(print_highlighted_line, 0, line))
            else:
                print_items.append(partial(print_line, 0, line))
        return print_items
    
    def cut_wrapped_items_to_window(self, selected_index, items, window_size):
        """
        Determines which items from the wrapped lines should be visible in
        the scrollable list, based on the selected index and the window size.
        """
        start, stop = self.start_stop(selected_index, window_size, len(items))
        return start, stop, islice(items, start, stop)

    def _wrap_text(self, text: str, width: int) -> List[str]:
        """Wraps text to the specified width."""
        lines = []
        words = text.split()
        current_line = ""
        for word in words:
            if len(current_line) + len(word) + 1 <= width:
                current_line += (word + " ")
            else:
                lines.append(current_line.rstrip())
                current_line = word + " "
        lines.append(current_line.rstrip())
        return lines

    def _find_item_index_for_line(self, line_index) -> int:
        """Determines the item index for the given line number"""
        for item_index, (start_line, num_lines) in self.item_line_map.items():
            if start_line <= line_index < start_line + num_lines:
                return item_index
        return -1

    def start_stop(self, index, window_size, list_size):
        """Calculates start and stop indices for visible items in the scrolling list,
        taking into account the number of lines consumed by each item"""

        if window_size % 2 == 0:
            up = window_size//2
            down = window_size//2 - 1
        else:
            up = window_size//2
            down = window_size//2

        # if topped out
        if index - up < 0:
            return 0, min(window_size, list_size)
        # if bottomed out
        elif index + down > list_size:
            return max(0, list_size-window_size), list_size
        else:
            return index - up, index + down+1  

def make_scroll_list(screen, context, remaining_height) -> List[callable]:
    num_rows, num_cols = screen.getmaxyx()
    selected_index = context.get("selected_index", 0)
    is_focused = get_is_focused(context)

    start, stop, visible_items = cut_items_to_window(selected_index, context["items"], remaining_height)

    print_items = []
    for index, item in zip(range(start, stop), visible_items):
        text = str(item)
        sanitized = text[:num_cols]
        if index == selected_index and is_focused:
            print_items.append(partial(print_highlighted_line, 0, sanitized))
        else:
            is_checked = text.lstrip().startswith("[x]")
            if is_checked:
                print_items.append(partial(print_bold_line, 0, sanitized))
            else:
                print_items.append(partial(print_line, 0, sanitized))

    return print_items


def print_context_menu(context, screen, y):
    selected_index = context.get("selected_index", 0)
    x = context.get("x", 0)

    if 'label' in context:
        label = f"{context['label']}: "
    else:
        label = ""
        
    screen.addstr(y, x, label, curses.A_BOLD)
    x += len(label)

    for index in range(len(context["items"])):
        item = context["items"][index]
        text = f"[{item['text']}]"
        if index == selected_index:
            screen.addstr(y, x, text, curses.A_REVERSE)
        else:
            screen.addstr(y, x, text, curses.A_NORMAL)

        x += len(text) + 1

def make_context_menu(context, remaining_height=0) -> List[callable]:
    return [partial(print_context_menu, context),
            print_empty_line]

def get_is_focused(context) -> bool:
    return context.get("focused", False)

def print_text_input(x, context, screen, y):
    cursor_index = get_cursor_index(context)
    text = str(get_text(context) or "")
    is_focused = get_is_focused(context)

    label_text = f"{context.get('label', '')}: "
    num_rows, num_cols = screen.getmaxyx()

    x_position = x
    screen.addstr(y, x_position, label_text, curses.A_BOLD)
    x_position += len(label_text)

    available_width = max(1, num_cols - x_position - 2)

    view_offset = context.get("view_offset", 0)
    cursor_index = max(0, min(cursor_index, len(text)))

    if cursor_index < view_offset:
        view_offset = cursor_index
    elif cursor_index > view_offset + available_width:
        view_offset = cursor_index - available_width
    context["view_offset"] = view_offset

    visible_text = text[view_offset:view_offset + available_width]
    relative_position = cursor_index - view_offset

    screen.addstr(y, x_position, "[", curses.A_BOLD)
    screen.addstr(y, x_position + 1, visible_text, curses.A_NORMAL)

    bracket_attr = curses.A_BOLD
    if is_focused and relative_position >= len(visible_text):
        bracket_attr = curses.A_REVERSE
    closing_x = x_position + 1 + len(visible_text)
    screen.addstr(y, closing_x, "]", bracket_attr)

    if is_focused and 0 <= relative_position < len(visible_text):
        ch = visible_text[relative_position]
        screen.addstr(y, x_position + 1 + relative_position, ch, curses.A_REVERSE)

    clear_x = min(num_cols - 1, closing_x + 1)
    screen.move(y, clear_x)
    screen.clrtoeol()


def make_text_input(context, remaining_height):
    if context.get("hidden", False) is True:
        return []
    return [partial(print_text_input, 0, context)]


def print_text_line(screen, context, start_index, remaining_height):
    screen.addstr(start_index, get_x(context), context["text"])

    return context["fixed_size"]


def make_text_editor(screen, context, remaining_height) -> List[callable]:
    num_rows, num_cols = screen.getmaxyx()

    selected_index = context.get("selected_index", 0)
    is_focused = get_is_focused(context)

    context["items"] = context["text"].split("\n")

    start, stop, visible_items = cut_items_to_window(selected_index, get_items_list(context), remaining_height)

    print_items = []
    for index, item in zip(range(start, stop), visible_items):
        sanitized = str(item)[:num_cols]
        if index == selected_index and is_focused:
            print_items.append(partial(print_highlighted_line, 0, sanitized))
        else:
            print_items.append(partial(print_line, 0, sanitized))

    return print_items

def make_multiline_text(context, remaining_height):
    printers = []

    for line in context["lines"]:
        printers.append(partial(print_line, 0, line))

    return printers
