import time
from functions import startGame, wait, parse, get_instructions, query_equip, error_message, change_equip
from random import randint
from enum import Enum, auto


# pyinstaller --onefile --paths=C:\Users\kasai\PycharmProjects\pythonProject\Kanjin\KanjinEngine.py

class Engine(object):

    def __init__(self, scene_map):
        # self.name = "name"
        # self.descrip = "descrip"
        self.seed = None
        self.scene_map = scene_map
        # gets the game's map from instance "mymap" at bottom of this file

    def play(self):
        current_scene = self.scene_map.opening_scene()
        last_scene = self.scene_map.next_scene('finished')

        while current_scene != last_scene:
            next_scene_name = current_scene.enter()
            current_scene = self.scene_map.next_scene(next_scene_name)
        current_scene.enter()

            # #  note: will throw error if no new scene passed in by next line:
            # next_scene_name = current_scene.action()
            # #  get the name of the next scene from the action() function that
            # #     runs in the current scene - what it returns
            #
            # current_scene = self.scene_map.next_scene(next_scene_name)
            # #  here we use that val returned by current scene to go to
            # #    the next scene, running function in Map


class Player:
    """Character Creation"""

    # Default Character
    def __init__(self, name, gender, age, race, job):
        # Personality
        self.name = name
        self.gender = gender
        self.age = age
        self.race = race
        self.job = job
        # Health
        self.hitDie = randint(1, 6)
        self.currentHP = 100
        self.maxHP = 100
        # Levels
        self.level = 1
        self.exp = 0
        self.maxEXP = 100
        # Magic
        self.spells_known = []
        self.spells_ready = []
        # Equipment
        self.weapon = rock
        self.armor = tornRags
        # Defence
        self.ac = 0
        self.elemental_resistance = {DamageType.FIRE: False, DamageType.WATER: False, DamageType.LIGHTNING: False}
        self.physical_resistance = {DamageType.SLASHING: False, DamageType.CRUSHING: False, DamageType.PIERCING: False}
        # Ability Scores
        self.stats = {"Strength": 0,
                      "Dexterity": 0,
                      "Constitution": 0,
                      "Intelligence": 0,
                      "Wisdom": 0,
                      "Charisma": 0}
        # Ability Modifiers
        self.mods = {"Strength": 0,
                     "Dexterity": 0,
                     "Constitution": 0,
                     "Intelligence": 0,
                     "Wisdom": 0,
                     "Charisma": 0}
        # Held items
        self.inventory = {rock: {"Count": 1,
                                 "object": Weapon,
                                 "equipped": True
                                 },
                          paper: {"Count": 1,
                                 "object": Weapon,
                                 "equipped": False
                                 },
                          tornRags: {"Count": 1,
                                     "object": Armor,
                                     "equipped": True
                                     },
                          GP1: {"Count": 10,
                                "object": Money,
                                "equipped": False
                                },
                          }

    def getModifier(self):
        self.mods["Strength"] = -5 + self.stats["Strength"] // 2
        self.mods["Dexterity"] = -5 + self.stats["Dexterity"] // 2
        self.mods["Constitution"] = -5 + self.stats["Constitution"] // 2
        self.mods["Intelligence"] = -5 + self.stats["Intelligence"] // 2
        self.mods["Wisdom"] = -5 + self.stats["Wisdom"] // 2
        self.mods["Charisma"] = -5 + self.stats["Charisma"] // 2

    def set_stats_race(self):
        if self.race.lower() == "human":
            self.stats["Strength"] += 1,
            self.stats["Dexterity"] += 1,
            self.stats["Constitution"] += 1,
            self.stats["Intelligence"] += 1,
            self.stats["Wisdom"] += 1,
            self.stats["Charisma"] += 1

        elif self.race.lower() == "elf":
            self.stats["Dexterity"] += 2
            self.stats["Intelligence"] += 1

        elif self.race.lower() == "dwarf":
            self.stats["Constitution"] += 2
            self.stats["Wisdom"] += 1

    def set_stats_job(self):
        if self.job.lower() == "warrior":
            self.hitDie = randint(1, 12)
            self.maxHP = (12 + self.mods["Constitution"])
            self.currentHP = (12 + self.mods["Constitution"])

        elif self.job.lower() == "ranger":
            self.hitDie = randint(1, 10)
            self.maxHP = (10 + self.mods["Constitution"])
            self.currentHP = (10 + self.mods["Constitution"])

        elif self.job.lower() == "mage":
            self.hitDie = randint(1, 8)
            self.maxHP = (8 + self.mods["Constitution"])
            self.currentHP = (8 + self.mods["Constitution"])

    def current_stats(self):
        """Prints a display of the user's current statistics."""
        print("Your current stats are:")
        print(f'Hit Points: {self.currentHP}/{self.maxHP}')
        print(f'Strength: {self.stats["Strength"]}')
        print(f'Dexterity: {self.stats["Dexterity"]}')
        print(f'Constitution: {self.stats["Constitution"]}')
        print(f'Intelligence: {self.stats["Intelligence"]}')
        print(f'Wisdom: {self.stats["Wisdom"]}')
        print(f'Charisma: {self.stats["Charisma"]}')

    def current_equip(self):
        """Prints a list of items currently equipped."""
        print(f"You have equipped:\n {self.armor.name}\n"
              f"    {self.armor.description}"
              f"\n{self.weapon.name}\n"
              f"    {self.weapon.description}\n")

    def current_inventory(self):
        """Prints a list of items in your backpack that aren't equipped to your person."""
        print(f'In your rucksack you have:')
        for item_key, item_value in self.inventory.items():  # string, dictionary
            for key, value in item_value.items():  # variable/object, integer
                if (key == "equipped") and not value:  # if equipped = False
                    values = item_value.values()  # make a variable of all values
                    values_list = list(values)  # turn variable into a list
                    count_value = values_list[0]  # take the first value from list - why?
                    print(f'{item_key.name} x {count_value}\n'
                          f'    {item_key.description}')

    def drop_item(self, object1):
        pass

    def loot_object(self, object1):
        for scene, room in Map.scenes.items():  # iterate map scenes
            if Engine.seed == scene:  # set new Engine seed in each room.enter()
                for list_key, list_value in room.lootable_items.items():  # string, dictionary
                    if object1 in list_key:  # if user input is in the lootable items
                        for var, count in list_value.items():  # variable/object, integer

                            if var in self.inventory:  # if object is in inventory
                                self.inventory[var]["Count"] += count  # increase item count
                                print(f'{var.name} x {count} has been added to your existing stack.')

                            elif var not in self.inventory[var]:  # if variable not in inventory
                                self.inventory[var] = {"Count": count,  # add it in
                                                       "object": var.type,
                                                       "equipped": False}
                                print(f'{var.name} x {count} has been added to your rucksack.')

                        del room.lootable_items[object1]  # remove item from lootable list
                        break
                    else:
                        print("You don't see one of those to loot.")
                        break

    def add_inventory(self, object1):
        for scene, room in Map.scenes.items():  # iterate map scenes
            if Engine.seed == scene:  # set new Engine seed in each room.enter()
                for list_key, list_value in room.available_items.items():  # string, dictionary
                    if object1 in list_key:  # if user input is in the available items
                        for var, count in list_value.items():  # variable/object, integer

                            if var in self.inventory:  # if object is in inventory
                                self.inventory[var]["Count"] += count  # increase item count
                                print(f'{var.name} x {count} has been added to your existing stack.')

                            elif var not in self.inventory[var]:  # if variable not in inventory
                                self.inventory[var] = {"Count": count,  # add it in
                                                       "object": var.type,
                                                       "equipped": False}
                                print(f'{var.name} x {count} has been added to your rucksack.')

                        del room.available_items[object1]  # remove item from available list
                        break
                    elif object1 not in list_key:
                        print("You don't see any lying around..")
                        break

    def new_char(self):
        """Outputs final stats, armour, and inventory"""
        self.allocation()
        self.set_stats_race()
        self.set_stats_job()
        self.getModifier()
        self.current_stats()
        print(' ')
        time.sleep(1)
        self.current_equip()
        print(' ')
        self.current_inventory()
        print(' ')

    def health_check(self):
        """Print out current/max HP"""
        print(f'You have {self.currentHP}/{self.maxHP} HP.')

    def take_damage(self, DMGtype, size):
        """Define the damage type and dice size"""
        damage = randint(1, size)
        if DMGtype in self.elemental_resistance or DMGtype in self.physical_resistance:
            damage //= 2
        self.currentHP -= damage
        return damage

    def allocation(self):
        rolls = []
        stats = []
        attributes = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"]
        while len(stats) != 6:
            # roll 4d6
            for roll in range(4):
                r = randint(1, 6)
                if r < 6:
                    r = randint(1, 6)
                rolls.append(r)
            # Drop the lowest number and then add to single value
            del rolls[rolls.index(min(rolls))]
            val = sum(rolls)
            stats.append(str(val))
            rolls = []

        print("Please assign a stat to a selected attribute by entering the number then the attribute.\n"
              "For example, '10 strength' will assign the stat 10 to your strength, if you have a 10 showing.\n")
        print("Please select a stat and an attribute.\n\n")
        print("Your rolled stats are:\n" + ', '.join(stats) + "\n\n")
        print("Your attributes are:\n" + ', '.join(attributes) + "\n\n")

        # Allocate
        while len(stats) != 0:
            input_text = input(">> ").title()
            words = input_text.split()
            if words[0] in stats and words[1] in attributes:
                self.stats[words[1]] += int(words[0])
                stats.remove(words[0])
                attributes.remove(words[1])
                if len(stats) == 0:
                    print("")
                    break
                print("The remaining options are:\n")
                print(', '.join(stats))
                print(', '.join(attributes))
            else:
                print("Error. Input was not recognised. Please try again with 'Number' + 'Attribute'.")
                continue


