from collections import defaultdict
import math
import os
from typing import Any, Dict, List, Set

from .ProgressiveDistricts import get_flat_progressive_districts
from worlds.generic.Rules import forbid_item


from .Data import (
    get_boosts_data,
    get_era_required_items_data,
)

from .Rules import create_boost_rules
from .Container import (
    CivVIContainer,
    generate_goody_hut_sql,
    generate_new_items,
    generate_setup_file,
    generate_update_boosts_sql,
)
from .Enum import CivVICheckType, CivVIHintClassification
from .Items import (
    BOOSTSANITY_PROGRESSION_ITEMS,
    FILLER_DISTRIBUTION,
    CivVIEvent,
    CivVIItemData,
    FillerItemRarity,
    format_item_name,
    generate_item_table,
    CivVIItem,
    get_item_by_civ_name,
    get_random_filler_by_rarity,
)
from .Locations import (
    CivVILocation,
    CivVILocationData,
    EraType,
    generate_era_location_table,
    generate_flat_location_table,
)
from .Options import CivVIOptions
from .Regions import create_regions
from BaseClasses import Item, ItemClassification, MultiWorld, Tutorial
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess  # type: ignore


def run_client(*args: Any):
    print("Running Civ6 Client")
    from .Civ6Client import main  # lazy import

    launch_subprocess(main, name="Civ6Client")


components.append(
    Component(
        "Civ6 Client",
        func=run_client,
        component_type=Type.CLIENT,
        file_identifier=SuffixIdentifier(".apcivvi"),
    )
)


class CivVIWeb(WebWorld):
    tutorials = [
        Tutorial(
            "Multiworld Setup Guide",
            "A guide to setting up Civilization VI for MultiWorld.",
            "English",
            "setup_en.md",
            "setup/en",
            ["hesto2"],
        )
    ]
    theme = "ocean"


