from __future__ import print_function
import json
import logging
import os
import datetime
import calendar
import sys
from collections import OrderedDict

from pokemongo_bot.base_dir import _base_dir
from pokemongo_bot.services.item_recycle_worker import ItemRecycler

'''
Helper class for updating/retrieving Inventory data

Interesting info and formulas:
https://drive.google.com/file/d/0B0TeYGBPiuzaenhUNE5UWnRCVlU/view
https://www.reddit.com/r/pokemongodev/comments/4w7mdg/combat_damage_calculation_formula_exactly/
'''

class FileIOException(Exception):
    pass

#
# Abstraction

class _StaticInventoryComponent(object):
    # optionally load static data from file,
    # dropping the data in a static variable named STATIC_DATA
    STATIC_DATA_FILE = None
    STATIC_DATA = None

    def __init__(self):
        if self.STATIC_DATA_FILE is not None:
            self.init_static_data()

    @classmethod
    def init_static_data(cls):
        if not hasattr(cls, 'STATIC_DATA') or cls.STATIC_DATA is None:
            cls.STATIC_DATA = cls.process_static_data(
                json.load(open(cls.STATIC_DATA_FILE)))

    @classmethod
    def process_static_data(cls, data):
        # optional hook for processing the static data
        # default is to use the data directly
        return data

class _BaseInventoryComponent(_StaticInventoryComponent):
    TYPE = None  # base key name for items of this type
    ID_FIELD = None  # identifier field for items of this type

    def __init__(self):
        self._data = {}
        super(_BaseInventoryComponent, self).__init__()

    def parse(self, item):
        # optional hook for parsing the dict for this item
        # default is to use the dict directly
        return item

    def retrieve_data(self, inventory):
        assert self.TYPE is not None
        assert self.ID_FIELD is not None
        ret = {}
        for item in inventory:
            data = item['inventory_item_data']
            if self.TYPE in data:
                item = data[self.TYPE]
                key = item[self.ID_FIELD]
                ret[key] = self.parse(item)
        return ret

    def refresh(self, inventory):
        self._data = self.retrieve_data(inventory)

    def get(self, object_id):
        return self._data.get(object_id)

    def all(self):
        return list(self._data.values())

#
# Inventory Components
class Player(_BaseInventoryComponent):
    TYPE = 'player_stats'

    def __init__(self, bot):
        self.bot = bot
        self._exp = None
        self._level = None
        self.next_level_xp = None
        self.pokemons_captured = None
        self.poke_stop_visits = None
        self.player_stats = None
        super(_BaseInventoryComponent, self).__init__()

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, value):
        self._level = value

    @property
    def exp(self):
        return self._exp

    @exp.setter
    def exp(self, value):
        # if new exp is larger than or equal to next_level_xp
        if value >= self.next_level_xp:
            self.level = self._level + 1
            # increase next_level_xp to a big amount
            # will be fix on the next heartbeat
            self.next_level_xp += 10000000

        self._exp = value

    def refresh(self,inventory):
        self.player_stats = self.retrieve_data(inventory)

    def parse(self, item):
        if not item:
            item = {}

        self.next_level_xp = item.get('next_level_xp', 0)
        self.exp = item.get('experience', 0)
        self.level = item.get('level', 0)
        self.pokemons_captured = item.get('pokemons_captured', 0)
        self.poke_stop_visits = item.get('poke_stop_visits', 0)

    def retrieve_data(self, inventory):
        ret = {}
        for item in inventory:
            data = item['inventory_item_data']
            if self.TYPE in data:
                item = data[self.TYPE]
                ret = item
                self.parse(item)

        return ret

class Candies(_BaseInventoryComponent):
    TYPE = 'candy'
    ID_FIELD = 'family_id'

    @classmethod
    def family_id_for(cls, pokemon_id):
        return Pokemons.candyid_for(pokemon_id)

    def get(self, pokemon_id):
        family_id = self.family_id_for(pokemon_id)
        return self._data.setdefault(family_id, Candy(family_id, 0))

    def parse(self, item):
        candy = item['candy'] if 'candy' in item else 0
        return Candy(item['family_id'], candy)

class Pokedex(_BaseInventoryComponent):
    TYPE = 'pokedex_entry'
    ID_FIELD = 'pokemon_id'

    def seen(self, pokemon_id):
        return pokemon_id in self._data

    def captured(self, pokemon_id):
        return self.seen(pokemon_id) and self._data.get(pokemon_id, {}).get('times_captured', 0) > 0

    def shiny_seen(self, pokemon_id):
        return self._data.get(pokemon_id, {}).get('encountered_shiny', False)

    def shiny_captured(self, pokemon_id):
        return self._data.get(pokemon_id, {}).get('captured_shiny', False)

class Item(object):
    """
    Representation of an item.
    """
    def __init__(self, item_id, item_count):
        """
        Representation of an item
        :param item_id: ID of the item
        :type item_id: int
        :param item_count: Quantity of the item
        :type item_count: int
        :return: An item
        :rtype: Item
        """
        self.id = item_id
        self.name = Items.name_for(self.id)
        self.count = item_count

    def remove(self, amount):
        """
        Remove a specified amount of an item from the cached inventory.
        Note that it does **NOT** removes it in the server, it only removes it from the local cached inventory.
        :param amount: Amount to remove
        :type amount: int
        :return: Nothing
        :rtype: None
        """
        if self.count < amount:
            raise Exception('Tried to remove more {} than you have'.format(self.name))
        self.count -= amount

    def recycle(self, amount_to_recycle):
        """
        Recycle (discard) the specified amount of item from the item inventory.
        It is making a call to the server to request a recycling as well as updating the cached inventory.
        :param amount_to_recycle: The amount to recycle.
        :type amount_to_recycle: int
        :return: Returns whether or not the task went well
        :rtype: worker_result.WorkerResult
        """
        if self.count < amount_to_recycle:
            raise Exception('Tried to remove more {} than you have'.format(self.name))

        item_recycler = ItemRecycler(_inventory.bot, self, amount_to_recycle)
        item_recycler_work_result = item_recycler.work()

        if item_recycler.is_recycling_success():
            self.remove(amount_to_recycle)

        return item_recycler_work_result

    def add(self, amount):
        """
        Add a specified amount of the item to the local cached inventory
        :param amount: Amount to add
        :type amount: int
        :return: Nothing.
        :rtype: None
        """
        if amount < 0:
            raise Exception('Must add positive amount of {}'.format(self.name))
        self.count += amount

    def __str__(self):
        return self.name + " : " + str(self.count)

class Items(_BaseInventoryComponent):
    TYPE = 'item'
    ID_FIELD = 'item_id'
    STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json')

    def parse(self, item_data):
        """
        Make an instance of an Item from raw item data.
        :param item_data: Item data to make an item from
        :return: Instance of the Item.
        :rtype: Item
        """
        item_id = item_data.get(Items.ID_FIELD, None)
        item_count = item_data['count'] if 'count' in item_data else 0
        return Item(item_id, item_count)

    def all(self):
        """
        Get EVERY Item from the cached inventory.
        :return: List of evey item in the cached inventory
        :rtype: list of Item
        """
        return list(self._data.values())

    def get(self, item_id):
        """
        Get ONE Item from the cached inventory.
        :param item_id: Item's ID to search for.
        :return: Instance of the item from the cached inventory
        :rtype: Item
        """
        return self._data.setdefault(item_id, Item(item_id, 0))

    @classmethod
    def name_for(cls, item_id):
        """
        Search the name for an item from its ID.
        :param item_id: Item's ID to search for.
        :return: Item's name.
        :rtype: str
        """
        return cls.STATIC_DATA[str(item_id)]

    @classmethod
    def get_space_used(cls):
        """
        Counts the space used in item inventory.
        :return: The space used in item inventory.
        :rtype: int
        """
        space_used = 1
        for item_in_inventory in _inventory.items.all():
            space_used += item_in_inventory.count
        return space_used

    @classmethod
    def get_space_left(cls):
        """
        Compute the space  left in item inventory.
        :return: The space left in item inventory. 0 if the player has more item than his item inventory can carry.
        :rtype: int
        """
        _inventory.retrieve_inventories_size()
        space_left = _inventory.item_inventory_size - cls.get_space_used()
        # Space left should never be negative. Returning 0 if the computed value is negative.
        return space_left if space_left >= 0 else 0

    @classmethod
    def has_space_for_loot(cls):
        """
        Returns a value indicating whether or not the item inventory has enough space to loot a fort
        :return: True if the item inventory has enough space; otherwise, False.
        :rtype: bool
        """
        max_number_of_items_looted_at_stop = 5
        return cls.get_space_left() >= max_number_of_items_looted_at_stop

class AppliedItem(object):
    """
    Representation of an applied item, like incense.
    """
    def __init__(self, item_id, expire_ms, applied_ms):
        """
        Representation of an applied item
        :param item_id: ID of the item
        :type item_id: int
        :param expire_ms: expire in ms
        :type expire_ms: in
        :param applied_ms: applied at
        :type applied_ms: int
        :return: An applied item
        :rtype: AppliedItemItem
        """
        self.id = item_id
        self.name = Items.name_for(self.id)
        self.applied_ms = applied_ms
        self.expire_ms = expire_ms

    def refresh(self,inventory):
        self.retrieve_data(inventory)

    def parse(self, item):
        if not item:
            item = {}

        self.id = item.get('id', 0)
        self.name = Items.name_for(self.id)
        self.expire_ms = item.get('expire_ms', 0)
        self.applied_ms = item.get('applied_ms', 0)

    def retrieve_data(self, inventory):
        ret = {}
        for item in inventory:
            data = item['inventory_item_data']
            if self.TYPE in data:
                item = data[self.TYPE]
                ret = item
                self.parse(item)

        return ret

    def __str__(self):
        return self.name

class AppliedItems(_BaseInventoryComponent):
    TYPE='applied_items'
    ID_FIELD = 'item_id'
    STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'items.json')

    def all(self):
        """
        Get EVERY Item from the cached inventory.
        :return: List of evey item in the cached inventory
        :rtype: list of Item
        """
        return list(self._data.values())

    def get(self, item_id):
        """
        Get ONE Item from the cached inventory.
        :param item_id: Item's ID to search for.
        :return: Instance of the item from the cached inventory
        :rtype: Item
        """
        return self._data.setdefault(item_id, Item(item_id, 0))

    @classmethod
    def name_for(cls, item_id):
        """
        Search the name for an item from its ID.
        :param item_id: Item's ID to search for.
        :return: Item's name.
        :rtype: str
        """
        return cls.STATIC_DATA[str(item_id)]