class Scene(object):
    def __init__(self):
        self.objects = []

    # def enter(self):
    #     print(self.name)  # works
    #     print(self.descrip)  # works
    # # this applies to all classes under Scene, but Zed does it differently


class DamageType(Enum):
    SLASHING = auto()
    CRUSHING = auto()
    PIERCING = auto()
    FIRE = auto()
    WATER = auto()
    LIGHTNING = auto()


class DamageMod(Enum):
    STRENGTH = auto()
    DEXTERITY = auto()
    CONSTITUTION = auto()
    INTELLIGENCE = auto()
    WISDOM = auto()
    CHARISMA = auto()


class ItemType(Enum):
    ARMOR = auto()
    WEAPON = auto()
    MONEY = auto()
    ITEM = auto()


class Item:
    """The base class for all items"""

    def __init__(self, name, description, value, magical):
        self.name = name
        self.description = description
        self.value = value
        self.magical = magical
        self.type = Item
        self.count = 1

    def __repr__(self):
        return f"{self.name}\n=====\n{self.description}\nValue: {self.value}\n"


class Money(Item):
    """The currency item used in the world of Kanjin"""

    def __init__(self, name, amt, magical):
        self.name = name
        self.amt = amt
        self.magical = magical
        self.type = ItemType.MONEY
        super().__init__(name=self.name,
                         description=f"A small round coin made of {self.name.lower()} "
                                     f"with the imperial city logo stamped on the face.",
                         value=self.amt,
                         magical=self.magical)


