from __future__ import annotations

from dataclasses import dataclass
import typing

from schema import Schema, Optional, And, Or, SchemaError

from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
    StartInventoryPool, PerGameCommonOptions, OptionGroup


# schema helpers
class FloatRange:
    def __init__(self, low, high):
        self._low = low
        self._high = high

    def validate(self, value) -> float:
        if not isinstance(value, (float, int)):
            raise SchemaError(f"should be instance of float or int, but was {value!r}")
        if not self._low <= value <= self._high:
            raise SchemaError(f"{value} is not between {self._low} and {self._high}")
        return float(value)


LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))


class MaxSciencePack(Choice):
    """Maximum level of science pack required to complete the game.
    This also affects the relative cost of silo and satellite recipes if they are randomized.
    That is the only thing in which the Utility Science Pack and Space Science Pack settings differ."""
    display_name = "Maximum Required Science Pack"
    option_automation_science_pack = 0
    option_logistic_science_pack = 1
    option_military_science_pack = 2
    option_chemical_science_pack = 3
    option_production_science_pack = 4
    option_utility_science_pack = 5
    option_space_science_pack = 6
    default = 6

    def get_allowed_packs(self):
        return {option.replace("_", "-") for option, value in self.options.items() if value <= self.value} - \
               {"space-science-pack"}  # with rocket launch being the goal, post-launch techs don't make sense

    @classmethod
    def get_ordered_science_packs(cls):
        return [option.replace("_", "-") for option, value in sorted(cls.options.items(), key=lambda pair: pair[1])]

    def get_max_pack(self):
        return self.get_ordered_science_packs()[self.value].replace("_", "-")


class Goal(Choice):
    """Goal required to complete the game."""
    display_name = "Goal"
    option_rocket = 0
    option_satellite = 1
    default = 0


class TechCost(Range):
    range_start = 1
    range_end = 10000
    default = 5


class MinTechCost(TechCost):
    """The cheapest a Technology can be in Science Packs."""
    display_name = "Minimum Science Pack Cost"
    default = 5


class MaxTechCost(TechCost):
    """The most expensive a Technology can be in Science Packs."""
    display_name = "Maximum Science Pack Cost"
    default = 500


class TechCostDistribution(Choice):
    """Random distribution of costs of the Science Packs.
    Even: any number between min and max is equally likely.
    Low: low costs, near the minimum, are more likely.
    Middle: medium costs, near the average, are more likely.
    High: high costs, near the maximum, are more likely."""
    display_name = "Tech Cost Distribution"
    option_even = 0
    option_low = 1
    option_middle = 2
    option_high = 3


class TechCostMix(Range):
    """Percent chance that a preceding Science Pack is also required.
    Chance is rolled per preceding pack."""
    display_name = "Science Pack Cost Mix"
    range_end = 100
    default = 70


class RampingTechCosts(Toggle):
    """Forces the amount of Science Packs required to ramp up with the highest involved Pack. Average is preserved.
    For example:
    off: Automation (red)/Logistics (green) sciences can range from 1 to 1000 Science Packs,
    on: Automation (red) ranges to ~500 packs and Logistics (green) from ~500 to 1000 Science Packs"""
    display_name = "Ramping Tech Costs"


class Silo(Choice):
    """Ingredients to craft rocket silo or auto-place if set to spawn."""
    display_name = "Rocket Silo"
    option_vanilla = 0
    option_randomize_recipe = 1
    option_spawn = 2
    default = 0


class Satellite(Choice):
    """Ingredients to craft satellite."""
    display_name = "Satellite"
    option_vanilla = 0
    option_randomize_recipe = 1
    default = 0


class FreeSamples(Choice):
    """Get free items with your technologies."""
    display_name = "Free Samples"
    option_none = 0
    option_single_craft = 1
    option_half_stack = 2
    option_stack = 3
    default = 3


class FreeSamplesQuality(Choice):
    """If free samples are on, determine the quality of the granted items.
    Requires the quality mod, which is part of the Space Age DLC. Without it, normal quality is given."""
    display_name = "Free Samples Quality"
    option_normal = 0
    option_uncommon = 1
    option_rare = 2
    option_epic = 3
    option_legendary = 4
    default = 0


class TechTreeLayout(Choice):
    """Selects how the tech tree nodes are interwoven.
    Single: No dependencies
    Diamonds: Several grid graphs (4/9/16 nodes each)
    Pyramids: Several top halves of diamonds (6/10/15 nodes each)
    Funnels: Several bottom halves of diamonds (6/10/15 nodes each)
    Trees: Several trees
    Choices: A single balanced binary tree
    """
    display_name = "Technology Tree Layout"
    option_single = 0
    option_small_diamonds = 1
    option_medium_diamonds = 2
    option_large_diamonds = 3
    option_small_pyramids = 4
    option_medium_pyramids = 5
    option_large_pyramids = 6
    option_small_funnels = 7
    option_medium_funnels = 8
    option_large_funnels = 9
    option_trees = 10
    option_choices = 11
    default = 0


