from enum import Enum
from typing import Dict, List, Tuple, Optional
from multiplayer_crosswords.dictionary import Dictionary, Word
from multiplayer_crosswords.crossword_algorithm import CrosswordGeneratorStep, Slot, Orientation, extract_slots
import random
import logging

logger = logging.getLogger(__name__)

from multiplayer_crosswords.utils import load_de_dictionary

class CrosswordWord:

    def __init__(
            self,
            word: str,
            start_x: int,
            start_y: int,
            orientation: Orientation,
            hint: str,
            index: Optional[int],
            solved: bool = False,

        ):
            self.word = word
            self.start_x = start_x
            self.start_y = start_y
            self.orientation = orientation
            self.hint = hint
            self.length = len(word)
            self.index: Optional[int] = index
            self.solved = solved

    
class Crossword:

    @property
    def solved_grid(self) -> List[List[Optional[str]]]:
        return self._solved_grid 

    @property
    def dictionary(self) -> Dictionary:
        return self._dictionary 
    
    @property
    def words(self) -> List[CrosswordWord]:
        return self._words

    @property
    def current_grid(self) -> List[List[Optional[str]]]:
        return self._current_grid

    @property
    def solution_word_positions(self) -> Optional[List[Tuple[int, int]]]:
        return self._solution_word_positions
    

    def get_words_by_y_x_position(self, x, y) -> List[CrosswordWord]:
        """Get the list of CrosswordWord objects that start at position (x, y)."""
        result = []
        horizontal_cw = self._horizontal_words_by_y_x_position.get((y, x))
        if horizontal_cw is not None:
            result.append(horizontal_cw)
        vertical_cw = self._vertical_words_by_y_x_position.get((y, x))
        if vertical_cw is not None:
            result.append(vertical_cw)
        return result
    
    @classmethod
    def from_serialized(cls, data: dict, dictionary: Dictionary) -> "Crossword":
        """Create a Crossword object from serialized data."""
        grid = data.get("solved_grid", [])

        words: List[CrosswordWord] = []
        for wdata in data.get("words", []):
            cw = CrosswordWord(
                word=wdata.get("word"),
                start_x=wdata.get("start_x"),
                start_y=wdata.get("start_y"),
                orientation=Orientation[wdata.get("orientation")],
                hint=wdata.get("hint", ""),
                index=wdata.get("index", None),
                solved=wdata.get("solved", False)
            )
            words.append(cw)

        crossword = cls(
            dictionary=dictionary,
            grid=grid,
            current_grid=data.get("current_grid", None),
            words=words
        )

        return crossword 

    @staticmethod
    def generate(
        dictionary: Dictionary,
        grid_width: int = 15, 
        grid_height: int = 15,
        grid_block_ratio: float = 0.4,
        seed: Optional[int] = None, 

    ) -> "Crossword": 
        
        if seed is None:
            # initialize seed based on current system time
            seed = random.randint(0, 2**32 - 1) 
        generator = CrosswordGeneratorStep(
            grid_width=grid_width,
            grid_height=grid_height,
            dictionary=dictionary,
            grid_block_ratio=grid_block_ratio,
            seed=seed
        )
        final_step = generator.generate(
            max_tries_per_step=2,
            max_allowed_single_choice=1,
            max_allowed_threshold_choice=3,
            max_allowed_threshold=5,
            show_progress=False
        )

        if final_step is None:
            logger.warning("Crossword generation failed for grid size %dx%d with block ratio %.2f", grid_width, grid_height, grid_block_ratio)
            return None

        logger.info("Crossword generated successfully for grid size %dx%d with block ratio %.2f", grid_width, grid_height, grid_block_ratio)

        # Build letter position index for efficient lookup
        letter_to_positions: Dict[str, List[Tuple[int, int]]] = {}
        for r in range(len(final_step.grid)):
            for c in range(len(final_step.grid[0])):
                cell = final_step.grid[r][c]
                if cell and cell != '#':
                    if cell not in letter_to_positions:
                        letter_to_positions[cell] = []
                    letter_to_positions[cell].append((r, c))

        # Now find a solution word: generate a random word from the dictionary (with 10-20 letters)
        # and try to find the necessary letter positions in the grid. if we fail we repeat with another word (max 10 tries)
        solution_word_positions: Optional[List[Tuple[int, int]]] = None
        max_solution_word_attempts = 20
        for i in range(max_solution_word_attempts):
            random_length = random.randint(10, 27 - (i // 2))
            possible_words = dictionary.find_by_pattern('*' * random_length)
            if not possible_words:
                continue
            
            chosen_word = random.choice(possible_words)
            letter_positions = []
            used_positions = set()
            
            for letter in chosen_word.word:
                if letter not in letter_to_positions:
                    letter_positions = []
                    break
                
                # Pick random position for this letter that's not already used
                available = [p for p in letter_to_positions[letter] if p not in used_positions]
                if not available:
                    letter_positions = []
                    break
                
                chosen_position = random.choice(available)
                letter_positions.append(chosen_position)
                used_positions.add(chosen_position)
            
            if len(letter_positions) == random_length:
                solution_word_positions = letter_positions
                break
        
        if solution_word_positions is None:
            logger.warning("Failed to find a solution word for the generated crossword after %d attempts", max_solution_word_attempts)
            return None

        cw = Crossword(
            dictionary=dictionary,
            grid=final_step.grid,
            solution_word_positions=solution_word_positions
        )

        logger.debug("Generated Crossword: \n\n%s", cw)


        return cw
    
 
    def __init__(
            self,
            dictionary: Dictionary, 
            grid: List[List[Optional[str]]],
            current_grid: Optional[List[List[Optional[str]]]] = None,
            words: Optional[List[CrosswordWord]] = None,
            solution_word_positions: Optional[List[Tuple[int, int]]] = None,
        ): 
            """
            Initialize a Crossword object.

            Args:
                dictionary (Dictionary): The dictionary containing words and hints.
                grid (List[List[Optional[str]]]): The solved crossword grid.
                current_grid (Optional[List[List[Optional[str]]]]): The current state of the crossword grid.
                words (Optional[List[CrosswordWord]]): Pre-extracted list of CrosswordWord objects.
                solution_word_positions (Optional[List[Tuple[int, int]]]): Positions of letters building the solution word.
            """

            self._dictionary = dictionary
            self._solved_grid = grid
            self._words: List[CrosswordWord] = []

            self._horizontal_words_by_y_x_position = {}
            self._vertical_words_by_y_x_position = {}

            self._solution_word_positions = solution_word_positions

            if current_grid is not None: 
                self._current_grid = current_grid
            
            else:
                # create an empty grid (empty = all letter cells are '')
                self._current_grid = [['' for _ in range(len(grid[0]))] for _ in range(len(grid))]

            # place walls in the current grid
            for r in range(len(grid)):
                for c in range(len(grid[0])):
                    if grid[r][c] == '#':
                        self._current_grid[r][c] = '#'
            
            if words is not None:
                self._words = words
            else:
                self.extract_words()

            if current_grid is not None:
                # current grid was passed, check which words are already solved
                for cw in self._words:
                    cw.solved = self.check_word_solved(cw)
            
            for cw in self._words:
                if cw.orientation == Orientation.HORIZONTAL:
                    for i in range(cw.length):
                        self._horizontal_words_by_y_x_position[(cw.start_y, cw.start_x + i)] = cw
                else:
                    for i in range(cw.length):
                        self._vertical_words_by_y_x_position[(cw.start_y + i, cw.start_x)] = cw

    def get_clues(self) -> Tuple[Dict[int, str], Dict[int, str]]:
        """Get the clues for horizontal and vertical words.

        Returns:
            Tuple[Dict[int, str], Dict[int, str]]: Two dictionaries,
            each containing (index, hint) for horizontal and vertical words respectively.
        """
        clues_across = {}
        clues_down = {}

        for word in self._words:
            if word.orientation == Orientation.HORIZONTAL:
                clues_across[str(word.index)] = word.hint
            else:
                clues_down[str(word.index)] = word.hint

        return clues_across, clues_down

    def get_clue_positions(self) -> Tuple[Dict[int, Tuple[int, int]], Dict[int, Tuple[int, int]]]:
        """Get the starting positions for horizontal and vertical clues.

        Returns:
            Tuple[Dict[int, Tuple[int, int]], Dict[int, Tuple[int, int]]]: Two dictionaries,
            each containing (index, (x, y)) for horizontal and vertical words respectively.
        """
        positions_across = {}
        positions_down = {}

        for word in self._words:
            if word.orientation == Orientation.HORIZONTAL:
                positions_across[str(word.index)] = (word.start_x, word.start_y)
            else:
                positions_down[str(word.index)] = (word.start_x, word.start_y)

        return positions_across, positions_down

    def extract_words(self):
        """Extract all fully placed words from the grid and store them in self._words.

        A "placed" word is a slot (horizontal or vertical, length >= 2) where every
        cell contains a letter (not '' and not '#'). For each found word we try to
        retrieve a random hint from the dictionary (using Dictionary.Word.copy_with_random_hint)
        and store it on the CrosswordWord.hint field (empty string if no hint found).
        """
        self._words = []

        # Use extract_slots to find candidate slots (min_length=2)
        slots = extract_slots(self._solved_grid, min_length=2)

        word_index = 1

        for slot in slots:
            # Build the word and check all cells are filled with letters
            dr, dc = (0, 1) if slot.orientation == Orientation.HORIZONTAL else (1, 0)
            chars = []
            fully_filled = True
            for i in range(slot.length):
                r, c = slot.row + dr * i, slot.col + dc * i
                cell = self._solved_grid[r][c]
                # treat '#' or empty string as not filled
                if not cell or cell == '#':
                    fully_filled = False
                    break
                chars.append(cell)

            if not fully_filled:
                continue

            word_str = ''.join(chars)

            # Find the Word object in the dictionary (if available) to extract a random hint
            hint_str = ""
            try:
                match = next((w for w in self._dictionary.words if w.word == word_str), None)
                if match is not None:
                    hint_word = match.copy_with_random_hint()
                    hint_str = hint_word.hints[0] if hint_word.hints else ""
            except Exception:
                # Fallback: ignore dictionary problems and leave hint empty
                
                hint_str = "ERROR while generating hint for word " + word_str 
            
            if len(hint_str) == 0:
                hint_str = "No hint available for this word: " + word_str 

            # start_x/start_y: choose x=col, y=row (cartesian-like)
            cw = CrosswordWord(
                word=word_str,
                start_x=slot.col,
                start_y=slot.row,
                orientation=slot.orientation,
                hint=hint_str,
                index=word_index,
            )
            word_index += 1
            self._words.append(cw)
    
    def serialize(self) -> dict:
        """Serialize the crossword to a dictionary format."""
        return {
            "grid_width": len(self._solved_grid[0]),
            "grid_height": len(self._solved_grid),
            "solved_grid": self._solved_grid,
            "current_grid": self._current_grid,
            "words": [
                {
                    "word": w.word,
                    "start_x": w.start_x,
                    "start_y": w.start_y,
                    "orientation": w.orientation.name,
                    "hint": w.hint,
                    "length": w.length,
                    "solved": w.solved,
                    "index": w.index,
                }
                for w in self._words
            ],
        }
    
    def place_letter(self, y: int, x: int, letter: str) -> bool:
        """Place a letter at position (x, y) in the current grid.

        Args:
            x (int): The x-coordinate (column index).
            y (int): The y-coordinate (row index).
            letter (str): The letter to place.
        Returns:
            bool: True if the letter was placed successfully, False otherwise.
        """
        letter = letter.lower()
        if y < 0 or y >= len(self._current_grid):
            return False
        if x < 0 or x >= len(self._current_grid[0]):
            return False
        if self._solved_grid[y][x] == '#':
            return False  # Cannot place letter on a wall
        self._current_grid[y][x] = letter

        horizontal_cw = self._horizontal_words_by_y_x_position.get((y, x))
        if horizontal_cw is not None:
            horizontal_cw.solved = self.check_word_solved(horizontal_cw)

        vertical_cw = self._vertical_words_by_y_x_position.get((y, x))
        if vertical_cw is not None:
            vertical_cw.solved = self.check_word_solved(vertical_cw)

        return True

    def check_word_solved(self, crossword_word: CrosswordWord) -> bool:
        """Check if the given CrosswordWord is correctly solved in the current grid.

        Args:
            crossword_word (CrosswordWord): The word to check.
        
        Returns:
            bool: True if the word is correctly solved, False otherwise.
        """
        dr, dc = (0, 1) if crossword_word.orientation == Orientation.HORIZONTAL else (1, 0)
        for i in range(crossword_word.length):
            r = crossword_word.start_y + dr * i
            c = crossword_word.start_x + dc * i
            if self._current_grid[r][c].lower() != crossword_word.word[i].lower():
                return False
        return True

    def get_words_at_position(self, x: int, y: int) -> List[CrosswordWord]:
        """
        get horizontal and vertical words at position (x, y)
        """
        result = []
        horizontal_cw = self._horizontal_words_by_y_x_position.get((y, x))
        if horizontal_cw is not None:
            result.append(horizontal_cw)
        vertical_cw = self._vertical_words_by_y_x_position.get((y, x))
        if vertical_cw is not None:
            result.append(vertical_cw)
        return result

    def __str__(self):
        # Simple string representation for debugging
        result = "\nCurrent Grid:\n"
        for row in self._current_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  

        result += "\n\nSolved Grid:\n"
        for row in self._solved_grid:
            row_str = ' '.join(cell if cell else ' ' for cell in row).replace("#", "█") + "\n"
            row_str = row_str.replace("█ █", "███").replace("█ █", "███")
            result += row_str
        return result

 
if __name__ == "__main__":
    # Example usage
    dictionary = load_de_dictionary()

    dictionary._build_pos_index_list()


    crossword = Crossword.generate(
        dictionary=dictionary,
        seed=None,
        grid_width=25,
        grid_height=25,
        grid_block_ratio=0.38
    )



    for word in crossword.words:
        print(f"Word: {word.word}, Start: ({word.start_x}, {word.start_y}), Orientation: {word.orientation}, Hint: {word.hint}") 


    print(crossword)