class Pokemons(_BaseInventoryComponent):
    TYPE = 'pokemon_data'
    ID_FIELD = 'id'
    STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'pokemon.json')

    @classmethod
    def process_static_data(cls, data):
        data = [PokemonInfo(d) for d in data]

        # process evolution info
        for p in data:
            next_all = p.next_evolutions_all
            if len(next_all) <= 0:
                continue

            # only next level evolutions, not all possible
            p.next_evolution_ids = [idx for idx in next_all
                                    if data[idx-1].prev_evolution_id == p.id]

            # only final evolutions
            p.last_evolution_ids = [idx for idx in next_all
                                    if not data[idx-1].has_next_evolution]
            assert len(p.last_evolution_ids) > 0

        return data

    @classmethod
    def get_space_used(cls):
        """
        Counts the space used in pokemon inventory.
        :return: The space used in pokemon inventory.
        :rtype: int
        """
        return len(_inventory.pokemons.all_with_eggs())

    @classmethod
    def get_space_left(cls):
        """
        Compute the space  left in pokemon inventory.
        :return: The space left in pokemon inventory.
        :rtype: int
        """
        _inventory.retrieve_inventories_size()
        space_left = _inventory.pokemon_inventory_size - cls.get_space_used()
        return space_left

    @classmethod
    def data_for(cls, pokemon_id):
        # type: (int) -> PokemonInfo
        return cls.STATIC_DATA[pokemon_id - 1]

    @classmethod
    def name_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).name
    @classmethod
    def candyid_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).candyid

    @classmethod
    def id_for(cls, pokemon_name):
        # TODO: Use a better searching algorithm. This one is O(n)
        for data in cls.STATIC_DATA:
            if data.name.lower() == pokemon_name.lower():
                return data.id

        raise Exception('Could not find pokemon named {}'.format(pokemon_name))

    @classmethod
    def first_evolution_id_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).first_evolution_id

    @classmethod
    def prev_evolution_id_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).prev_evolution_id

    @classmethod
    def next_evolution_ids_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).next_evolution_ids

    @classmethod
    def last_evolution_ids_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).last_evolution_ids

    @classmethod
    def has_next_evolution(cls, pokemon_id):
        return cls.data_for(pokemon_id).has_next_evolution

    @classmethod
    def evolution_cost_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).evolution_cost

    @classmethod
    def evolution_item_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).evolution_item

    @classmethod
    def evolution_items_needed_for(cls, pokemon_id):
        return cls.data_for(pokemon_id).evolution_item_needed

    def parse(self, item):
        if 'is_egg' in item:
            return Egg(item)
        return Pokemon(item)

    def all(self):
        # by default don't include eggs in all pokemon (usually just
        # makes caller's lives more difficult)
        return [p for p in super(Pokemons, self).all() if not isinstance(p, Egg)]

    def all_with_eggs(self):
        # count pokemon AND eggs, since eggs are counted as bag space
        return super(Pokemons, self).all()

    def add(self, pokemon):
        if pokemon.unique_id <= 0:
            raise ValueError("Can't add a pokemon without id")
        if pokemon.unique_id in self._data:
            raise ValueError("Pokemon already present in the inventory")
        self._data[pokemon.unique_id] = pokemon

    def get_from_unique_id(self, pokemon_unique_id):
        if pokemon_unique_id not in self._data:
            raise ValueError("Pokemon not present in the inventory")
        return self._data[pokemon_unique_id]

    def remove(self, pokemon_unique_id):
        if pokemon_unique_id not in self._data:
            raise ValueError("Pokemon not present in the inventory")
        self._data.pop(pokemon_unique_id)

#
# Static Components

class Types(_StaticInventoryComponent):
    """
    Types of attacks and pokemons

    See more information:
    https://i.redd.it/oy7lrixl8r9x.png
    https://www.reddit.com/r/TheSilphRoad/comments/4t8seh/pokemon_go_type_advantage_chart/
    https://github.com/jehy/Pokemon-Go-Weakness-calculator/blob/master/app/src/main/java/ru/jehy/pokemonweaknesscalculator/MainActivity.java#L31
    """

    STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'types.json')

    @classmethod
    def process_static_data(cls, data):
        # create instances
        ret = OrderedDict()
        for t in sorted(data, key=lambda x: x["name"]):
            name = str(t["name"])
            ret[name] = Type(name, t["effectiveAgainst"], t["weakAgainst"])

        # additional manipulations
        size = len(ret)
        by_effectiveness = {}
        by_resistance = {}
        for t in ret.itervalues():  # type: Type
            t.attack_effective_against = [ret[name] for name in t.attack_effective_against]
            t.attack_weak_against = [ret[name] for name in t.attack_weak_against]

            # group types effective against, weak against specific types
            for l, d in (t.attack_effective_against, by_effectiveness), \
                        (t.attack_weak_against, by_resistance):
                for tt in l:
                    if tt not in d:
                        d[tt] = set()
                    d[tt].add(t)

            # calc average factor for damage of this type relative to all types
            t.rate = (size
                      + ((EFFECTIVENESS_FACTOR-1) * len(t.attack_effective_against))
                      - ((1-RESISTANCE_FACTOR) * len(t.attack_weak_against))) / size

        # set pokemon type resistance/weakness info
        for t in ret.itervalues():  # type: Type
            t.pokemon_resistant_to = by_resistance[t]
            t.pokemon_vulnerable_to = by_effectiveness[t]

        return ret

    @classmethod
    def get(cls, type_name):
        # type: (Union[string, Type]) -> Type
        type_name = str(type_name)
        if type_name not in cls.STATIC_DATA:
            raise ValueError("Unknown type: {}".format(type_name))
        return cls.STATIC_DATA[type_name]

    @classmethod
    def all(cls):
        return cls.STATIC_DATA.values()

    @classmethod
    def rating(cls):
        return sorted(cls.all(), key=lambda x: x.rate, reverse=True)

class LevelToCPm(_StaticInventoryComponent):
    """
    Data for the CP multipliers at different levels
    See http://pokemongo.gamepress.gg/cp-multiplier
    See https://github.com/justinleewells/pogo-optimizer/blob/edd692d/data/game/level-to-cpm.json
    """

    STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'level_to_cpm.json')
    MAX_LEVEL = 40
    MAX_CPM = .0

    @classmethod
    def init_static_data(cls):
        super(LevelToCPm, cls).init_static_data()
        cls.MAX_CPM = cls.cp_multiplier_for(cls.MAX_LEVEL)
        assert cls.MAX_CPM > .0

    @classmethod
    def cp_multiplier_for(cls, level):
        return cls.STATIC_DATA[int(2 * (level - 1))]

    @classmethod
    def level_from_cpm(cls, cp_multiplier):
        return min(range(len(cls.STATIC_DATA)), key=lambda i: abs(cls.STATIC_DATA[i] - cp_multiplier)) * 0.5 + 1

class _Attacks(_StaticInventoryComponent):
    BY_NAME = {}  # type: Dict[string, Attack]
    BY_TYPE = {}  # type: Dict[List[Attack]]
    BY_DPS = []  # type: List[Attack]

    @classmethod
    def process_static_data(cls, moves):
        ret = {}
        by_type = {}
        by_name = {}
        fast = cls is FastAttacks
        for attack in moves:
            attack = Attack(attack) if fast else ChargedAttack(attack)
            ret[attack.id] = attack
            by_name[attack.name] = attack

            attack_type = str(attack.type)
            if attack_type not in by_type:
                by_type[attack_type] = []
            by_type[attack_type].append(attack)

        for t in by_type.iterkeys():
            attacks = sorted(by_type[t], key=lambda m: m.dps, reverse=True)
            min_dps = attacks[-1].dps
            max_dps = attacks[0].dps - min_dps
            if max_dps > .0:
                for attack in attacks:  # type: Attack
                    attack.rate_in_type = (attack.dps - min_dps) / max_dps
            by_type[t] = attacks

        cls.BY_NAME = by_name
        cls.BY_TYPE = by_type
        cls.BY_DPS = sorted(ret.values(), key=lambda m: m.dps, reverse=True)

        return ret

    @classmethod
    def data_for(cls, attack_id):
        # type: (int) -> Attack
        if attack_id not in cls.STATIC_DATA:
            raise ValueError("Attack {} not found in {}".format(
                attack_id, cls.__name__))
        return cls.STATIC_DATA[attack_id]

    @classmethod
    def by_name(cls, name):
        # type: (string) -> Attack
        return cls.BY_NAME[name]

    @classmethod
    def list_for_type(cls, type_name):
        # type: (Union[string, Type]) -> List[Attack]
        """
        :return: Attacks sorted by DPS in descending order
        """
        return cls.BY_TYPE[str(type_name)]

    @classmethod
    def all(cls):
        return cls.STATIC_DATA.values()

    @classmethod
    def all_by_dps(cls):
        return cls.BY_DPS

class FastAttacks(_Attacks):
    STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'fast_moves.json')

class ChargedAttacks(_Attacks):
    STATIC_DATA_FILE = os.path.join(_base_dir, 'data', 'charged_moves.json')

#
# Instances

class Type(object):
    def __init__(self, name, effective_against, weak_against):
        # type: (string, Iterable[Type], Iterable[Type]) -> None

        self.name = name

        # effective way to represent type with one character
        # for example it's very useful for nicknaming pokemon
        #  using its attack types
        #
        # if first char is unique - use it, in other case
        #  use suitable substitute
        type_to_one_char_map = {
            'Bug': 'B',
            'Dark': 'K',
            'Dragon': 'D',
            'Electric': 'E',
            'Fairy': 'Y',
            'Fighting': 'T',
            'Fire': 'F',
            'Flying': 'L',
            'Ghost': 'H',
            'Grass': 'A',
            'Ground': 'G',
            'Ice': 'I',
            'Normal': 'N',
            'Poison': 'P',
            'Psychic': 'C',
            'Rock': 'R',
            'Steel': 'S',
            'Water': 'W',
        }
        self.as_one_char = type_to_one_char_map[name]

        # attack of this type is effective against ...
        self.attack_effective_against = set(effective_against)
        # attack of this type is weak against ...
        self.attack_weak_against = set(weak_against)
        # pokemon of this type is resistant to ...
        self.pokemon_resistant_to = set()  # type: Set[Type]
        # pokemon of this type is vulnerable to ...
        self.pokemon_vulnerable_to = set()  # type: Set[Type]

        # average factor for damage of this type relative to all types
        self.rate = 1.

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

class Candy(object):
    def __init__(self, family_id, quantity):
        self.type = Pokemons.name_for(family_id)
        self.quantity = quantity

    def consume(self, amount):
        if self.quantity < amount:
            raise Exception('Tried to consume more {} candy than you have'.format(self.type))
        self.quantity -= amount

    def add(self, amount):
        if amount < 0:
            raise Exception('Must add positive amount of candy')
        self.quantity += amount

class Egg(object):
    def __init__(self, data):
        self._data = data

    def has_next_evolution(self):
        return False