class TechTreeInformation(Choice):
    """How much information should be displayed in the tech tree.
    None: No indication of what a research unlocks.
    Advancement: Indicates if a research unlocks an item that is considered logical advancement, but not who it is for.
    Full: Labels with exact names and recipients of unlocked items; all researches are prefilled into the !hint command.
    """
    display_name = "Technology Tree Information"
    option_none = 0
    option_advancement = 1
    option_full = 2
    default = 2


class RecipeTime(Choice):
    """Randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.
    Fast: 0.25X - 1X
    Normal: 0.5X - 2X
    Slow: 1X - 4X
    Chaos: 0.25X - 4X
    New category: ignores vanilla recipe time and rolls new one
    New Fast: 0.25 - 2 seconds
    New Normal: 0.25 - 10 seconds
    New Slow:  5 - 10 seconds
    """
    display_name = "Recipe Time"
    option_vanilla = 0
    option_fast = 1
    option_normal = 2
    option_slow = 4
    option_chaos = 5
    option_new_fast = 6
    option_new_normal = 7
    option_new_slow = 8


class Progressive(Choice):
    """Merges together Technologies like "automation-1" to "automation-3" into 3 copies of "Progressive Automation",
    which awards them in order."""
    display_name = "Progressive Technologies"
    option_off = 0
    option_grouped_random = 1
    option_on = 2
    default = 2

    def want_progressives(self, random):
        return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)


class RecipeIngredients(Choice):
    """Select if rocket, or rocket + science pack ingredients should be random."""
    display_name = "Random Recipe Ingredients Level"
    option_rocket = 0
    option_science_pack = 1


class RecipeIngredientsOffset(Range):
    """When randomizing ingredients, remove or add this many "slots" of items.
    For example, at -1 a randomized Automation Science Pack will only require 1 ingredient, instead of 2."""
    display_name = "Randomized Recipe Ingredients Offset"
    range_start = -1
    range_end = 5


class FactorioStartItems(OptionDict):
    """Mapping of Factorio internal item-name to amount granted on start."""
    display_name = "Starting Items"
    default = {"burner-mining-drill": 4, "stone-furnace": 4,  "raw-fish": 50}
    schema = Schema(
        {
            str: And(int, lambda n: n > 0,
                     error="amount of starting items has to be a positive integer"),
        }
    )


class FactorioFreeSampleBlacklist(OptionSet):
    """Set of items that should never be granted from Free Samples"""
    display_name = "Free Sample Blacklist"


class FactorioFreeSampleWhitelist(OptionSet):
    """Overrides any free sample blacklist present. This may ruin the balance of the mod, be warned."""
    display_name = "Free Sample Whitelist"


class TrapCount(Range):
    range_end = 25


class AttackTrapCount(TrapCount):
    """Trap items that when received trigger an attack on your base."""
    display_name = "Attack Traps"


class TeleportTrapCount(TrapCount):
    """Trap items that when received trigger a random teleport.
    It is ensured the player can walk back to where they got teleported from."""
    display_name = "Teleport Traps"


class GrenadeTrapCount(TrapCount):
    """Trap items that when received trigger a grenade explosion on each player."""
    display_name = "Grenade Traps"


class ClusterGrenadeTrapCount(TrapCount):
    """Trap items that when received trigger a cluster grenade explosion on each player."""
    display_name = "Cluster Grenade Traps"


class ArtilleryTrapCount(TrapCount):
    """Trap items that when received trigger an artillery shell on each player."""
    display_name = "Artillery Traps"


class AtomicRocketTrapCount(TrapCount):
    """Trap items that when received trigger an atomic rocket explosion on each player.
    Warning: there is no warning. The launch is instantaneous."""
    display_name = "Atomic Rocket Traps"


class AtomicCliffRemoverTrapCount(TrapCount):
    """Trap items that when received trigger an atomic rocket explosion on a random cliff.
    Warning: there is no warning. The launch is instantaneous."""
    display_name = "Atomic Cliff Remover Traps"


class EvolutionTrapCount(TrapCount):
    """Trap items that when received increase the enemy evolution."""
    display_name = "Evolution Traps"
    range_end = 10


