# algorithms will be implemnented here

from typing import List, Tuple, Dict, Set, Optional
from multiplayer_crosswords.dictionary import Dictionary, Word
from multiplayer_crosswords.utils import load_en_dictionary, load_de_dictionary
from copy import copy, deepcopy
import random
import os
from enum import Enum

class Orientation(Enum):
    HORIZONTAL = 1
    VERTICAL = 2

class Slot:
    def __init__(self, row: int, col: int, orientation: Orientation, length: int):
        self.row = row
        self.col = col
        self.orientation = orientation
        self.length = length
        self.id = (row, col, orientation)  # Unique identifier

    def __repr__(self):
        return f"Slot(row={self.row}, col={self.col}, orientation={self.orientation}, length={self.length})"

def extract_slots(grid: List[List[str]], min_length: int = 2) -> List[Slot]:
    """Extracts slots from the grid where words can be placed.
    
    A slot is a sequence of empty cells (not '#') either horizontally or vertically.

    Args:
        grid (List[List[str]]): 2D grid representing the crossword layout.
        min_length (int): Minimum length of a slot to be considered valid.
    Returns:
        List[Slot]: List of extracted slots.
    """

    rows, cols = len(grid), len(grid[0])
    slots = []

    # Horizontal slots
    for r in range(rows):
        c = 0
        while c < cols:
            while c < cols and grid[r][c] == '#':
                c += 1
            start = c
            while c < cols and grid[r][c] != '#':
                c += 1
            length = c - start
            if length >= min_length:
                slots.append(Slot(r, start, Orientation.HORIZONTAL, length))
    # Vertical slots
    for c in range(cols):
        r = 0
        while r < rows:
            while r < rows and grid[r][c] == '#':
                r += 1
            start = r
            while r < rows and grid[r][c] != '#':
                r += 1
            length = r - start
            if length >= min_length:
                slots.append(Slot(start, c, Orientation.VERTICAL, length))
    return slots

def slot_pattern(grid: List[List[str]], slot: Slot) -> str:
    """Generates a pattern string for a given slot in the grid.

    Args:
        grid (List[List[str]]): 2D grid representing the crossword layout.
        slot (Slot): The slot for which to generate the pattern.
    Returns:
        str: Pattern string with known letters and '*' for unknowns.
    """
    dr, dc = (0, 1) if slot.orientation == Orientation.HORIZONTAL else (1, 0)
    pattern = []
    for i in range(slot.length):
        r, c = slot.row + dr * i, slot.col + dc * i
        cell = grid[r][c]
        pattern.append(cell if cell and cell != '#' else '*')
    return ''.join(pattern)

