from functools import partial
from itertools import islice
from typing import Optional, List, Callable, Dict, Tuple, Any
from loguru import logger
from ..Attrs import NORMAL, BOLD, REVERSE
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, NORMAL)

def print_highlighted_line(x, text, screen, y):
    s = str(text).replace("\n", " ").replace("\r", "")
    screen.addstr(y, x, s, 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, 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, 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, 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)
    context["_rendered_start"] = start

    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, 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, REVERSE)
        else:
            screen.addstr(y, x, text, 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, 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, "[", BOLD)
    screen.addstr(y, x_position + 1, visible_text, NORMAL)

    bracket_attr = BOLD
    if is_focused and relative_position >= len(visible_text):
        bracket_attr = 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, REVERSE)

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

    if is_focused:
        cursor_screen_x = x_position + 1 + relative_position
        screen.request_cursor(y, min(cursor_screen_x, num_cols - 1))


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