class PokemonInfo(object):
    """
    Static information about pokemon kind
    """

    def __init__(self, data):
        self._data = data
        self.id = int(data["Number"])
        self.name = data['Name']  # type: string
        self.classification = data['Classification']  # type: string

        # prepare types
        self.type1 = Types.get(data['Type I'][0])
        self.type2 = None
        self.types = [self.type1]  # required type
        for t in data.get('Type II', []):
            self.type2 = Types.get(t)
            self.types.append(self.type2)  # second type
            break

        # base chance to capture pokemon
        self.capture_rate = data['CaptureRate']
        # chance of the pokemon to flee away
        self.flee_rate = data['FleeRate']

        # km needed for buddy reward
        self.buddy_distance_needed = data['BuddyDistanceNeeded']

        # prepare attacks (moves)
        self.fast_attacks = self._process_attacks()
        self.charged_attack = self._process_attacks(charged=True)

        # prepare movesets
        self.movesets = self._process_movesets()

        # Basic Values of the pokemon (identical for all pokemons of one kind)
        self.base_attack = data['BaseAttack']
        self.base_defense = data['BaseDefense']
        self.base_stamina = data['BaseStamina']

        # calculate maximum CP for the pokemon (best IVs, lvl 40)
        self.max_cp = _calc_cp(self.base_attack, self.base_defense,
                               self.base_stamina)

        #
        # evolutions info for this pokemon

        # id of the very first level evolution
        self.first_evolution_id = self.id
        # id of the previous evolution (one level only)
        self.prev_evolution_id = None
        # ids of all available previous evolutions in the family
        self.prev_evolutions_all = []
        if 'Previous evolution(s)' in data:
            ids = [int(e['Number']) for e in data['Previous evolution(s)']]
            self.first_evolution_id = ids[0]
            self.prev_evolution_id = ids[-1]
            self.prev_evolutions_all = ids

        # Number of candies for the next evolution (if possible)
        self.evolution_cost = 0
        # Next evolution doesn't need a special item
        self.evolution_item = None
        self.evolution_item_needed = 0
        # next evolution flag
        self.has_next_evolution = 'Next evolution(s)' in data \
                                  or 'Next Evolution Requirements' in data
        # ids of the last level evolutions
        self.last_evolution_ids = [self.id]
        # ids of the next possible evolutions (one level only)
        self.next_evolution_ids = []
        #candies
        self.candyid = int(data['Candy']['FamilyID'])
        self.candyName = (data['Candy']['Name'])
        self.next_evolutions_all = []
        if self.has_next_evolution:
            ids = [int(e['Number']) for e in data['Next evolution(s)']]
            self.next_evolutions_all = ids
            self.evolution_cost = int(data['Next Evolution Requirements']['Amount'])
            if 'EvoItem' in data['Next Evolution Requirements']:
                self.evolution_item = int(data['Next Evolution Requirements']['EvoItem'])
                self.evolution_item_needed = int(data['Next Evolution Requirements']['EvoItemNeeded'])

    @property
    def family_id(self):
        return self.first_evolution_id

    @property
    def is_seen(self):
        return pokedex().seen(self.id)

    @property
    def is_captured(self):
        return pokedex().captured(self.id)

    def _process_movesets(self):
        # type: () -> List[Moveset]
        """
        The optimal moveset is the combination of two moves, one quick move
        and one charge move, that deals the most damage over time.

        Because each quick move gains a certain amount of energy (different
        for different moves) and each charge move requires a different amount
        of energy to use, sometimes, a quick move with lower DPS will be
        better since it charges the charge move faster.  On the same note,
        sometimes a charge move that has lower DPS will be more optimal since
        it may require less energy or it may last for a longer period of time.

        Attacker have STAB (Same-type attack bonus - x1.25) pokemon have the
        same type as attack. So we add it to the "Combo DPS" of the moveset.

        The defender attacks in intervals of 1 second for the first 2 attacks,
        and then in intervals of 2 seconds for the remainder of the attacks.
        This explains why we see two consecutive quick attacks at the beginning
        of the match.  As a result, we add +2 seconds to the DPS calculation
        for defender DPS output.

        So to determine an optimal defensive moveset, we follow the same  method
        as we did for optimal offensive movesets, but instead calculate  the
        highest "Combo DPS" with an added 2 seconds to the quick move cool down.

        Note: critical hits have not yet been implemented in the game

        See http://pokemongo.gamepress.gg/optimal-moveset-explanation
        See http://pokemongo.gamepress.gg/defensive-tactics
        """

        # Prepare movesets
        movesets = []
        for fm in self.fast_attacks:
            for chm in self.charged_attack:
                movesets.append(Moveset(fm, chm, self.types, self.id))
        assert len(movesets) > 0

        # Calculate attack perfection for each moveset
        movesets = sorted(movesets, key=lambda m: m.dps_attack)
        worst_dps = movesets[0].dps_attack
        best_dps = movesets[-1].dps_attack
        if best_dps > worst_dps:
            for moveset in movesets:
                current_dps = moveset.dps_attack
                moveset.attack_perfection = \
                    (current_dps - worst_dps) / (best_dps - worst_dps)

        # Calculate defense perfection for each moveset
        movesets = sorted(movesets, key=lambda m: m.dps_defense)
        worst_dps = movesets[0].dps_defense
        best_dps = movesets[-1].dps_defense
        if best_dps > worst_dps:
            for moveset in movesets:
                current_dps = moveset.dps_defense
                moveset.defense_perfection = \
                    (current_dps - worst_dps) / (best_dps - worst_dps)

        return sorted(movesets, key=lambda m: m.dps, reverse=True)

    def _process_attacks(self, charged=False):
        # type: (bool) -> List[Attack]
        key = 'Fast Attack(s)' if not charged else 'Special Attack(s)'
        moves_dict = (ChargedAttacks if charged else FastAttacks).BY_NAME
        moves = []
        for name in self._data[key]:
            if name not in moves_dict:
                raise KeyError('Unknown {} attack: "{}"'.format(
                    'charged' if charged else 'fast', name))
            moves.append(moves_dict[name])
        moves = sorted(moves, key=lambda m: m.dps, reverse=True)
        assert len(moves) > 0
        return moves

class Pokemon(object):
    def __init__(self, data):
        self._data = data
        # Unique ID for this particular Pokemon
        self.unique_id = data.get('id', 0)
        # Let's try this
        self.encounter_id = data.get('encounter_id')
        # Id of the such pokemons in pokedex
        self.pokemon_id = data['pokemon_id']
        # Static information
        self.static = Pokemons.data_for(self.pokemon_id)

        # Shiny information
        self.display_data = data.get('pokemon_display')
        self.shiny = self.display_data.get('shiny', False)
        # self.form = self.display_data.get('form', )

        # Combat points value
        self.cp = data['cp']
        # Base CP multiplier, fixed at the catch time
        self.cp_bm = data['cp_multiplier']
        # Changeable part of the CP multiplier, increasing at power up
        self.cp_am = data.get('additional_cp_multiplier', .0)
        # Resulting CP multiplier
        self.cp_m = self.cp_bm + self.cp_am

        # Current pokemon level (half of level is a normal value)
        self.level = LevelToCPm.level_from_cpm(self.cp_m)

        if 'level' not in self._data:
            self._data['level'] = self.level

        # Maximum health points
        self.hp_max = data['stamina_max']
        # Current health points
        self.hp = data.get('stamina', 0) #self.hp_max)
        assert 0 <= self.hp <= self.hp_max

        # Individial Values of the current specific pokemon (different for each)
        self.iv_attack = data.get('individual_attack', 0)
        self.iv_defense = data.get('individual_defense', 0)
        self.iv_stamina = data.get('individual_stamina', 0)

        # Basic Values of the pokemon (identical for all pokemons of one kind)
        base_attack = self.static.base_attack
        base_defense = self.static.base_defense
        base_stamina = self.static.base_stamina

        self.name = self.static.name
        self.nickname_raw = data.get('nickname', '')
        self.nickname = self.nickname_raw or self.name

        self.in_fort = 'deployed_fort_id' in data
        if 'deployed_fort_id' in data:
            self.fort_id = data['deployed_fort_id']

        self.is_favorite = data.get('favorite', 0) is 1
        self.buddy_candy = data.get('buddy_candy_awarded', 0)
        self.is_bad = data.get('is_bad', False)
        self.buddy_distance_needed = self.static.buddy_distance_needed

        self.fast_attack = FastAttacks.data_for(data['move_1'])
        self.charged_attack = ChargedAttacks.data_for(data['move_2'])  # type: ChargedAttack

        # Individial values (IV) perfection percent
        self.iv = self._compute_iv_perfection()

        # IV CP perfection - kind of IV perfection percent but calculated
        #  using weight of each IV in its contribution to CP of the best
        #  evolution of current pokemon
        # So it tends to be more accurate than simple IV perfection
        self.ivcp = self._compute_cp_perfection()

        # Exact value of current CP (not rounded)
        self.cp_exact = _calc_cp(
            base_attack, base_defense, base_stamina,
            self.iv_attack, self.iv_defense, self.iv_stamina, self.cp_m)
        #assert max(int(self.cp_exact), 10) == self.cp

        # Percent of maximum possible CP
        self.cp_percent = self.cp_exact / self.static.max_cp

        # Get moveset instance with calculated DPS and perfection percents
        self.moveset = self._get_moveset()

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

    def update_nickname(self, new_nickname):
        self.nickname_raw = new_nickname
        self.nickname = self.nickname_raw or self.name

    def can_evolve_now(self):
        if self.evolution_item is None:
            return self.has_next_evolution() and \
                self.candy_quantity >= self.evolution_cost
        else:
            evo_items = items().get(self.evolution_item).count
            return self.has_next_evolution() and \
                self.candy_quantity >= self.evolution_cost and \
                evo_items >= self.evolution_items_needed

    def has_next_evolution(self):
        return self.static.has_next_evolution

    def has_seen_next_evolution(self):
        for pokemon_id in self.next_evolution_ids:
            if pokedex().captured(pokemon_id):
                return True
        return False

    @property
    def family_id(self):
        return self.static.family_id

    @property
    def first_evolution_id(self):
        return self.static.first_evolution_id

    @property
    def prev_evolution_id(self):
        return self.static.prev_evolution_id

    @property
    def next_evolution_ids(self):
        return self.static.next_evolution_ids

    @property
    def last_evolution_ids(self):
        return self.static.last_evolution_ids

    @property
    def candy_quantity(self):
        return candies().get(self.pokemon_id).quantity

    @property
    def evolution_cost(self):
        return self.static.evolution_cost

    @property
    def evolution_item(self):
        return self.static.evolution_item

    @property
    def evolution_items_needed(self):
        return self.static.evolution_item_needed

    @property
    def iv_display(self):
        return '{}/{}/{}'.format(self.iv_attack, self.iv_defense, self.iv_stamina)

    def _compute_iv_perfection(self):
        total_iv = self.iv_attack + self.iv_defense + self.iv_stamina
        iv_perfection = round((total_iv / 45.0), 2)
        return iv_perfection

    def _compute_cp_perfection(self):
        """
        CP perfect percent is more accurate than IV perfect

        We know attack plays an important role in CP, and different
        pokemons have different base value, that's means 15/14/15 is
        better than 14/15/15 for lot of pokemons, and if one pokemon's
        base def is more than base sta, 15/15/14 is better than 15/14/15.

        See https://github.com/jabbink/PokemonGoBot/issues/469

        So calculate CP perfection at final level for the best of the final
        evolutions of the pokemon.
        """
        variants = []
        iv_attack = self.iv_attack
        iv_defense = self.iv_defense
        iv_stamina = self.iv_stamina
        cp_m = LevelToCPm.MAX_CPM
        last_evolution_ids = self.last_evolution_ids
        for pokemon_id in last_evolution_ids:
            poke_info = Pokemons.data_for(pokemon_id)
            base_attack = poke_info.base_attack
            base_defense = poke_info.base_defense
            base_stamina = poke_info.base_stamina

            # calculate CP variants at maximum level
            worst_cp = _calc_cp(base_attack, base_defense, base_stamina,
                                0, 0, 0, cp_m)
            perfect_cp = _calc_cp(base_attack, base_defense, base_stamina,
                                  cp_multiplier=cp_m)
            current_cp = _calc_cp(base_attack, base_defense, base_stamina,
                                  iv_attack, iv_defense, iv_stamina, cp_m)
            cp_perfection = (current_cp - worst_cp) / (perfect_cp - worst_cp)
            variants.append(cp_perfection)

        # get best value (probably for the best evolution)
        cp_perfection = max(variants)
        return cp_perfection

    def _get_moveset(self):
        move1 = self.fast_attack
        move2 = self.charged_attack
        movesets = self.static.movesets
        current_moveset = None
        for moveset in movesets:  # type: Moveset
            if moveset.fast_attack == move1 and moveset.charged_attack == move2:
                current_moveset = moveset
                break

        if current_moveset is None:
            error = "Unexpected moveset [{}, {}] for #{} {}," \
                    " please update info in pokemon.json and create issue/PR" \
                .format(move1, move2, self.pokemon_id, self.name)
            # raise ValueError(error)
            logging.getLogger(type(self).__name__).error(error)
            current_moveset = Moveset(
                move1, move2, self.static.types, self.pokemon_id)

        return current_moveset