class Weapon(Item):
    """The base class for all weapons"""

    def __init__(self, name, description, value, damage1H, damage2H, dmgType, dmgMod, versatile, thrown, magical):
        self.damage1H = damage1H
        self.damage2H = damage2H
        self.dmgType = dmgType
        self.dmgMod = dmgMod
        self.versatile = versatile
        self.thrown = thrown
        self.type = Weapon
        super().__init__(name, description, value, magical)

    def __repr__(self):
        return f"{self.name}\n=====" \
               f"\n{self.description}" \
               f"\nValue: {self.value}" \
               f"\nDamage: One Handed - {self.damage1H}" \
               f"\nDamage: Two Handed - {self.damage2H}" \
               f"\nDamage Type: {self.dmgType}" \
               f"\nMagical: {self.magical}\n"


class Armor(Item):
    """The base class for all armor"""

    def __init__(self, name, description, value, grade, ac, disadvantage, dmgRes, magical):
        self.grade = grade
        self.ac = ac
        self.stealthDis = disadvantage
        self.dmgRes = dmgRes
        self.type = Armor
        super().__init__(name, description, value, magical)

    def __repr__(self):
        if self.stealthDis:
            return f"{self.name}\n" \
                   f"=====\n" \
                   f"{self.description}\n" \
                   f"Value: {self.value}\n" \
                   f"AC: {self.ac}\n" \
                   f"Magical: {self.magical}"
        elif not self.stealthDis:
            return f"{self.name}\n=====" \
                   f"\n{self.description}\n" \
                   f"Value: {self.value}\n" \
                   f"AC: {self.ac}\n" \
                   f"Disadvantage on Stealth checks: {self.stealthDis}\n" \
                   f"Magical: {self.magical}\n"