def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, seed: Optional[int] = None, max_slot_length: int = 15, min_slot_length: int = 3) -> List[List[str]]:
    """Generates a grid template with blocks ('#') and empty cells (''). It will be rotationally symmetric.

    Args:
        width (int): Width of the grid.
        height (int): Height of the grid.
        block_ratio (float): Approximate ratio of blocks in the grid.
        seed (Optional[int]): Random seed for reproducibility.
        max_slot_length (int): Maximum length of any slot to avoid overly long slots.
        min_slot_length (int): Minimum length of any slot.
    Returns:
        List[List[str]]: Generated grid template.
    """

    rnd = random.Random()
    if seed is not None:
        rnd.seed(seed)

    # Initialize empty grid
    grid = [['' for _ in range(width)] for _ in range(height)]
    
    def is_connected(test_grid: List[List[str]]) -> bool:
        """Check if all empty cells are connected using flood fill"""
        rows, cols = len(test_grid), len(test_grid[0])
        visited = set()
        
        # Find first empty cell
        start = None
        for r in range(rows):
            for c in range(cols):
                if test_grid[r][c] == '':
                    start = (r, c)
                    break
            if start:
                break
        
        if not start:
            return True  # No empty cells
        
        # Flood fill from start position
        stack = [start]
        while stack:
            r, c = stack.pop()
            if (r, c) in visited or r < 0 or r >= rows or c < 0 or c >= cols or test_grid[r][c] == '#':
                continue
            visited.add((r, c))
            stack.extend([(r+1, c), (r-1, c), (r, c+1), (r, c-1)])
        
        # Count total empty cells
        empty_count = sum(1 for r in range(rows) for c in range(cols) if test_grid[r][c] == '')
        return len(visited) == empty_count

    def get_horizontal_length(r: int, c: int) -> int:
        if grid[r][c] == '#': return 0
        # look left
        lc = c
        while lc >= 0 and grid[r][lc] != '#':
            lc -= 1
        # look right
        rc = c
        while rc < width and grid[r][rc] != '#':
            rc += 1
        return (rc - 1) - (lc + 1) + 1

    def get_vertical_length(r: int, c: int) -> int:
        if grid[r][c] == '#': return 0
        # look up
        ur = r
        while ur >= 0 and grid[ur][c] != '#':
            ur -= 1
        # look down
        dr = r
        while dr < height and grid[dr][c] != '#':
            dr += 1
        return (dr - 1) - (ur + 1) + 1

    def check_valid_lengths_around(r: int, c: int) -> bool:
        """Check if placing a block at (r, c) creates any slots of invalid length (1 < length < min_slot_length)"""
        # grid[r][c] is assumed to be '#'
        
        # Horizontal Left
        if c > 0 and grid[r][c-1] != '#':
            start_c = c - 1
            while start_c >= 0 and grid[r][start_c] != '#':
                start_c -= 1
            length = (c - 1) - start_c
            if 1 < length < min_slot_length:
                return False

        # Horizontal Right
        if c < width - 1 and grid[r][c+1] != '#':
            end_c = c + 1
            while end_c < width and grid[r][end_c] != '#':
                end_c += 1
            length = end_c - (c + 1)
            if 1 < length < min_slot_length:
                return False

        # Vertical Up
        if r > 0 and grid[r-1][c] != '#':
            start_r = r - 1
            while start_r >= 0 and grid[start_r][c] != '#':
                start_r -= 1
            length = (r - 1) - start_r
            if 1 < length < min_slot_length:
                return False
                
        # Vertical Down
        if r < height - 1 and grid[r+1][c] != '#':
            end_r = r + 1
            while end_r < height and grid[end_r][c] != '#':
                end_r += 1
            length = end_r - (r + 1)
            if 1 < length < min_slot_length:
                return False
                
        return True
    
    def can_place_block(r: int, c: int) -> bool:
        """Check if we can place a block at (r,c) while maintaining symmetry and connectivity"""
        sym_r, sym_c = height - 1 - r, width - 1 - c
        
        # Can't place if positions are already blocked
        if grid[r][c] == '#' or grid[sym_r][sym_c] == '#':
            return False
        
        # Temporarily place blocks
        grid[r][c] = '#'
        grid[sym_r][sym_c] = '#'
        
        possible = True
        # Check connectivity
        if not is_connected(grid):
            possible = False
        
        # Check slot lengths
        if possible and not check_valid_lengths_around(r, c):
             possible = False
             
        if possible and (r != sym_r or c != sym_c):
            if not check_valid_lengths_around(sym_r, sym_c):
                possible = False
        
        # Restore original state
        grid[r][c] = ''
        grid[sym_r][sym_c] = ''
        
        return possible
    
    def place_block_permanently(r: int, c: int):
        """Place block at (r,c) and its symmetric position"""
        grid[r][c] = '#'
        grid[height - 1 - r][width - 1 - c] = '#'
    
    # Calculate target number of blocks
    total_cells = width * height
    target_blocks = int(total_cells * block_ratio)
    
    # Create candidate positions for the upper half of the grid
    # We only work with the upper half due to rotational symmetry
    candidates = []
    for r in range((height + 1) // 2):
        for c in range(width):
            # For center row (if height is odd), only consider left half + center
            if r == height // 2 and height % 2 == 1:
                if c <= width // 2:
                    candidates.append((r, c))
            else:
                candidates.append((r, c))
    
    rnd.shuffle(candidates)
    
    # Place blocks
    blocks_placed = 0
    for r, c in candidates:
        if blocks_placed >= target_blocks:
            break
            
        if can_place_block(r, c):
            place_block_permanently(r, c)
            # Count blocks added (1 if center position, 2 otherwise)
            sym_r, sym_c = height - 1 - r, width - 1 - c
            blocks_added = 1 if (r == sym_r and c == sym_c) else 2
            blocks_placed += blocks_added
    
    # Break long slots by strategically placing additional blocks
    for _ in range(3):  # Limited iterations to prevent infinite loops
        slots = extract_slots(grid)
        long_slots = [s for s in slots if s.length > max_slot_length]
        
        if not long_slots:
            break
            
        # Sort by length (longest first) to prioritize breaking worst offenders
        long_slots.sort(key=lambda x: x.length, reverse=True)
        
        for slot in long_slots:
            # Try to break the slot in the middle
            break_pos = slot.length // 2
            
            if slot.orientation == Orientation.HORIZONTAL:
                r, c = slot.row, slot.col + break_pos
            else:  # VERTICAL
                r, c = slot.row + break_pos, slot.col
            
            # Only break if we can maintain connectivity and symmetry
            if can_place_block(r, c):
                place_block_permanently(r, c)
                break  # Break one slot at a time and re-evaluate

    return grid

class CrosswordGeneratorStep(object):

    @property
    def grid(self) -> List[List[str]]:
        return self._grid 

    def __init__(
            self,
            dictionary: Dictionary,
            grid: Optional[List[List[str]]] = None,
            known_slots: Optional[List[Slot]] = None,
            field_slotindex_map_hor: Optional[List[List[int]]] = None,
            field_slotindex_map_ver: Optional[List[List[int]]] = None,
            seed: float | int | None = None,
            grid_width: int | None = None,
            grid_height: int | None = None,
            grid_block_ratio: float = 0.25,
            max_slot_length: int = None,
            min_slot_length: int = 3,
            available_words_for_slotindex: Optional[Dict[int, Set[Word]]] = None,
            unfilled_slots: Optional[Set[int]] = None,
            rnd=None,
        ):
        self._dictionary = dictionary
        self._grid   = grid
        self._known_slots = known_slots
        self._field_slotindex_map_hor = field_slotindex_map_hor
        self._field_slotindex_map_ver = field_slotindex_map_ver
        self._seed = seed
        self._unfilled_slots = unfilled_slots

        if self._seed is None:
            self._seed = random.randint(0, 2**31 - 1)

        if rnd is None:
            self._rnd = random.Random()
            self._rnd.seed(self._seed)
        else:
            self._rnd = rnd

        
        
        


        self._grid_width = grid_width
        self._grid_height = grid_height 
        self._grid_block_ratio = grid_block_ratio
        if max_slot_length is None:
            max_slot_length = min(max(grid_width, grid_height) // 2 + max(grid_width, grid_height) + 4, 20)
        self._max_slot_length = max_slot_length
        self._min_slot_length = min_slot_length
        self._available_words_for_slotindex = available_words_for_slotindex
        

        if self._grid is None:
            if grid_width is None or grid_height is None:
                raise ValueError("If no grid is provided, grid_width and grid_height must be specified.")
            # create a default grid if none is provided
            self._grid = generate_grid_template(width=grid_width,
                                                                height=grid_height,
                                                                block_ratio=grid_block_ratio,
                                                                seed=self._seed,
                                                                max_slot_length=self._max_slot_length,
                                                                min_slot_length=self._min_slot_length)
        
        if self._known_slots is None:
            self._known_slots = extract_slots(self._grid, min_length=self._min_slot_length)
        if self._field_slotindex_map_hor is None:
            self._field_slotindex_map_hor = [[-1 for _ in range(len(self._grid[0]))] for _ in range(len(self._grid))]
            for idx, slot in enumerate(self._known_slots):
                if slot.orientation != Orientation.HORIZONTAL:
                    continue
                dr, dc = (0, 1)
                for i in range(slot.length):
                    r, c = slot.row + dr * i, slot.col + dc * i
                    self._field_slotindex_map_hor[r][c] = idx
        if self._field_slotindex_map_ver is None:
            self._field_slotindex_map_ver = [[-1 for _ in range(len(self._grid[0]))] for _ in range(len(self._grid))]
            for idx, slot in enumerate(self._known_slots):
                if slot.orientation != Orientation.VERTICAL:
                    continue
                dr, dc = (1, 0) 
                for i in range(slot.length):
                    r, c = slot.row + dr * i, slot.col + dc * i
                    self._field_slotindex_map_ver[r][c] = idx

        if self._available_words_for_slotindex is None:
            self._available_words_for_slotindex = {}
            for idx, slot in enumerate(self._known_slots):
                pattern = slot_pattern(self._grid, slot)
                matching_words = self._dictionary.find_by_pattern(pattern)
                self._available_words_for_slotindex[idx] = matching_words
        
        if self._unfilled_slots is None:
            # initialize with all slots unfilled
            self._unfilled_slots = set(range(len(self._known_slots)))

    
    def copy(self) -> "CrosswordGeneratorStep":
        return CrosswordGeneratorStep(
            dictionary=self._dictionary,
            grid=[row.copy() for row in self._grid],
            known_slots=self._known_slots.copy(),
            field_slotindex_map_hor=[row.copy() for row in self._field_slotindex_map_hor] if self._field_slotindex_map_hor else None,
            field_slotindex_map_ver=[row.copy() for row in self._field_slotindex_map_ver] if self._field_slotindex_map_ver else None,
            seed=self._seed,
            grid_width=self._grid_width,
            grid_height=self._grid_height,
            grid_block_ratio=self._grid_block_ratio,
            max_slot_length=self._max_slot_length,
            min_slot_length=self._min_slot_length,
            available_words_for_slotindex={k: v.copy() for k, v in self._available_words_for_slotindex.items()} if self._available_words_for_slotindex else None,
            unfilled_slots=self._unfilled_slots.copy(),
            rnd=self._rnd,
        )
    
    def fill_next_slot(self) -> bool:
        if len(self._unfilled_slots) == 0:
            return True  # All slots filled, everything done

        # choose slot randomly from all slots with minimal available words
        min_num_words = min(len(self._available_words_for_slotindex.get(idx, [])) for idx in self._unfilled_slots)
        

        candidates = [idx for idx in self._unfilled_slots if len(self._available_words_for_slotindex.get(idx, [])) == min_num_words ]
        
        slot_to_fill = self._rnd.choice(candidates)
        available_words = list(self._available_words_for_slotindex.get(slot_to_fill, []))

        if len(available_words) == 0:
            return False  # No available words for this slot, dead end
        
        # choose a random word from available words
        chosen_word = self._rnd.choice(available_words)
        slot = self._known_slots[slot_to_fill]

        # update own slot
        self._available_words_for_slotindex[slot_to_fill] = {chosen_word}

        # Place the word in the grid
        dr, dc = (0, 1) if slot.orientation == Orientation.HORIZONTAL else (1, 0)
        for i in range(slot.length):
            r, c = slot.row + dr * i, slot.col + dc * i
            self._grid[r][c] = chosen_word.word[i]
        self._unfilled_slots.remove(slot_to_fill)

        # Update available words for intersecting slots
        for i in range(slot.length):
            r, c = slot.row + dr * i, slot.col + dc * i
            # Check horizontal slot
            hor_slot_idx = self._field_slotindex_map_hor[r][c]
            if hor_slot_idx != -1 and hor_slot_idx in self._unfilled_slots:
                pattern = slot_pattern(self._grid, self._known_slots[hor_slot_idx])
                matching_words = self._dictionary.find_by_pattern(pattern)
                 # Dead end, no words fit anymore
                self._available_words_for_slotindex[hor_slot_idx] = set(matching_words)
                if len(matching_words) == 0:
                    return False 
            # Check vertical slot
            ver_slot_idx = self._field_slotindex_map_ver[r][c]
            if ver_slot_idx != -1 and ver_slot_idx in self._unfilled_slots:
                pattern = slot_pattern(self._grid, self._known_slots[ver_slot_idx])
                matching_words = self._dictionary.find_by_pattern(pattern)
                
                self._available_words_for_slotindex[ver_slot_idx] = set(matching_words)
                if len(matching_words) == 0:
                    return False  # Dead end, no words fit anymore
        return True

    def generate(self,
                 max_tries_per_step: int = 10,
                 max_allowed_single_choice: int = 1,
                 max_allowed_threshold_choice: int = 3,
                 max_allowed_threshold: int = 3,
                 show_progress: bool = False
            ) -> Optional["CrosswordGeneratorStep"]:
        # count how many slots have only one available word
        single_choice_slots = sum(1 for idx in self._unfilled_slots if len(self._available_words_for_slotindex.get(idx, [])) == 1)
        if single_choice_slots > max_allowed_single_choice:
            return None  # Too many single-choice slots, backtrack
        # count how many slots have only less than threshold available words
        threshold_choice_slots = sum(1 for idx in self._unfilled_slots if len(self._available_words_for_slotindex.get(idx, [])) <= max_allowed_threshold)
        if threshold_choice_slots > max_allowed_threshold_choice:
            return None  # Too many threshold-choice slots, backtrack
        for i in range(max_tries_per_step if single_choice_slots == 0 else 1):
            step_copy = self.copy()
            if step_copy.fill_next_slot():
                if show_progress:
                    self.print_grid()
                if len(step_copy._unfilled_slots) == 0:
                    return step_copy  # Successfully filled all slots
                else:
                    next_step = step_copy.generate(
                        max_tries_per_step,
                        max_allowed_single_choice=max_allowed_single_choice,
                        max_allowed_threshold_choice=max_allowed_threshold_choice,
                        max_allowed_threshold=max_allowed_threshold,
                        show_progress=show_progress
                    )
                    if next_step is not None:
                        return next_step
                    else:
                        if show_progress:
                            self.print_grid()
                        #pass
            else:
                #self.print_grid()
                pass
        return None  # Failed to fill a slot after max tries
    
    def print_grid(self):
        # clear as many rows in terminal as we will print

        os.system('cls' if os.name == 'nt' else 'clear')

        result = ""
        for row in self._grid:
            # print on stdout dircectly and only flush in the end
            row_str = ' '.join(cell if cell else ' ' for cell in row).replace("#", "█") + "\n"
            row_str = row_str.replace("█ █", "███").replace("█ █", "███")
            #row_str = row_str.replace(" █", "██")
            result += row_str
        
        print(result, flush=True)
    
    def get_horizontal_slots(self) -> List[Slot]:
        return [slot for slot in self._known_slots if slot.orientation == Orientation.HORIZONTAL]
    def get_vertical_slots(self) -> List[Slot]:
        return [slot for slot in self._known_slots if slot.orientation == Orientation.VERTICAL]

    def get_grid(self) -> List[List[str]]:
        return self._grid

            
    def __str__(self):
        # Simple string representation for debugging
        result = "Crossword Grid:\n"
        for row in self._grid:
            result += ' '.join(cell if cell else ' ' for cell in row).replace("#", ".") + "\n"

        #return result
        result += "\n"
        result += f"Horizontal Slot Map:\n"
        for r in range(len(self._field_slotindex_map_hor)):
            result += ' '.join(f"{idx:2d}" if idx != -1 else " ." for idx in self._field_slotindex_map_hor[r]) + "\n"

        result += f"Vertical Slot Map:\n"
        for r in range(len(self._field_slotindex_map_ver)):
            result += ' '.join(f"{idx:2d}" if idx != -1 else " ." for idx in self._field_slotindex_map_ver[r]) + "\n"

        result += "Slots:\n"
        for i, slot in enumerate(self._known_slots):
            result += f"{i}: {slot} Pattern: {slot_pattern(self._grid, slot)} -- "
            result += f"Available Words: {len(self._available_words_for_slotindex.get(i, []))}\n"
        return result


if __name__ == "__main__":

    dummy_words = ["apple", "banana", "grape", "orange", "melon", "kiwi", "peach", "pear", "plum", "mango",
                   "cherry", "berry", "lemon", "lime", "apricot", "date", "fig", "papaya", "quince", "tangerine"]

    dict_obj = load_de_dictionary()

    dict_obj._build_pos_index_list()

    test_seeds = [42]
    for seed in test_seeds:
        print(f"Testing with seed {seed}")
        generator = CrosswordGeneratorStep(dictionary=dict_obj,
                                          seed=seed,
                                          grid_width=40,
                                          grid_height=20,
                                          grid_block_ratio=0.41,
                                          min_slot_length=3)
        final_step = generator.generate(
            max_tries_per_step=2,
            show_progress=True,
            max_allowed_single_choice=1,
            max_allowed_threshold_choice=3,
            max_allowed_threshold=5
        )
        if final_step is None:
            print("Failed to generate crossword. Last attempt:")
            print(generator)
        else:
            
            final_step.print_grid()
            print("Successfully generated crossword")

    #grid = generate_grid_template(width=15, height=15, block_ratio=0.25, seed=42, min_slot_length=4)
    # print grid
    #for row in grid: 
    #    print(' '.join(cell if cell else ' ' for cell in row).replace("#", "█").replace(" ", ".")) 