class EvolutionTrapIncrease(Range):
    """How much an Evolution Trap increases the enemy evolution.
    Increases scale down proportionally to the session's current evolution factor
    (40 increase at 0.50 will add 0.20... 40 increase at 0.75 will add 0.10...)"""
    display_name = "Evolution Trap % Effect"
    range_start = 1
    default = 10
    range_end = 100


class InventorySpillTrapCount(TrapCount):
    """Trap items that when received trigger dropping your main inventory and trash inventory onto the ground."""
    display_name = "Inventory Spill Traps"


class FactorioWorldGen(OptionDict):
    """World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
    with in-depth documentation at https://lua-api.factorio.com/latest/concepts/MapGenSettings.html"""
    display_name = "World Generation"
    # FIXME: do we want default be a rando-optimized default or in-game DS?
    value: dict[str, dict[str, typing.Any]]
    default = {
        "autoplace_controls": {
            # terrain
            "water": {"frequency": 1, "size": 1, "richness": 1},
            "nauvis_cliff": {"frequency": 1, "size": 1, "richness": 1},
            "starting_area_moisture": {"frequency": 1, "size": 1, "richness": 1},
            # resources
            "coal": {"frequency": 1, "size": 3, "richness": 6},
            "copper-ore": {"frequency": 1, "size": 3, "richness": 6},
            "crude-oil": {"frequency": 1, "size": 3, "richness": 6},
            "iron-ore": {"frequency": 1, "size": 3, "richness": 6},
            "stone": {"frequency": 1, "size": 3, "richness": 6},
            "uranium-ore": {"frequency": 1, "size": 3, "richness": 6},
            # misc
            "trees": {"frequency": 1, "size": 1, "richness": 1},
            "enemy-base": {"frequency": 1, "size": 1, "richness": 1},
        },
        "seed": None,
        "starting_area": 1,
        "peaceful_mode": False,
        "cliff_settings": {
            "name": "cliff",
            "cliff_elevation_0": 10,
            "cliff_elevation_interval": 40,
            "richness": 1
        },
        "property_expression_names": {
            "control-setting:moisture:bias": 0,
            "control-setting:moisture:frequency:multiplier": 1,
            "control-setting:aux:bias": 0,
            "control-setting:aux:frequency:multiplier": 1
        },
        "pollution": {
            "enabled": True,
            "diffusion_ratio": 0.02,
            "ageing": 1,
            "enemy_attack_pollution_consumption_modifier": 1,
            "min_pollution_to_damage_trees": 60,
            "pollution_restored_per_tree_damage": 10
        },
        "enemy_evolution": {
            "enabled": True,
            "time_factor": 40.0e-7,
            "destroy_factor": 200.0e-5,
            "pollution_factor": 9.0e-7
        },
        "enemy_expansion": {
            "enabled": True,
            "max_expansion_distance": 7,
            "settler_group_min_size": 5,
            "settler_group_max_size": 20,
            "min_expansion_cooldown": 14400,
            "max_expansion_cooldown": 216000
        }
    }
    schema = Schema({
        "basic": {
            Optional("autoplace_controls"): {
                str: {
                    "frequency": FloatRange(0, 6),
                    "size": FloatRange(0, 6),
                    "richness": FloatRange(0.166, 6)
                }
            },
            Optional("seed"): Or(None, And(int, lambda n: n >= 0)),
            Optional("width"): And(int, lambda n: n >= 0),
            Optional("height"): And(int, lambda n: n >= 0),
            Optional("starting_area"): FloatRange(0.166, 6),
            Optional("peaceful_mode"): LuaBool,
            Optional("cliff_settings"): {
                "name": str, "cliff_elevation_0": FloatRange(0, 99),
                "cliff_elevation_interval": FloatRange(0.066, 241),  # 40/frequency
                "richness": FloatRange(0, 6)
            },
            Optional("property_expression_names"): Schema({
                Optional("control-setting:moisture:bias"): FloatRange(-0.5, 0.5),
                Optional("control-setting:moisture:frequency:multiplier"): FloatRange(0.166, 6),
                Optional("control-setting:aux:bias"): FloatRange(-0.5, 0.5),
                Optional("control-setting:aux:frequency:multiplier"): FloatRange(0.166, 6),
                Optional(str): object  # allow overriding all properties
            }),
        },
        "advanced": {
            Optional("pollution"): {
                Optional("enabled"): LuaBool,
                Optional("diffusion_ratio"): FloatRange(0, 0.25),
                Optional("ageing"): FloatRange(0.1, 4),
                Optional("enemy_attack_pollution_consumption_modifier"): FloatRange(0.1, 4),
                Optional("min_pollution_to_damage_trees"): FloatRange(0, 9999),
                Optional("pollution_restored_per_tree_damage"): FloatRange(0, 9999)
            },
            Optional("enemy_evolution"): {
                Optional("enabled"): LuaBool,
                Optional("time_factor"): FloatRange(0, 1000e-7),
                Optional("destroy_factor"): FloatRange(0, 1000e-5),
                Optional("pollution_factor"): FloatRange(0, 1000e-7),
            },
            Optional("enemy_expansion"): {
                Optional("enabled"): LuaBool,
                Optional("max_expansion_distance"): FloatRange(2, 20),
                Optional("settler_group_min_size"): FloatRange(1, 20),
                Optional("settler_group_max_size"): FloatRange(1, 50),
                Optional("min_expansion_cooldown"): FloatRange(3600, 216000),
                Optional("max_expansion_cooldown"): FloatRange(18000, 648000)
            }
        }
    })

    def __init__(self, value: dict[str, typing.Any]):
        advanced = {"pollution", "enemy_evolution", "enemy_expansion"}
        self.value = {
            "basic": {k: v for k, v in value.items() if k not in advanced},
            "advanced": {k: v for k, v in value.items() if k in advanced}
        }

        # verify min_values <= max_values
        def optional_min_lte_max(container, min_key, max_key):
            min_val = container.get(min_key, None)
            max_val = container.get(max_key, None)
            if min_val is not None and max_val is not None and min_val > max_val:
                raise ValueError(f"{min_key} can't be bigger than {max_key}")

        enemy_expansion = self.value["advanced"].get("enemy_expansion", {})
        optional_min_lte_max(enemy_expansion, "settler_group_min_size", "settler_group_max_size")
        optional_min_lte_max(enemy_expansion, "min_expansion_cooldown", "max_expansion_cooldown")

    @classmethod
    def from_any(cls, data: dict[str, typing.Any]) -> FactorioWorldGen:
        if type(data) == dict:
            return cls(data)
        else:
            raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")