class Attack(object):
    def __init__(self, data):
        # self._data = data  # Not needed - all saved in fields
        self.id = data['id']
        self.name = data['name']
        self.type = Types.get(data['type'])
        self.damage = data['damage']
        self.duration = data['duration'] / 1000.0  # duration in seconds

        # Energy addition for fast attack
        # Energy cost for charged attack
        self.energy = data['energy']

        # Damage Per Second
        # recalc for better precision
        self.dps = self.damage / self.duration

        # Perfection of the attack in it's type (from 0 to 1)
        self.rate_in_type = .0

    @property
    def damage_with_stab(self):
        # damage with STAB (Same-type attack bonus)
        return self.damage * STAB_FACTOR

    @property
    def dps_with_stab(self):
        # DPS with STAB (Same-type attack bonus)
        return self.dps * STAB_FACTOR

    @property
    def effective_against(self):
        return self.type.attack_effective_against

    @property
    def weak_against(self):
        return self.type.attack_weak_against

    @property
    def energy_per_second(self):
        return self.energy / self.duration

    @property
    def dodge_window(self):
        # TODO:  Attack Dodge Window
        return NotImplemented

    @property
    def is_charged(self):
        return False

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

class ChargedAttack(Attack):
    def __init__(self, data):
        super(ChargedAttack, self).__init__(data)

    @property
    def is_charged(self):
        return True

class Moveset(object):
    def __init__(self, fm, chm, pokemon_types=(), pokemon_id=-1):
        # type: (Attack, ChargedAttack, List[Type], int) -> None
        if len(pokemon_types) <= 0 < pokemon_id:
            pokemon_types = Pokemons.data_for(pokemon_id).types

        self.pokemon_id = pokemon_id
        self.fast_attack = fm
        self.charged_attack = chm

        # See Pokemons._process_movesets()
        # See http://pokemongo.gamepress.gg/optimal-moveset-explanation
        # See http://pokemongo.gamepress.gg/defensive-tactics

        fm_number = 100  # for simplicity we use 100

        fm_energy = fm.energy * fm_number
        fm_damage = fm.damage * fm_number
        fm_secs = fm.duration * fm_number

        # Defender attacks in intervals of 1 second for the
        #   first 2 attacks, and then in intervals of 2 seconds
        # So add 1.95 seconds to the quick move cool down for defense
        #   1.95 is something like an average here
        #   TODO: Do something better?
        fm_defense_secs = (fm.duration + 1.95) * fm_number

        chm_number = fm_energy / chm.energy
        chm_damage = chm.damage * chm_number
        chm_secs = chm.duration * chm_number

        damage_sum = fm_damage + chm_damage
        # raw Damage-Per-Second for the moveset
        self.dps = damage_sum / (fm_secs + chm_secs)
        # average DPS for defense
        self.dps_defense = damage_sum / (fm_defense_secs + chm_secs)

        # apply STAB (Same-type attack bonus)
        if fm.type in pokemon_types:
            fm_damage *= STAB_FACTOR
        if chm.type in pokemon_types:
            chm_damage *= STAB_FACTOR

        # DPS for attack (counting STAB)
        self.dps_attack = (fm_damage + chm_damage) / (fm_secs + chm_secs)

        # Moveset perfection percent for attack and for defense
        # Calculated for current pokemon kind only, not between all pokemons
        # So 100% perfect moveset can be weak if pokemon is weak (e.g. Caterpie)
        self.attack_perfection = .0
        self.defense_perfection = .0

        # TODO: True DPS for real combat (floor(Attack/200 * MovePower * STAB) + 1)
        # See http://pokemongo.gamepress.gg/pokemon-attack-explanation

    def __str__(self):
        return '[{}, {}]'.format(self.fast_attack, self.charged_attack)

    def __repr__(self):
        return '[{}, {}]'.format(self.fast_attack, self.charged_attack)

class Inventory(object):
    def __init__(self, bot):
        self.bot = bot
        self.last_holo_timestamp_ms = 0
        self.last_timestamp_ms = 0
        self.pokedex = Pokedex()
        self.candy = Candies()
        self.items = Items()
        self.applied_items = AppliedItems()
        self.pokemons = Pokemons()
        self.player = Player(self.bot)  # include inventory inside Player?
        self.egg_incubators = None
        self.refresh()
        self.item_inventory_size = None
        self.pokemon_inventory_size = None

    def refresh(self, inventory=None):
        if inventory is None:
            request = self.bot.api.create_request()
            request.get_holo_inventory(last_timestamp_ms=self.last_timestamp_ms)
            inventory = request.call()
            if 'inventory_delta' not in inventory['responses']['GET_HOLO_INVENTORY']:
                self.bot.logger.info("No player information found, possiblity of temp ban")
                sys.exit(0)
            else:
                self.last_holo_timestamp_ms = (inventory['responses']
                                                ['GET_HOLO_INVENTORY']
                                                ['inventory_delta']
                                                .get('new_timestamp_ms', 0))
        else:
            request = self.bot.api.create_request()
            request.get_inventory(last_timestamp_ms=self.last_timestamp_ms)
            inventory = request.call()
            print(format(inventory))
            self.last_timestamp_ms = (inventory['responses']
                                    ['GET_INVENTORY']
                                    ['inventory_delta']
                                    .get('new_timestamp_ms', 0))

        if 'GET_HOLO_INVENTORY' in inventory['responses']:
            inventory = inventory['responses']['GET_HOLO_INVENTORY']['inventory_delta']['inventory_items']
        else:
            inventory = inventory['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']

        for i in (self.pokedex, self.candy, self.items, self.pokemons, self.player):
            i.refresh(inventory)

        # self.applied_items = [x["inventory_item_data"] for x in inventory if "applied_items" in x["inventory_item_data"]]
        self.egg_incubators = [x["inventory_item_data"] for x in inventory if "egg_incubators" in x["inventory_item_data"]]

        self.update_web_inventory()

    def init_inventory_outfile(self):
        web_inventory = os.path.join(_base_dir, "web", "inventory-%s.json" % self.bot.config.username)

        if not os.path.exists(web_inventory):
            self.bot.logger.info('No inventory file %s found. Creating a new one' % web_inventory)

            json_inventory = []

            with open(web_inventory, "w") as outfile:
                json.dump(json_inventory, outfile)

    def update_web_inventory(self):
        web_inventory = os.path.join(_base_dir, "web", "inventory-%s.json" % self.bot.config.username)

        if not os.path.exists(web_inventory):
            self.init_inventory_outfile()

        json_inventory = self.jsonify_inventory()

        try:
            with open(web_inventory, "w") as outfile:
                json.dump(json_inventory, outfile)
        except (IOError, ValueError) as e:
            self.bot.logger.info('[x] Error while opening inventory file for write: %s' % e, 'red')
        except:
            raise FileIOException("Unexpected error writing to {}".web_inventory)

    def jsonify_inventory(self):
        json_inventory = []

        json_inventory.append({"inventory_item_data": {"player_stats": self.player.player_stats}})

        for pokedex in self.pokedex.all():
            json_inventory.append({"inventory_item_data": {"pokedex_entry": pokedex}})

        for family_id, candy in self.candy._data.items():
            json_inventory.append({"inventory_item_data": {"candy": {"family_id": family_id, "candy": candy.quantity}}})

        for item_id, item in self.items._data.items():
            json_inventory.append({"inventory_item_data": {"item": {"item_id": item_id, "count": item.count}}})

        for pokemon in self.pokemons.all_with_eggs():
            json_inventory.append({"inventory_item_data": {"pokemon_data": pokemon._data}})

        for inc in self.egg_incubators:
            json_inventory.append({"inventory_item_data": inc})

        # for item in self.applied_items:
            # json_inventory.append({"inventory_applied_item_data": {"applied_item": {"item_id": item.item_id, "applied_ms": item.applied_ms, "expire_ms": item.expire_ms}}})

        return json_inventory

    def retrieve_inventories_size(self):
        """
        Retrieves the item inventory size
        :return: Nothing.
        :rtype: None
        """
        # TODO: Force to update it if the player upgrades its size
        if self.item_inventory_size is None or self.pokemon_inventory_size is None:
            request = self.bot.api.create_request()
            request.get_player()
            player_data = request.call()['responses']['GET_PLAYER']['player_data']

            self.item_inventory_size = player_data['max_item_storage']
            self.pokemon_inventory_size = player_data['max_pokemon_storage']

#
# Other

# STAB (Same-type attack bonus)
# Factor applied to attack of the same type as pokemon
STAB_FACTOR = 1.2
# Factor applied to attack when it's effective against defending pokemon type
EFFECTIVENESS_FACTOR = 1.4
# Factor applied to attack when it's weak against defending pokemon type
RESISTANCE_FACTOR = 0.714
IMMUNITY_FACTOR = 0.51