def process_command(command, object1):
    output = None
    if command == "help":
        output = get_instructions()
    elif command == "inventory":
        user.current_inventory()
    elif command == "equipment":
        response = query_equip()
        if response == "view":
            user.current_equip()
        elif response == "change":
            output = change_equip()
    elif command == "examine":
        for item_key, item_value in user.inventory.items():
            if object1 == item_key.name.lower():
                print(item_key)
            else:
                output = "Unable to examine that."
    elif command == "take":
        user.add_inventory(object1)
    elif command == "loot":
        user.loot_object(object1)
    elif command == "error":
        output = error_message()

    return output


# Different types of coins
GP1 = Money("Gold", 1, False)
SP1 = Money("Silver", 1, False)
CP1 = Money("Copper", 1, False)

# starting equip
rock = Weapon(
    name="Rock",
    description="A fist-sized rock, suitable for bludgeoning.",
    value="No value",
    damage1H=randint(1, 6),
    damage2H=0,
    dmgType=DamageType.CRUSHING,
    dmgMod=DamageMod.STRENGTH,
    versatile=False,
    thrown=True,
    magical=False
)

tornRags = Armor(
    name="Torn Rags",
    description="A ripped and worn-out outfit.",
    value="No Value",
    grade="Light",
    ac=0,
    disadvantage=True,
    dmgRes="None",
    magical=False
)
paper = Item(
    name="Paper",
    description="A blank piece of paper",
    value=0,
    magical=False
)

user = Player("Jamie", "Male", 29, "Elf", "Mage")


# user = Player(*startGame())


# enter() needs to return a map name.
class Tutorial(Scene):
    available_items = {
        "rock": {rock: 12},
        "paper": {paper: 3}
    }
    lootable_items = {
        "chest": {GP1: 5, rock: 1},
        "corpse": {GP1: 100}
    }
    name = "Tutorial Level"
    # descrip = ("There are certain commands that will be available almost anytime you are able to type,\n"
    #            "such as viewing your inventory, checking your equipped items, and also changing them.\n"
    #            "You can also view your stats including your current and max hp.\n"
    #            "Give it a go starting with 'Check inventory', 'Check equipment', and 'View stats'.\n"
    #            "See what else you can do, then enter 'continue' to move to the next step.")
    descrip = f'Scattered around the room you see {available_items["rock"][rock]}x rock.'
    has_visited = False
    current_seed = "tutorial"

    def enter(self):
        Engine.seed = "tutorial"
        if not self.has_visited:
            print("===========")
            print(self.name)
            print("===========")
            print(self.descrip)
        else:
            print("Welcome back to the tutorial.")
        self.has_visited = True
        action = input(">> ").lower()

        while action not in ("continue", "scene"):  # move scene to parser
            process_command(*parse(action))
            action = input(">> ").lower()
        else:
            if action == "continue":
                return "begin"
            elif action == "scene":
                print(self.available_items)
                return self.current_seed

    def action(self):
        pass