class ImportedBlueprint(DefaultOnToggle):
    """Allow or Disallow Blueprints from outside the current savegame."""
    display_name = "Blueprints"


class EnergyLink(Toggle):
    """Allow sending energy to other worlds. 25% of the energy is lost in the transfer."""
    display_name = "Energy Link"


@dataclass
class FactorioOptions(PerGameCommonOptions):
    max_science_pack: MaxSciencePack
    goal: Goal
    tech_tree_layout: TechTreeLayout
    min_tech_cost: MinTechCost
    max_tech_cost: MaxTechCost
    tech_cost_distribution: TechCostDistribution
    tech_cost_mix: TechCostMix
    ramping_tech_costs: RampingTechCosts
    silo: Silo
    satellite: Satellite
    free_samples: FreeSamples
    free_samples_quality: FreeSamplesQuality
    tech_tree_information: TechTreeInformation
    starting_items: FactorioStartItems
    free_sample_blacklist: FactorioFreeSampleBlacklist
    free_sample_whitelist: FactorioFreeSampleWhitelist
    recipe_time: RecipeTime
    recipe_ingredients: RecipeIngredients
    recipe_ingredients_offset: RecipeIngredientsOffset
    imported_blueprints: ImportedBlueprint
    world_gen: FactorioWorldGen
    progressive: Progressive
    teleport_traps: TeleportTrapCount
    grenade_traps: GrenadeTrapCount
    cluster_grenade_traps: ClusterGrenadeTrapCount
    artillery_traps: ArtilleryTrapCount
    atomic_rocket_traps: AtomicRocketTrapCount
    atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount
    inventory_spill_traps: InventorySpillTrapCount
    attack_traps: AttackTrapCount
    evolution_traps: EvolutionTrapCount
    evolution_trap_increase: EvolutionTrapIncrease
    death_link: DeathLink
    energy_link: EnergyLink
    start_inventory_from_pool: StartInventoryPool


option_groups: list[OptionGroup] = [
    OptionGroup(
        "Technologies",
        [
            TechTreeLayout,
            Progressive,
            MinTechCost,
            MaxTechCost,
            TechCostDistribution,
            TechCostMix,
            RampingTechCosts,
            TechTreeInformation,
        ]
    ),
    OptionGroup(
        "Traps",
        [
            AttackTrapCount,
            EvolutionTrapCount,
            EvolutionTrapIncrease,
            TeleportTrapCount,
            GrenadeTrapCount,
            ClusterGrenadeTrapCount,
            ArtilleryTrapCount,
            AtomicRocketTrapCount,
            AtomicCliffRemoverTrapCount,
            InventorySpillTrapCount,
        ],
        start_collapsed=True
    ),
]