_inventory = None  # type: Inventory

def _calc_cp(base_attack, base_defense, base_stamina,
             iv_attack=15, iv_defense=15, iv_stamina=15,
             cp_multiplier=.0):
    """
    CP calculation

    CP = (Attack * Defense^0.5 * Stamina^0.5 * CP_Multiplier^2) / 10
    CP = (BaseAtk+AtkIV) * (BaseDef+DefIV)^0.5 * (BaseStam+StamIV)^0.5 * Lvl(CPScalar)^2 / 10

    See https://www.reddit.com/r/TheSilphRoad/comments/4t7r4d/exact_pokemon_cp_formula/
    See https://www.reddit.com/r/pokemongodev/comments/4t7xb4/exact_cp_formula_from_stats_and_cpm_and_an_update/
    See http://pokemongo.gamepress.gg/pokemon-stats-advanced
    See http://pokemongo.gamepress.gg/cp-multiplier
    See http://gaming.stackexchange.com/questions/280491/formula-to-calculate-pokemon-go-cp-and-hp

    :param base_attack:   Pokemon BaseAttack
    :param base_defense:  Pokemon BaseDefense
    :param base_stamina:  Pokemon BaseStamina
    :param iv_attack:     Pokemon IndividualAttack (0..15)
    :param iv_defense:    Pokemon IndividualDefense (0..15)
    :param iv_stamina:    Pokemon IndividualStamina (0..15)
    :param cp_multiplier: CP Multiplier (0.79030001 is max - value for level 40)
    :return: CP as float
    """
    assert base_attack > 0
    assert base_defense > 0
    assert base_stamina > 0

    if cp_multiplier <= .0:
        cp_multiplier = LevelToCPm.MAX_CPM
        assert cp_multiplier > .0

    return (base_attack + iv_attack) \
        * ((base_defense + iv_defense)**0.5) \
        * ((base_stamina + iv_stamina)**0.5) \
        * (cp_multiplier ** 2) / 10

# Initialize static data in the right order
Types()  # init Types
LevelToCPm()  # init LevelToCPm
FastAttacks()  # init FastAttacks
ChargedAttacks()  # init ChargedAttacks
Pokemons()  # init Pokemons

#
# Usage helpers
# TODO : Complete the doc
# Only type return have been filled for now. It helps the IDE to suggest methods of the class.

def init_inventory(bot):
    """
    Initialises the cached inventory, retrieves data from the server.
    :param bot: Instance of the bot.
    :type bot: pokemongo_bot.PokemonGoBot
    :return: Nothing.
    :rtype: None
    """
    global _inventory
    _inventory = Inventory(bot)

def refresh_inventory(data=None):
    """
    Refreshes the cached inventory, retrieves data from the server.
    :return: Nothing.
    :rtype: None
    """
    try:
        _inventory.refresh(data)
    except AttributeError:
        print('_inventory was not initialized')

def jsonify_inventory():
    try:
        return _inventory.jsonify_inventory()
    except AttributeError:
        print('_inventory was not initialized')
        return []

def update_web_inventory():
    _inventory.update_web_inventory()

def get_item_inventory_size():
    """
    Access to the Item inventory size.
    :return: Item inventory size.
    :rtype: int
    """
    _inventory.retrieve_inventories_size()
    return _inventory.item_inventory_size

def get_pokemon_inventory_size():
    """
    Access to the Item inventory size.
    :return: Item inventory size.
    :rtype: int
    """
    _inventory.retrieve_inventories_size()
    return _inventory.pokemon_inventory_size

def pokedex():
    """

    :return:
    :rtype: Pokedex
    """
    # Are new pokemons added to the pokedex ?
    return _inventory.pokedex

def player():
    return _inventory.player

def candies():
    """

    :return:
    :rtype: Candies
    """
    return _inventory.candy

def pokemons():
    """

    :return:
    :rtype: Pokemons
    """
    return _inventory.pokemons

def items():
    """
    Access to the cached item inventory.
    :return: Instance of the cached item inventory.
    :rtype: Items
    """
    return _inventory.items

def applied_items():
    """
    Access to the cached applied item inventory.
    :return: Instance of the cached applied item inventory.
    :rtype: Items
    """
    return _inventory.applied_items

def types_data():
    """

    :return:
    :rtype: Types
    """
    return Types

def levels_to_cpm():
    """

    :return:
    :rtype: LevelToCPm
    """
    return LevelToCPm

def fast_attacks():
    """

    :return:
    :rtype: FastAttacks
    """
    return FastAttacks

def charged_attacks():
    """

    :return:
    :rtype: ChargedAttack
    """
    return ChargedAttacks


# !/usr/bin/env python
# -*- coding: utf-8 -*-

"""
@author: lishuang
@description: 利用 LSTM 预测股票价格
"""
import os
from itertools import chain

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from tensorflow.keras.layers import Dense, Dropout, LSTM
from tensorflow.keras.models import load_model, Sequential

TIME_STEPS_IN = 3
TIME_STEPS_OUT = 3
EPOCHS = 300
BATCH_SIZE = 100

def series_to_supervised(series, n_in=1, n_out=1, drop_nan=True):
    """
    将时间序列数据转换为适用于监督学习的数据
    给定输入序列和输出序列的长度

    :param series: 观察序列
    :param n_in: 观测数据 input(X) 的步长，范围是 [1, len(data)]，默认为1
    :param n_out: 观测数据 output(y) 的步长，范围为 [0, len(data)-1]，默认为1
    :param drop_nan: 是否删除 NaN 行，默认为 True
    :return: 适用于监督学习的数据集
    """
    n_vars = 1 if type(series) is list else series.shape[1]
    df = pd.DataFrame(series)
    cols, names = list(), list()
    # 输入序列 (t-n, ..., t-1)
    for i in range(n_in, 0, -1):
        cols.append(df.shift(i))
        names += [f'var{j + 1}(t-{i})' for j in range(n_vars)]

    # 预测序列 (t, t+1, ..., t+n)
    for i in range(0, n_out):
        cols.append(df.shift(-i))
        if i == 0:
            names += [f'var{j + 1}(t)' for j in range(n_vars)]
        else:
            names += [f'var{j + 1}(t+{i})' for j in range(n_vars)]

    # 拼接到一起
    agg = pd.concat(cols, axis=1)
    agg.columns = names
    if drop_nan:
        agg.dropna(inplace=True)
    return agg

def get_train_set(dataset, time_steps_in=1, time_steps_out=1):
    """
    将数据转换为可用于监督学习的数据

    :param dataset:
    :param time_steps_in:
    :param time_steps_out:
    :return:
    """
    train_dataset = np.array(dataset)
    print(train_dataset)
    reframed_train_dataset = np.array(series_to_supervised(train_dataset, time_steps_in, time_steps_out).values)
    print(reframed_train_dataset)
    train_x, train_y = reframed_train_dataset[:, :-time_steps_out], reframed_train_dataset[:, -time_steps_out:]
    # 将数据集重构为符合 LSTM 要求的数据格式，即：[样本数，时间步，特征]
    train_x = train_x.reshape(train_x.shape[0], time_steps_in, 1)
    return train_x, train_y

def plot_img(source_dataset, train_predict):
    """
    绘制图像

    :param source_dataset:
    :param train_predict:
    :return:
    """
    plt.figure(figsize=(24, 8))
    # 原始数据蓝色
    plt.plot(source_dataset[:, -1], c='b', label='actual')
    # 训练数据绿色
    plt.plot([_ for _ in train_predict], c='g', label='predict')
    plt.legend()
    plt.show()

def lstm_model(source_dataset, train_data, label_data, epochs, batch_size, time_steps_out):
    if os.path.exists('data/model.h5'):
        model = load_model('data/model.h5')
    else:
        model = Sequential()
        # 第一层，隐藏层神经元节点个数为128，返回整个序列
        model.add(
            LSTM(128, return_sequences=True, activation='relu', input_shape=(train_data.shape[1], train_data.shape[2]))
        )
        # 第二层，隐藏层神经元节点个数128，只返回序列最后一个输出
        model.add(LSTM(128, return_sequences=False))
        model.add(Dropout(0.5))
        # 第三层，因为是回归问题所以使用linear
        model.add(Dense(time_steps_out, activation='linear'))
        model.compile(loss='mean_squared_error', optimizer='adam')
        model.save('./data/model.h5', overwrite=True)

    # LSTM 训练
    # verbose=2 为每个epoch输出一行记录；
    # verbose=1 为输出进度条记录；
    # verbose=0 不在标准输出流输出日志信息
    res = model.fit(train_data, label_data, batch_size, epochs, verbose=2, shuffle=False)

    # 模型预测
    train_predict = model.predict(train_data)
    train_predict_list = list(chain(*train_predict))

    plt.plot(res.history['loss'], label='train')
    plt.show()
    print(model.summary())
    plot_img(source_dataset, train_predict)

if __name__ == '__main__':
    data = pd.read_csv('data/shanghai_index_1990_12_19_to_2020_03_12.csv', encoding='GB2312')
    data.rename(columns={'日期': 'Date', '收盘价': 'Price'}, inplace=True)
    data_set = data[['Price']].values.astype('float64')
    print(data_set)
    # 转换为可用于监督学习的数据
    train_X, label_y = get_train_set(data_set, TIME_STEPS_IN, TIME_STEPS_OUT)
    # 使用 LSTM 进行训练、预测
    lstm_model(data_set, train_X, label_y, EPOCHS, BATCH_SIZE, TIME_STEPS_OUT)


from bs4 import BeautifulSoup
import re
import requests

ENROLLMENT_BOUNDS = {
    0: [0, 5000],
    1: [5000, 15000],
    2: [15000, 30000],
    3: [30000, 10000000],
}

def get_college_basic(uni_id_list):
    where_str = ''
    order_by_str = 'ORDER BY (CASE'
    counter = 1
    for id in uni_id_list:
        id_str = f'u.university_id = {id}'
        if len(where_str) > 0:
            where_str = f'{where_str} or {id_str}'
        else:
            where_str = f'{id_str}'

        order_by_str = f'{order_by_str} WHEN {id_str} THEN {counter}'
        counter += 1
    order_by_str = f'{order_by_str} END) ASC'

    columns = "u.university_id, u.name, u.city, u.state, u.website, u.campus_location, u.total_enrollment"
    query = f'SELECT {columns} FROM university u WHERE {where_str} {order_by_str};'
    print(query)
    return query