class CivVIWorld(World):
    """
    Civilization VI is a turn-based strategy video game in which one or more players compete alongside computer-controlled opponents to grow their individual civilization from a small tribe to control the entire planet across several periods of development.
    """

    game = "Civilization VI"
    topology_present = False
    options_dataclass = CivVIOptions
    options: CivVIOptions  # type: ignore

    web = CivVIWeb()

    item_name_to_id = {item.name: item.code for item in generate_item_table().values()}
    location_name_to_id = {
        location.name: location.code
        for location in generate_flat_location_table().values()
    }

    item_table: Dict[str, CivVIItemData] = {}
    location_by_era: Dict[str, Dict[str, CivVILocationData]]
    required_client_version = (0, 4, 5)
    location_table: Dict[str, CivVILocationData]
    era_required_non_progressive_items: Dict[EraType, List[str]]
    era_required_progressive_items_counts: Dict[EraType, Dict[str, int]]
    era_required_progressive_era_counts: Dict[EraType, int]
    item_by_civ_name: Dict[str, str]

    def __init__(self, multiworld: MultiWorld, player: int):
        super().__init__(multiworld, player)
        self.location_by_era = generate_era_location_table()

        self.location_table: Dict[str, CivVILocationData] = {}
        self.item_table = generate_item_table()

        self.era_required_non_progressive_items = {}
        self.era_required_progressive_items_counts = {}
        self.era_required_progressive_era_counts = {}

        for locations in self.location_by_era.values():
            for location in locations.values():
                self.location_table[location.name] = location

    def generate_early(self) -> None:
        flat_progressive_items = get_flat_progressive_districts()

        self.item_by_civ_name = {
            item.civ_name: get_item_by_civ_name(item.civ_name, self.item_table).name
            for item in self.item_table.values()
            if item.civ_name
        }

        previous_era_counts = None
        eras_list = [e.value for e in EraType]
        for era in EraType:
            # Initialize era_required_progressive_era_counts
            era_index = eras_list.index(era.value)
            self.era_required_progressive_era_counts[era] = (
                0
                if era in {EraType.ERA_FUTURE, EraType.ERA_INFORMATION}
                else era_index + 1
            )

            # Initialize era_required_progressive_items_counts
            self.era_required_progressive_items_counts[era] = defaultdict(int)

            if previous_era_counts:
                self.era_required_progressive_items_counts[era].update(
                    previous_era_counts
                )

            # Initialize era_required_non_progressive_items and add to item counts
            self.era_required_non_progressive_items[era] = []

            for item in get_era_required_items_data()[era.value]:
                if (
                    item in flat_progressive_items
                    and self.options.progression_style != "none"
                ):
                    progressive_name = format_item_name(flat_progressive_items[item])
                    self.era_required_progressive_items_counts[era][
                        progressive_name
                    ] += 1
                else:
                    self.era_required_non_progressive_items[era].append(
                        self.item_by_civ_name[item]
                    )

            previous_era_counts = self.era_required_progressive_items_counts[era].copy()

    def get_filler_item_name(self) -> str:
        return get_random_filler_by_rarity(self, FillerItemRarity.COMMON).name

    def create_regions(self) -> None:
        create_regions(self)

    def set_rules(self) -> None:
        if self.options.boostsanity:
            create_boost_rules(self)

    def create_event(self, event: str):
        return CivVIEvent(event, ItemClassification.progression, None, self.player)

    def create_item(self, name: str) -> Item:
        item: CivVIItemData = self.item_table[name]
        classification = item.classification
        if self.options.boostsanity:
            if item.civ_name in BOOSTSANITY_PROGRESSION_ITEMS:
                classification = ItemClassification.progression

        return CivVIItem(item, self.player, classification)

    def create_items(self) -> None:
        data = get_era_required_items_data()
        early_items = data[EraType.ERA_ANCIENT.value]
        early_locations = [
            location
            for location in self.location_table.values()
            if location.era_type == EraType.ERA_ANCIENT.value
        ]
        for item_name, item_data in self.item_table.items():
            # These item types are handled individually
            if item_data.item_type in [
                CivVICheckType.PROGRESSIVE_DISTRICT,
                CivVICheckType.ERA,
                CivVICheckType.GOODY,
            ]:
                continue

            # If we're using progressive districts, we need to check if we need to create a different item instead
            item_to_create = item_name
            item: CivVIItemData = self.item_table[item_name]
            if self.options.progression_style != "none":
                if item.progressive_name:
                    item_to_create = self.item_table[item.progressive_name].name

            self.multiworld.itempool += [self.create_item(item_to_create)]
            if item.civ_name in early_items:
                self.multiworld.early_items[self.player][item_to_create] = 1
            elif self.item_table[item_name].era in [
                EraType.ERA_ATOMIC,
                EraType.ERA_INFORMATION,
                EraType.ERA_FUTURE,
            ]:
                for location in early_locations:
                    found_location = None
                    try:
                        found_location = self.get_location(location.name)
                        forbid_item(found_location, item_to_create, self.player)
                    except KeyError:
                        pass

        # Era items
        if self.options.progression_style == "eras_and_districts":
            # Add one less than the total number of eras (start in ancient, don't need to find it)
            for era in EraType:
                if era.value == "ERA_ANCIENT":
                    continue
                progressive_era_item = self.item_table.get("Progressive Era")
                assert progressive_era_item is not None
                self.multiworld.itempool += [
                    self.create_item(progressive_era_item.name)
                ]

            self.multiworld.early_items[self.player]["Progressive Era"] = 2

        num_filler_items = 0
        # Goody items, create 10 by default if options are enabled
        if self.options.shuffle_goody_hut_rewards:
            num_filler_items += 10

        if self.options.boostsanity:
            num_filler_items += len(get_boosts_data())

        filler_count = {
            rarity: math.ceil(FILLER_DISTRIBUTION[rarity] * num_filler_items)
            for rarity in FillerItemRarity.__reversed__()
        }
        filler_count[FillerItemRarity.COMMON] -= (
            sum(filler_count.values()) - num_filler_items
        )
        self.multiworld.itempool += [
            self.create_item(get_random_filler_by_rarity(self, rarity).name)
            for rarity, count in filler_count.items()
            for _ in range(count)
        ]

    def post_fill(self) -> None:
        if not self.options.pre_hint_items.value:
            return

        def is_hintable_filler_item(item: Item) -> bool:
            return (
                item.classification == 0
                and CivVIHintClassification.FILLER.value
                in self.options.pre_hint_items.value
            )

        start_location_hints: Set[str] = self.options.start_location_hints.value
        non_filler_flags = [
            CivVIHintClassification(flag).to_item_classification()
            for flag in self.options.pre_hint_items.value
            if flag != CivVIHintClassification.FILLER.value
        ]
        for location_name, location_data in self.location_table.items():
            if (
                location_data.location_type != CivVICheckType.CIVIC
                and location_data.location_type != CivVICheckType.TECH
            ):
                continue

            location: CivVILocation = self.get_location(location_name)  # type: ignore

            if location.item and (
                is_hintable_filler_item(location.item)
                or any(
                    flag in location.item.classification for flag in non_filler_flags
                )
            ):
                start_location_hints.add(location_name)

    def fill_slot_data(self) -> Dict[str, Any]:
        return self.options.as_dict(
            "progression_style",
            "death_link",
            "research_cost_multiplier",
            "death_link_effect",
            "death_link_effect_percent",
        )

    def generate_output(self, output_directory: str):
        mod_name = self.multiworld.get_out_file_name_base(self.player)
        mod_dir = os.path.join(output_directory, mod_name)
        mod_files = {
            f"NewItems.xml": generate_new_items(self),
            f"InitOptions.lua": generate_setup_file(self),
            f"GoodyHutOverride.sql": generate_goody_hut_sql(self),
            f"UpdateExistingBoosts.sql": generate_update_boosts_sql(self),
        }
        mod = CivVIContainer(
            mod_files,
            mod_dir,
            output_directory,
            self.player,
            self.multiworld.get_file_safe_player_name(self.player),
        )
        mod.write()
