from fnmatch import fnmatch
from heuristics.heuristic_base import Heuristic

def get_parts(fact):
    """Extract the components of a PDDL fact by removing parentheses and splitting the string."""
    return fact[1:-1].split()

def match(fact, *args):
    """
    Check if a PDDL fact matches a given pattern.

    - `fact`: The complete fact as a string, e.g., "(at tray1 kitchen)".
    - `args`: The expected pattern (wildcards `*` allowed).
    - Returns `True` if the fact matches the pattern, else `False`.
    """
    parts = get_parts(fact)
    return all(fnmatch(part, arg) for part, arg in zip(parts, args))

class ChildsnackHeuristic(Heuristic):
    """
    A domain-dependent heuristic for the Childsnacks domain.

    # Summary
    This heuristic estimates the number of actions required to serve all children with sandwiches, considering the constraints of gluten allergies and the availability of ingredients.

    # Assumptions:
    - Each child must be served exactly one sandwich.
    - Children with gluten allergies must be served gluten-free sandwiches.
    - Sandwiches must be made from available bread and content portions in the kitchen.
    - Sandwiches must be placed on trays and moved to the appropriate locations before serving.

    # Heuristic Initialization
    - Extract the goal conditions (served children) and static facts (allergy information, waiting locations, etc.).
    - Identify the number of children, trays, and available ingredients.

    # Step-By-Step Thinking for Computing Heuristic
    1. Count the number of children who are not yet served.
    2. For each unserved child:
       - If the child is allergic to gluten, ensure a gluten-free sandwich is available.
       - If the child is not allergic, any sandwich can be used.
    3. Count the number of sandwiches that need to be made:
       - For each required sandwich, check if the necessary bread and content portions are available in the kitchen.
    4. Count the number of sandwiches that need to be placed on trays:
       - Each sandwich must be placed on a tray before it can be served.
    5. Count the number of trays that need to be moved to the correct locations:
       - Each tray must be moved to the location where the child is waiting.
    6. Sum the actions required for making sandwiches, placing them on trays, moving trays, and serving the sandwiches.
    """

    def __init__(self, task):
        """Initialize the heuristic by extracting goal conditions and static facts."""
        self.goals = task.goals  # Goal conditions.
        self.static = task.static  # Static facts.

        # Extract the list of children from the static facts.
        self.children = {
            get_parts(fact)[1]
            for fact in self.static
            if match(fact, "allergic_gluten", "*") or match(fact, "not_allergic_gluten", "*")
        }

        # Extract the list of trays from the static facts.
        self.trays = {
            get_parts(fact)[1]
            for fact in self.static
            if match(fact, "at", "tray*", "*")
        }

        # Extract the list of sandwiches from the static facts.
        self.sandwiches = {
            get_parts(fact)[1]
            for fact in self.static
            if match(fact, "notexist", "*")
        }

        # Extract the list of bread and content portions from the static facts.
        self.bread_portions = {
            get_parts(fact)[1]
            for fact in self.static
            if match(fact, "at_kitchen_bread", "*")
        }
        self.content_portions = {
            get_parts(fact)[1]
            for fact in self.static
            if match(fact, "at_kitchen_content", "*")
        }

    def __call__(self, node):
        """Compute an estimate of the minimal number of required actions."""
        state = node.state  # Current world state.

        # Count the number of children who are not yet served.
        unserved_children = len(self.children) - sum(1 for fact in state if match(fact, "served", "*"))

        # Count the number of sandwiches that need to be made.
        sandwiches_to_make = 0
        for child in self.children:
            if not any(match(fact, "served", child) for fact in state):
                if any(match(fact, "allergic_gluten", child) for fact in self.static):
                    # Gluten-free sandwich required.
                    sandwiches_to_make += 1
                else:
                    # Any sandwich will do.
                    sandwiches_to_make += 1

        # Count the number of sandwiches that need to be placed on trays.
        sandwiches_on_trays = sum(1 for fact in state if match(fact, "ontray", "*", "*"))
        sandwiches_to_place = max(0, sandwiches_to_make - sandwiches_on_trays)

        # Count the number of trays that need to be moved.
        trays_to_move = 0
        for tray in self.trays:
            if not any(match(fact, "at", tray, "*") for fact in state):
                trays_to_move += 1

        # Sum the actions required.
        total_cost = (
            sandwiches_to_make  # Actions to make sandwiches.
            + sandwiches_to_place  # Actions to place sandwiches on trays.
            + trays_to_move  # Actions to move trays.
            + unserved_children  # Actions to serve sandwiches.
        )

        return total_cost