def get_college(uni_id):
    select_str = "SELECT u.university_id, u.name, u.city, u.state, u.website, u.campus_location, u.total_enrollment, si.control, si.religious, si.accepts_ap_credit, si.study_abroad, si.offers_rotc, si.has_football, si.has_basketball, si.ncaa_member, si.retention_rate, si.graduation_rate, adm.total_applicants, adm.total_admitted, adm.admission_rate, adm.male_applicants, adm.female_applicants, adm.male_admitted, adm.female_admitted, adm.sat_rw_25, adm.sat_rw_75, adm.sat_math_25, adm.sat_math_75, adm.act_25, adm.act_75, fin.in_state_price, fin.out_of_state_price, fin.average_price_after_aid, fin.percent_given_aid, di.percent_american_indian_native_alaskan, di.percent_asian, di.percent_hawaiian_pacific_islander, di.percent_black, di.percent_white, di.percent_hispanic, di.percent_other, di.percent_two_races"
    from_str = "FROM university as u, school_info as si, admission_stats as adm, financial_stats as fin, diversity_stats as di"
    where_str = f"WHERE u.university_id = {uni_id} AND u.university_id = si.university_id AND u.university_id = adm.university_id AND u.university_id = fin.university_id AND u.university_id = di.university_id"

    info_query = f"{select_str}\n{from_str}\n{where_str};"
    majors_query = f"SELECT program_name FROM academic_programs WHERE {uni_id} = university_id;"

    return info_query, majors_query

def generate_query(params):
    state = params.get('state')
    state_location = int(params.get('state_location'))
    campus_location = params.getlist('campus_location')
    size = params.getlist('enrollment')
    majors = params.getlist('study-fields')
    sat_math = int(params.get('sat_math'))
    sat_reading = int(params.get('sat_reading'))
    act = int(params.get('act'))
    tuition = int(params.get('tuition'))
    financial_aid = int(params.get('financial-aid'))
    religious = params.get('religious')
    ap_credit = params.get('ap_credit')
    study_abroad = params.get('study_abroad')
    offers_rotc = params.get('offers_rotc')
    ncaa = params.get('ncaa')

    # Multiple score by this much for each type
    location_mult = 6 - int(params.get('location_rank'))
    academics_mult = 6 - int(params.get('academics_rank'))
    finance_mult = 6 - int(params.get('finance_rank'))
    other_mult = 6 - int(params.get('other_rank'))

    # Initial SQL strings
    where_str = "WHERE u.university_id = si.university_id AND u.university_id = adm.university_id AND u.university_id = fin.university_id AND u.university_id = di.university_id"
    group_str = "GROUP BY u.university_id"

    select_str = "SELECT u.university_id, u.name, u.city, u.state, u.website, u.campus_location, u.total_enrollment"

    majors_query = ""
    if majors:
        majors_string = "(" + ",".join(majors) + ")"
        majors_query = f"""
        , (SELECT uni.university_id, COUNT(a.cip_code) as majors_count
        FROM university as uni, academic_programs as a
        WHERE uni.university_id = a.university_id AND a.cip_code IN {majors_string}
        GROUP BY uni.university_id
        ORDER BY majors_count
        ) as maj
        """
        where_str = f"{where_str} AND u.university_id = maj.university_id"

    from_str = f"FROM university as u, school_info as si, admission_stats as adm, financial_stats as fin, diversity_stats as di {majors_query}"

    # State selection
    location_order = []
    if state_location == 0:
        location_order.append(f"(SUM(u.state = '{state}'))")
    elif state_location == 1:
        location_order.append(f"(SUM(u.state = '{state}') * 3)")
    elif state_location == 2:
        location_order.append(f"SUM(u.state = '{state}')")

    # Campus location and enrollment
    if campus_location:
        location_str = "('" + "', '".join(campus_location) + "')"
        location_order.append(f"SUM(u.campus_location IN {location_str})")
    if size:
        lower_bound = ENROLLMENT_BOUNDS[int(min(size))][0]
        upper_bound = ENROLLMENT_BOUNDS[int(max(size))][1]
        location_order.append(f"SUM(u.total_enrollment >= {lower_bound})")
        location_order.append(f"SUM(u.total_enrollment <= {upper_bound})")
    location_str = "((" + " + ".join(location_order) + \
        f") * {location_mult}) as location_score"

    if location_order:
        select_str = f"{select_str}, {location_str}"
    else:
        select_str = f"{select_str}, 0 as location_score"

    # SAT/ACT Scores
    academics_order = []
    if sat_math > 200:
        academics_order.append(f"SUM(adm.sat_math_25 <= {sat_math})")
    if sat_reading > 200:
        academics_order.append(f"SUM(adm.sat_rw_25 <= {sat_reading})")
    if act > 1:
        academics_order.append(f"SUM(adm.act_25 <= {act})")
    if majors:
        academics_str = "(maj.majors_count + (" + " + ".join(academics_order) + \
            f") * {academics_mult}) as academics_score"
    else:
        academics_str = "((" + " + ".join(academics_order) + \
            f") * {academics_mult}) as academics_score"

    if academics_order:
        select_str = f"{select_str}, {academics_str}"
    else:
        select_str = f"{select_str}, 0 as academics_score"

    # Financial
    finance_order = []
    if tuition:
        if financial_aid:
            finance_order.append(
                f"SUM(fin.percent_given_aid >= 50) + SUM(fin.average_price_after_aid <= {tuition})")
        else:
            price_state = "in_state_price" if state_location == 1 else "out_of_state_price"
            finance_order.append(f"SUM(fin.{price_state} <= {tuition})")
    finance_str = "((" + " + ".join(finance_order) + \
        f") * {finance_mult}) as finance_score"

    if finance_order:
        select_str = f"{select_str}, {finance_str}"
    else:
        select_str = f"{select_str}, 0 as finance_score"

    # Other preferences
    other_order = []
    if religious is not None:
        if int(religious):
            other_order.append("SUM(si.religious = 1)")
        else:
            other_order.append("SUM(si.religious = 0)")
    if ap_credit is not None and int(ap_credit):
        other_order.append("SUM(si.accepts_ap_credit = 1)")
    if study_abroad is not None and int(study_abroad):
        other_order.append("SUM(si.study_abroad = 1)")
    if offers_rotc is not None and int(offers_rotc):
        other_order.append("SUM(si.offers_rotc = 1)")
    if ncaa is not None and int(ncaa):
        other_order.append("SUM(si.ncaa_member = 1)")
    other_str = "((" + " + ".join(other_order) + \
        f") * {other_mult}) as other_score"

    if other_order:
        select_str = f"{select_str}, {other_str}"
    else:
        select_str = f"{select_str}, 0 as other_score"

    # Academic priority
    order_str = "ORDER BY"
    ranks = {
        location_mult: "location_score DESC",
        academics_mult: "academics_score DESC",
        finance_mult: "finance_score DESC",
        other_mult: "other_score DESC",
    }
    order = [ranks[rank] for rank in range(5, 1, -1)]
    order_str = "ORDER BY " + ", ".join(order) + ", u.name ASC"

    limit_str = "LIMIT 21"

    columns = "university_id, name, city, state, website, campus_location, total_enrollment"
    return (f"{select_str}\n{from_str}\n{where_str}\n{group_str}\n{order_str}\n{limit_str};", order)

def find_major(majors_list):
    # second advanced function
    # NOTE: this does not seem too advanced, so maybe we can parse each major / type to describe what it is
    # calculate quiz output
    major_dict = {}
    for l in majors_list:
        for major in l:
            major_dict[major] = major_dict.get(major, 0) + 1
    major_type = max(major_dict, key=major_dict.get)

# query for majors based on type
    query = "SELECT major FROM majors_info WHERE type='" + \
        str(major_type) + "'"
    return major_type, query

def get_major_type_info(major):
    URL = "https://acd.iupui.edu/explore/choose-your-major/connect-majors-to-careers/interests/" + \
        major.lower() + "/index.html"
    html_text = requests.get(URL).text
    soup = BeautifulSoup(html_text, 'html.parser')

    sub_title = str(soup.find('h3'))[4:-5]

    desc = str(soup.find_all('div', class_="text", string=re.compile("These")))
    desc = desc.split('. ', 1)[1]
    desc = desc.split(' If not,')[0]

    return sub_title, desc

def get_major_info(major_name):
    major_path = "https://www.mymajors.com/college-majors/"
    name = re.sub("[^a-zA-z]+", " ", major_name)
    major = "-".join(name.lower().split())
    url = major_path + major

    page = requests.get(url).text

    default_text = "A general program that focuses on law and legal issues from the perspective of the social sciences and humanities."
    soup = BeautifulSoup(page, "html.parser")
    desc = soup.find("p", {"class": "lead"}).get_text()
    desc_text = desc.split('\n')[1].strip()
    if desc_text == default_text:
        return False
    class_list = soup.find("ul", {"class": "cols3"})
    classes = []
    if type(class_list) is None:
        classes = ["No Data Found"]
    else:
        class_list = class_list.get_text().strip().split('\n')
        classes = [cl.strip() for cl in class_list if cl.strip()]

    career_url = "https://www.mymajors.com/careers/" + major + "-major"
    careers_page = requests.get(career_url).text

    soup = BeautifulSoup(careers_page, "html.parser")
    job_list = soup.find("ul", {"class": "cols2"})
    jobs = []
    if job_list is None:
        jobs = ["No Data Found"]
    else:
        job_list = job_list.get_text().strip().split('\n')
        jobs = [job.strip() for job in job_list if job.strip()]

    salary_data = soup.find_all("td")
    salaries = {}
    for i in range(0, len(salary_data), 2):
        if (i + 1) < len(salary_data):
            label = salary_data[i].get_text().strip()
            salary = salary_data[i+1].get_text().strip()
            salaries[label] = salary
    if not len(salaries):
        salaries["Data"] = "Not Found"

    return desc_text, classes, jobs, salaries


from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.views.decorators.http import require_http_methods
from .forms import CustomUserCreationForm

@require_http_methods(['GET', 'POST'])
def login(request):
    if request.method == 'POST':
        # 다른 form과 인자 구성이 다름
        form = AuthenticationForm(request, request.POST)
        if form.is_valid():
            user = form.get_user()
            auth_login(request, user)
            next = request.GET.get('next')
            return redirect(next or 'board:board_index')
    else:
        form = AuthenticationForm()
    context = {
        'form' : form,
    }
    return render(request, 'accounts/login.html', context)

def logout(request):
    auth_logout(request)
    return redirect('board:board_index')