class Begin(Scene):
    available_items = {
        "rock": {rock: 10},
        "paper": {paper: 13}}
    name = "Kanjin - An RPG Text Adventure"
    descrip = "Welcome to Kanjin.\nThis game is based off of the D&D SRD ruleset, and will see you, our adventurer, " \
              "exploring, adventuring, and plundering.\n\nTo play Kanjin is very simple.\nRead the instructions, " \
              "understand the scene, and take control.\nAll directional cues will be given and clear but you " \
              "can also use specific commands at almost anytime when you give your input.\n" \
              "After character creation, type 'Help' to view a list of commands and explanations.\n"
    has_visited = False
    current_seed = "begin"

    def enter(self):
        Engine.seed = "begin"
        if not self.has_visited:
            print("===========")
            print(self.name)
            print("===========")
            print(self.descrip)
            wait()
            # user.new_char()
        else:
            print("Welcome back to the beginning")
        self.has_visited = True
        action = input(">> ").lower()

        while action not in ("continue", "scene", "back"):
            process_command(*parse(action))
            action = input(">> ").lower()
        else:
            if action == "continue":
                return "cave entrance"
            elif action == "back":
                return "tutorial"
            elif action == "scene":
                print(self.descrip)
                return self.current_seed
        return "Cave entrance"

    def action(self):
        pass


class CaveEntrance(Scene):
    available_items = {}
    name = "Cave Entrance"
    descrip = "A small broken statue at the mouth of a cave marks the entrance to a dungeon.\n" \
               "Beyond the broken statue lies a massive, rugged room, too dark to see into.\n" \
               "You make out the silhouette of broken pottery sprawled around.\n" \
               "\nBehind you is the forest you traversed through to get here, about 1500m away from the road.\n" \
               "To the east, a canyon. Too wide to cross and too deep to see the bottom.\n" \
               "On the west is a mountain face. You are not prepared to take that on.\n"
    descFalse = "You return to the cave entrance with the road behind you, a canyon to the east, " \
                "and a mountain face on the right."
    has_visited = False
    current_seed = "cave entrance"

    def enter(self):
        Engine.seed = "cave entrance"
        if not self.has_visited:
            print("===========")
            print(self.name)
            print("===========")
            print(self.descrip)
        else:
            print("Welcome back to the cave")
        self.has_visited = True
        action = input(">> ").lower()

        while action not in ("continue", "scene", "back"):
            process_command(*parse(action))
            action = input(">> ").lower()
        else:
            if action == "continue":
                return "begin"
            elif action == "back":
                return "begin"
            elif action == "scene":
                print(self.descrip)
        return "Cave entrance"

    def action(self):
        pass


class A1(Scene):
    available_items = {}
    name = "Laser Weapon Armory"
    descrip = "Shelves and cases line the walls of this room. Weapons of every description " \
              "fill the shelves and cases. There is a digital keypad set into the wall."
    current_seed = "A1"

    def enter(self):
        print("===========")
        print(self.name)
        print("===========")
        print(self.descrip)

    def action(self):
        pass


# class Death(Scene):
#
#     def __init__(self):
#         self.name = "You have died!"
#         self.descrip = "Your spirit leaves swiftly as your body collapses."
#
#     quips = [
#         "You died. You kinda suck at this.",
#         "Your Mom would be proud...if she were smarter.",
#         "Such a loser.",
#         "I have a small puppy that's better at this.",
#         "You're worse than your Dad's jokes."
#     ]
#
#     def enter(self):
#         print(Death.quips[randint(0, len(self.quips) - 1)])
#         exit(1)
#
#     def action(self):
#         pass


# Map tells us where we are and where we can go
# it does not make us move - Engine does that
class Map(object):
    scenes = {
        'tutorial': Tutorial(),
        "begin": Begin(),
        'cave entrance': CaveEntrance(),
        'A1': A1(),
        # 'death': Death(),
        # 'finished': Finished()
    }

    def __init__(self, start_scene_key):
        self.start_scene_key = start_scene_key
        # above we make a local var named start_scene_key
        # start_scene_key remains unchanged throughout the game

    @staticmethod
    def next_scene(scene_name):
        val = Map.scenes.get(scene_name)
        # above is how we get value out of the dictionary named scenes 
        return val
        # this function can be called repeatedly in the game, 
        # unlike opening_scene, which is called only ONCE 

    def opening_scene(self):
        return self.next_scene(self.start_scene_key)


seed = "tutorial"  # starts the game
myMap = Map(seed)  # instantiate a new Map object w/ one arg
myGame = Engine(myMap)  # instantiate a new Engine object w/ one arg
myGame.play()  # call function from that Engine instance