@require_http_methods(['GET', 'POST'])
def signup(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            auth_login(request, user)
            return redirect('board:board_index')
    else:
        form = CustomUserCreationForm()

    context = {'form' : form}
    return render(request, 'accounts/signup.html', context)


from django.shortcuts import render
from abb.models import visitors
from abb.forms import visitorform
# Create your views here.

def show_data(request):
    form=visitorform()
    if request.method=='POST':
        form=visitorform(request.POST)
        if form.is_valid():
            name=form.cleaned_data['name']
            mobile_no=form.cleaned_data['mobile_no']
            feedback=form.cleaned_data['feedback']
            reg=visitors(name=name,mobile_no=mobile_no,feedback=feedback)
            reg.save()
        form=visitorform()
    return render(request,'abb/home.html',{'form':form})


from scripts.src.common.config import Color, Keywords, Direct, DataType
from common_and_plotting_functions.functions import check_and_mkdir_of_direct
from scripts.src.common.plotting_functions import multi_row_col_bar_plot

from scripts.data.common_functions import common_data_loader

from scripts.model.model_loader import model_loader, ModelList
from scripts.src.core.model.model_constructor import common_model_constructor

from ...common.result_processing_functions import common_flux_comparison_func

data_wrap_obj, keyword = common_data_loader(DataType.renal_carcinoma, test_mode=False, natural_anti_correction=True)
# user_defined_model = model_loader(ModelList.invivo_infusion_model)
user_defined_model = model_loader(ModelList.base_model_with_glc_tca_buffer)
mfa_model_obj = common_model_constructor(user_defined_model)

name = 'renal_carcinoma_invivo_infusion'
# output_folder = '{}/{}'.format(Direct.output_direct, name)
# raw_data_folder = '{}/{}'.format(output_folder, Direct.raw_flux_analysis_direct)

class SpecificParameter(object):
    test_dynamic_constant_flux_list = []
    test_preset_constant_flux_value_dict = {
        # 'GLC_total_input': 100,
        'GLC_supplement_net': 0,
        'CIT_supplement_net': 0,
        # 'GLN_input': 80,
        'ASP_input': 50,
        # 'SER_input': 8,
        # 'ALA_input': 20,
        # 'OAC_supplement_net': 0,
    }
    specific_flux_range_dict = {
        'Salvage_c': (1, 10),
    }

test_data_param_raw_list = [
    {
        keyword.tissue: keyword.kidney,
        '': [
            {
                keyword.patient: 1,
                '': [
                    {
                        keyword.index: 1,
                    },
                    {
                        keyword.index: 2,
                    },
                ]
            }]
    }
]

complete_data_param_raw_list = [
    {
        keyword.tissue: keyword.kidney,
        '': [
            {
                keyword.patient: 1,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 2,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 3,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 4,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 5,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
        ]
    },
    {
        keyword.tissue: keyword.carcinoma,
        '': [
            {
                keyword.patient: 1,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 2,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 3,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 4,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 5,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
        ]
    },
    {
        keyword.tissue: keyword.brain,
        '': [
            {
                keyword.patient: 1,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 2,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 3,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 4,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
            {
                keyword.patient: 5,
                '': [
                    # {
                    #     keyword.index: 1,
                    # },
                    # {
                    #     keyword.index: 2,
                    # },
                    # {
                    #     keyword.index: 3,
                    # },
                    {
                        keyword.index: Keywords.average,
                    },
                ]
            },
        ]},
]

important_flux_list = [
    # 'GLC_unlabelled_input',
    ('FBA_c', 'FBA_c__R'),
    'PC_m',
    'G6PDH2R_PGL_GND_c',
    'PDH_m',
    'CS_m',
    'AKGD_m',
    'ICDH_m',
    'PYK_c',
    ('SUCD_m', 'SUCD_m__R'),
    ('FUMH_m', 'FUMH_m__R'),
    ('CIT_trans__R', 'CIT_trans'),
    ('MDH_c', 'MDH_c__R'),
    ('MDH_m', 'MDH_m__R'),
    ('GPT_c', 'GPT_c__R'),
    'PHGDH_PSAT_PSP_c',
    ('LDH_c', 'LDH_c__R'),
    'Salvage_c',
    'ACITL_c',
    'PEPCK_c',
    ('GAPD_c', 'GAPD_c__R'),
    'GLC_input',
    'GLN_input',
    ('SHMT_c', 'SHMT_c__R'),
    ('AKGMAL_m__R', 'AKGMAL_m')
]

mid_name_list = [
    ['3PG_c', 'SER_c', 'PYR_c+PYR_m', 'LAC_c'],
    ['ALA_c', 'CIT_c+CIT_m', 'SUC_m', 'FUM_m'],
    ['MAL_c+MAL_m', 'GLU_c+GLU_m', 'GLN_c+GLN_m', 'ASP_c+ASP_m']
]

def data_param_list_generator(param_raw_list):
    current_param_list = []
    for labeling_param_dict in param_raw_list:
        labeling_key = labeling_param_dict[keyword.tissue]
        labeling_content_list = labeling_param_dict['']
        for type_param_dict in labeling_content_list:
            type_key = type_param_dict[keyword.patient]
            type_content_list = type_param_dict['']
            for index_param_dict in type_content_list:
                index_key = index_param_dict[keyword.index]
                current_param_list.append({
                    keyword.tissue: labeling_key,
                    keyword.patient: type_key,
                    keyword.index: index_key,
                    Keywords.obj_threshold_key: None
                })
    return current_param_list

data_param_raw_list = complete_data_param_raw_list
total_param_list = data_param_list_generator(data_param_raw_list)

def collect_results(final_data_obj):
    final_mapping_dict = {}
    for param_dict in total_param_list:
        tissue_key = param_dict[keyword.tissue]
        patient_key = param_dict[keyword.patient]
        index_key = param_dict[keyword.index]
        project_name = data_wrap_obj.project_name_generator(tissue_key, patient_key, index_key)
        final_mapping_dict[project_name] = (tissue_key, patient_key, index_key)
        final_data_obj.load_current_result_label(project_name)
        if Keywords.obj_threshold_key in final_data_obj.final_information_dict[project_name]:
            del final_data_obj.final_information_dict[project_name][Keywords.obj_threshold_key]
    return final_mapping_dict

def experimental_results_comparison_parameter_generator(target_metabolite_data_dict):
    final_color_dict = {}
    final_data_dict_for_plotting = {}
    for labeling_key, each_labeling_data_dict in target_metabolite_data_dict.items():
        data_dict_for_plotting = {}
        color_dict = {}
        for type_key, each_type_data_dict in each_labeling_data_dict.items():
            for index_key, each_index_data_dict in each_type_data_dict.items():
                current_type_index_name = '{}_{}'.format(type_key, index_key)
                if current_type_index_name not in color_dict:
                    if type_key == 1:
                        current_color = Color.blue
                    elif type_key == 2:
                        current_color = Color.orange
                    else:
                        raise ValueError()
                    color_dict[current_type_index_name] = current_color
                for metabolite_name, each_metabolite_data_obj in each_index_data_dict.items():
                    if metabolite_name not in data_dict_for_plotting:
                        data_dict_for_plotting[metabolite_name] = {}
                    data_dict_for_plotting[metabolite_name][current_type_index_name] = \
                        each_metabolite_data_obj.data_vector
        final_data_dict_for_plotting[labeling_key] = data_dict_for_plotting
        final_color_dict[labeling_key] = color_dict
    return keyword.output_folder, final_data_dict_for_plotting, final_color_dict

def flux_comparison_parameter_generator(final_solution_data_dict, final_flux_name_index_dict):
    from ...common.config import index_calculation_func_dict
    current_index_name_func_dict = index_calculation_func_dict
    final_dict_for_flux_comparison_plotting = {}
    final_key_name_parameter_dict = {}
    final_color_dict = {}
    comparison_name_tissue_dict = {
        'kidney_tumor_vs_brain': [keyword.carcinoma, keyword.brain],
        'tumor_vs_kidney': [keyword.kidney, keyword.carcinoma]}
    current_important_flux_list = list(important_flux_list)
    if current_index_name_func_dict is not None:
        current_important_flux_list.extend(current_index_name_func_dict.items())
    patient_key_set = set()
    for comparison_name, current_tissue_name_list in comparison_name_tissue_dict.items():
        data_dict_for_plotting = {}
        key_name_parameter_dict = {}
        color_dict = {}
        data_dict_order_list = []
        if comparison_name == 'kidney_tumor_vs_brain':
            for tissue_index, tissue_name in enumerate(current_tissue_name_list):
                for patient_key, current_patient_dict in final_solution_data_dict[tissue_name].items():
                    if patient_key not in patient_key_set:
                        patient_key_set.add(patient_key)
                    current_flux_name_index_dict = final_flux_name_index_dict[tissue_name][patient_key]
                    data_dict_order_list.append(
                        (tissue_index, tissue_name, patient_key, current_patient_dict, current_flux_name_index_dict))
        elif comparison_name == 'tumor_vs_kidney':
            for patient_key in patient_key_set:
                for tissue_index, tissue_name in enumerate(current_tissue_name_list):
                    current_patient_dict = final_solution_data_dict[tissue_name][patient_key]
                    current_flux_name_index_dict = final_flux_name_index_dict[tissue_name][patient_key]
                    data_dict_order_list.append(
                        (tissue_index, tissue_name, patient_key, current_patient_dict, current_flux_name_index_dict))
        else:
            raise ValueError()
        for tissue_index, tissue_name, patient_key, current_patient_dict, current_flux_name_index_dict in data_dict_order_list:
            for index_num, current_data_array in current_patient_dict.items():
                key_name = '{}_{}_{}'.format(tissue_name, patient_key, index_num)
                key_name_parameter_dict[key_name] = (tissue_name, patient_key, index_num)
                if tissue_index == 0:
                    color_dict[key_name] = Color.blue
                else:
                    color_dict[key_name] = Color.orange
                common_flux_comparison_func(
                    current_important_flux_list, current_flux_name_index_dict[index_num], current_data_array,
                    data_dict_for_plotting, key_name)
        final_dict_for_flux_comparison_plotting[comparison_name] = data_dict_for_plotting
        final_key_name_parameter_dict[comparison_name] = key_name_parameter_dict
        final_color_dict[comparison_name] = color_dict
    return final_dict_for_flux_comparison_plotting, final_key_name_parameter_dict, final_color_dict

def experimental_data_plotting(
        complete_experimental_mid_data_obj_dict, complete_result_information_dict, output_direct):
    mid_data_dict_for_plotting = {}
    raw_data_dict_for_plotting = {}
    color_dict = {}
    patient_color_dict = {}
    complete_color_list = [Color.blue, Color.orange, Color.purple]
    for result_label, experimental_mid_data_obj_dict in complete_experimental_mid_data_obj_dict.items():
        result_information_dict = complete_result_information_dict[result_label]
        tissue = result_information_dict[keyword.tissue]
        patient = result_information_dict[keyword.patient]
        index = result_information_dict[keyword.index]
        if index == Keywords.average:
            continue
        if tissue not in mid_data_dict_for_plotting:
            mid_data_dict_for_plotting[tissue] = {}
            raw_data_dict_for_plotting[tissue] = {}
        for metabolite_name, mid_data_obj in experimental_mid_data_obj_dict.items():
            current_mid_label_tissue_dict = mid_data_dict_for_plotting[tissue]
            current_raw_label_tissue_dict = raw_data_dict_for_plotting[tissue]
            if metabolite_name not in current_mid_label_tissue_dict:
                current_mid_label_tissue_dict[metabolite_name] = {}
                current_raw_label_tissue_dict[metabolite_name] = {}
            patient_index_str = f'{patient}_{index}'
            current_mid_label_tissue_dict[metabolite_name][patient_index_str] = mid_data_obj.data_vector
            current_raw_label_tissue_dict[metabolite_name][patient_index_str] = mid_data_obj.raw_data_vector
            if patient_index_str not in color_dict:
                if patient not in patient_color_dict:
                    current_color = complete_color_list[len(patient_color_dict) % 3]
                    patient_color_dict[patient] = current_color
                color_dict[patient_index_str] = patient_color_dict[patient]
    target_emu_name_nested_list = [
        ['glucose', '3-phosphoglycerate', 'pyruvate', 'lactate'],
        ['alanine', 'citrate', 'succinate', 'fumarate'],
        ['malate', 'aspartate', 'glutamate', 'glutamine'],
    ]
    target_row_num = len(target_emu_name_nested_list)
    target_col_num = len(target_emu_name_nested_list[0])
    for tissue, each_tissue_mid_data_dict_for_plotting in mid_data_dict_for_plotting.items():
        each_tissue_raw_data_dict_for_plotting = raw_data_dict_for_plotting[tissue]
        for raw_data in (False, True):
            if raw_data:
                parent_direct = 'raw_data'
                complete_data_dict = each_tissue_raw_data_dict_for_plotting
                ylim = (0, None)
            else:
                parent_direct = 'mid_data'
                complete_data_dict = each_tissue_mid_data_dict_for_plotting
                ylim = (0, 1)
            current_title = f'{tissue}'
            current_output_direct = '{}/{}'.format(output_direct, parent_direct)
            check_and_mkdir_of_direct(current_output_direct)
            multi_row_col_bar_plot(
                complete_data_dict, target_emu_name_nested_list, target_row_num, target_col_num,
                error_bar_data_dict=None, color_dict=color_dict, title_dict=None,
                output_direct=current_output_direct, current_title=current_title, ylim=ylim,
                xlabel_list=None, figsize=None, legend=False)

def result_output_dataframe_dict_generator(complete_result_dict):
    pass
    # return {'Sheet1': pd.DataFrame()}

def metabolic_network_parameter_generator():
    experimental_mid_metabolite_set = {
        'PYR_c', 'PYR_m',
        'LAC_c',
        'ALA_c',
        'SUC_m',
        'FUM_m',
        'SER_c',
        'MAL_c', 'MAL_m',
        'ASP_c', 'ASP_m',
        'GLU_c', 'GLU_m',
        'GLN_c', 'GLN_m',
        'CIT_c', 'CIT_m',
        '3PG_c',
    }
    experimental_mixed_mid_metabolite_set = {
        'PYR_c', 'PYR_m',
        'MAL_c', 'MAL_m',
        'ASP_c', 'ASP_m',
        'GLU_c', 'GLU_m',
        'GLN_c', 'GLN_m',
        'CIT_c', 'CIT_m',
    }
    biomass_metabolite_set = {
        'ALA_c', 'RIB5P_c', 'GLY_c', 'SER_c', 'ASP_c',
        'ACCOA_c', 'GLU_c', 'GLN_c',
    }
    input_metabolite_set = {
        'GLC_e', 'GLC_unlabelled_e', 'GLN_e', 'ASP_e', 'SER_e', 'GLY_e', 'ALA_e', 'LAC_e',
    }
    c13_labeling_metabolite_set = {
        'GLC_e',
    }
    boundary_flux_set = {
        'GLC_input', 'GLC_unlabelled_input'
    }
    infusion = False
    return experimental_mid_metabolite_set, experimental_mixed_mid_metabolite_set, biomass_metabolite_set, \
        input_metabolite_set, c13_labeling_metabolite_set, boundary_flux_set, infusion


# #############################################################################
# 本題參數設定，請勿更改
seed = 0   # 亂數種子數
# #############################################################################
import warnings
warnings.filterwarnings('ignore')

import numpy as np
import pandas as pd

evaluation = pd.DataFrame({'Model': [],
                           'Details':[],
                           'RMSE (test)':[],
                           'R2 (train)':[],
                           'adj. R2 (train)':[],
                           'R2 (test)':[],
                           'adj. R2 (test)':[]}
                            )

# 讀取台北市房價資料集
df = pd.read_csv('Taipei_house.csv')

# 對"行政區"進行 one-hot encoding
df = pd.get_dummies(df, columns=['行政區'])
# 處理"車位類別"
df['車位類別'] = [0 if x=='無' else 1 for x in df['車位類別']]

# 計算 Adjusted R-squared
def adj_R2(r2, n, k):
    """ 函式描述：計算 Adjusted R-squared
    參數：
        r2:R-squared 數值
        n: 樣本數
        k: 特徵數

    回傳：
        Adjusted R-squared
    """
    return r2-(k-1)/(n-k)*(1-r2)

from sklearn.metrics import mean_squared_error
def measurement(model, X_train, X_test):
    y_pred = model.predict(X_test)

    rmse = round(np.sqrt(mean_squared_error(y_test, y_pred)), 0)
    r2_train = round(model.score(X_train, y_train), 4)
    adj_r2_train = round(adj_R2(model.score(X_train, y_train),
                                X_train.shape[0], X_train.shape[1]), 4)
    r2_test = round(model.score(X_test, y_test), 4)
    adj_r2_test = round(adj_R2(model.score(X_test, y_test),
                               X_test.shape[0], X_test.shape[1]), 4)
    return [rmse, r2_train, adj_r2_train, r2_test, adj_r2_test]

# 切分訓練集(80%)、測試集(20%)
features= ['土地面積', '建物總面積', '屋齡', '樓層', '總樓層', '用途',
           '房數', '廳數', '衛數', '電梯', '車位類別',
           '行政區_信義區', '行政區_大安區', '行政區_文山區','行政區_松山區']
target = '總價'
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(df[features],
                                                    df[target],
                                                    test_size=0.2,
                                                    random_state=seed)

lst_model, lst_info = [], []
# 複迴歸(參數皆為預設值)
# #########################################################################
# '行政區_信義區', '行政區_大安區', '行政區_文山區','行政區_松山區' 四個特徵是經過
# one-hot encoding 後產生，若欄位名稱不同可自行修改之。
# #########################################################################
from sklearn import linear_model
lst_model.append(linear_model.LinearRegression())
lst_info.append(['複迴歸','15 features'])

# 脊迴歸(Ridge regression)，除以下參數設定外，其餘為預設值
# #########################################################################
# alpha=10
# #########################################################################
lst_model.append(linear_model.Ridge(alpha=10))
lst_info.append(['Ridge','15 features'])

# 多項式迴歸，除以下參數設定外，其餘為預設值
# #########################################################################
# degree=2
# #########################################################################
from sklearn.preprocessing import PolynomialFeatures
poly_fea = PolynomialFeatures(degree=2)
X_train_poly = poly_fea.fit_transform(X_train)
X_test_poly = poly_fea.fit_transform(X_test)

lst_model.append(linear_model.LinearRegression())
lst_info.append(['多項式迴歸','deg=2'])

# 多項式迴歸 + Lasso迴歸，除以下參數設定外，其餘為預設值
# #########################################################################
# alpha=10
# #########################################################################
lst_model.append(linear_model.Lasso(alpha=10))
lst_info.append(['多項式迴歸+Lasso','deg=2'])

idx = evaluation.shape[0]
for i in range(len(lst_model)):
    if '多項式' in lst_info[i][0]:
        X_train, X_test = X_train_poly, X_test_poly

    model = lst_model[i].fit(X_train, y_train)
    row = lst_info[i] + measurement(model, X_train, X_test)
    evaluation.loc[idx+i] = row

print('對訓練集的最大 Adjusted R-squared: %.4f' % max(evaluation['adj. R2 (train)']))
print('對測試集的最小 RMSE:%d' % min(evaluation['RMSE (test)']))
print('兩個模型對測試集的最大 Adjusted R-squared: %.4f' %
      max(evaluation.loc[:1, 'adj. R2 (test)']))

''' 預測 '''
# 利用所有資料重新擬合模型，並進行預測
X = df[features]
y = df[target]
X_poly = poly_fea.fit_transform(X)

#features= ['土地面積', '建物總面積', '屋齡', '樓層', '總樓層', '用途',
#           '房數', '廳數', '衛數', '電梯', '車位類別',
#           '行政區_信義區', '行政區_大安區', '行政區_文山區','行政區_松山區']
new = np.array([36, 99, 32, 4, 4, 0, 3, 2, 1, 0, 0, 0, 0, 0, 1]).reshape(1, -1)
df_new = pd.DataFrame(new, columns=features)
df_poly_fea = poly_fea.fit_transform(df_new)

lst = evaluation['adj. R2 (test)'].tolist()
idx = lst.index(max(lst))
if idx <=1:
    model = lst_model[idx].fit(X, y)
    print('房價預測結果：%d' % model.predict(df_new))
else:
    model = lst_model[idx].fit(X_poly, y)
    print('房價預測結果：%d' % model.predict(df_poly_fea))


# import package
import os, random, tweepy, time, schedule

def job():
    folder=r"F:\mat\twitterbot\pics"    # Set your folder here, twitter allows .jpg, .jpeg, .png, .gif and mp4 videos to be uploaded as media.
                                        # Current limitations as of 18/07/2022: Images 5MB, gifs 15mb, MP4 512mb(when using media_category=amplify)
    a=random.choice(os.listdir(folder)) # I recommended to have only these files formats in your folder otherwise it might result in an error.
    print(a)

    #os.open(a, os.O_RDWR)              # Do not remove the #
    from PIL import Image               # You might have to 'pip install pillow' on command prompt
    img = folder+'\\'+a
    print(img)
    print("Media successfully picked")
    upload(img)

CONSUMER_KEY = ""
CONSUMER_SEC = ""
AUTH_ACC = ""
AUTH_SEC = ""
BEARER = ""

# 1.1 API
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SEC)
auth.set_access_token(AUTH_ACC, AUTH_SEC)
api = tweepy.API(auth, wait_on_rate_limit=True)

# 2.0 API
client = tweepy.Client(BEARER, CONSUMER_KEY, CONSUMER_SEC, AUTH_ACC, AUTH_SEC, wait_on_rate_limit=True)

try:
    api.verify_credentials()
    print("V1.1 Authentication OK")
except Exception as e:
    print(f"Error during authentication: {e}")

# upload media
def upload(filename):
    status_text = ""                               # Insert your text message here if you will or leave in blank otherwise for no text message.
    media_info = api.media_upload(filename)
    posted_status_v2 = client.create_tweet(text=status_text, media_ids=[media_info.media_id])
    print("Uploaded successfully")
    return 0

schedule.every().day.at("13:00").do(job)    # Set the hour of the day the bot will start running check https://schedule.readthedocs.io/en/stable/ for more info.
while True:
    schedule.run_pending()
    time.sleep(1)

##Credit for help
##STC, Merceal, Asplosions in Offline Chat
##Supe from Tweepy Discord server