import os
import sqlite3
import csv
import math
import random
import yaml
import tempfile
import logging
import pandas as pd
from time import time as current_time
from copy import deepcopy
from networkx.readwrite import json_graph

import networkx as nx

from tsih import History, Record, Key, NoHistory

from mesa import Model

from . import serialization, agents, analysis, utils, time

# These properties will be copied when pickling/unpickling the environment
_CONFIG_PROPS = [ 'name',
                  'states',
                  'default_state',
                  'interval',
                 ]

class Environment(Model):
    """
    The environment is key in a simulation. It contains the network topology,
    a reference to network and environment agents, as well as the environment
    params, which are used as shared state between agents.

    The environment parameters and the state of every agent can be accessed
    both by using the environment as a dictionary or with the environment's 
    :meth:`soil.environment.Environment.get` method.
    """

    def __init__(self, name=None,
                 network_agents=None,
                 environment_agents=None,
                 states=None,
                 default_state=None,
                 interval=1,
                 network_params=None,
                 seed=None,
                 topology=None,
                 schedule=None,
                 initial_time=0,
                 environment_params=None,
                 history=True,
                 dir_path=None,
                 **kwargs):


        super().__init__()

        self.schedule = schedule
        if schedule is None:
            self.schedule = time.TimedActivation()

        self.name = name or 'UnnamedEnvironment'
        seed = seed or current_time()
        random.seed(seed)
        if isinstance(states, list):
            states = dict(enumerate(states))
        self.states = deepcopy(states) if states else {}
        self.default_state = deepcopy(default_state) or {}

        if topology is None:
            network_params = network_params or {}
            topology = serialization.load_network(network_params,
                                                  dir_path=dir_path)
        if not topology:
            topology = nx.Graph()
        self.G = nx.Graph(topology) 


        self.environment_params = environment_params or {}
        self.environment_params.update(kwargs)

        self._env_agents = {}
        self.interval = interval
        if history:
            history = History
        else:
            history = NoHistory
        self._history = history(name=self.name,
                                backup=True)
        self['SEED'] = seed

        if network_agents:
            distro = agents.calculate_distribution(network_agents)
            self.network_agents = agents._convert_agent_types(distro)
        else:
            self.network_agents = []

        environment_agents = environment_agents or []
        if environment_agents:
            distro = agents.calculate_distribution(environment_agents)
            environment_agents = agents._convert_agent_types(distro)
        self.environment_agents = environment_agents

        self.logger = utils.logger.getChild(self.name)

    @property
    def now(self):
        if self.schedule:
            return self.schedule.time
        raise Exception('The environment has not been scheduled, so it has no sense of time')

    @property
    def agents(self):
        yield from self.environment_agents
        yield from self.network_agents

    @property
    def environment_agents(self):
        for ref in self._env_agents.values():
            yield ref

    @environment_agents.setter
    def environment_agents(self, environment_agents):
        self._environment_agents = environment_agents

        for (ix, agent) in enumerate(self._environment_agents):
            self.init_agent(len(self.G) + ix, agent_definitions=environment_agents, with_node=False)

    @property
    def network_agents(self):
        for i in self.G.nodes():
            node = self.G.nodes[i]
            if 'agent' in node:
                yield node['agent']

    @network_agents.setter
    def network_agents(self, network_agents):
        self._network_agents = network_agents
        for ix in self.G.nodes():
            self.init_agent(ix, agent_definitions=network_agents)

    def init_agent(self, agent_id, agent_definitions, with_node=True):
        init = False

        state = {}
        if with_node:
            node = self.G.nodes[agent_id]
            state = dict(node)
        state.update(self.states.get(agent_id, {}))

        agent_type = None
        if 'agent_type' in state:
            agent_type = state['agent_type']
        elif with_node and 'agent_type' in node:
            agent_type = node['agent_type']
        elif 'agent_type' in self.default_state:
            agent_type = self.default_state['agent_type']

        if agent_type:
            agent_type = agents.deserialize_type(agent_type)
        elif agent_definitions:
            agent_type, state = agents._agent_from_definition(agent_definitions, unique_id=agent_id)
        else:
            serialization.logger.debug('Skipping agent {}'.format(agent_id))
            return
        return self.set_agent(agent_id, agent_type, state, with_node=with_node)

    def set_agent(self, agent_id, agent_type, state=None, with_node=True):
        defstate = deepcopy(self.default_state) or {}
        defstate.update(self.states.get(agent_id, {}))
        if with_node:
            node = self.G.nodes[agent_id]
            defstate.update(node.get('state', {}))
        if state:
            defstate.update(state)
        a = None
        if agent_type:
            state = defstate
            a = agent_type(model=self,
                           unique_id=agent_id
            )

        for (k, v) in state.items():
            setattr(a, k, v)

        if with_node:
            node['agent'] = a
        self.schedule.add(a)
        return a

    def add_node(self, agent_type, state=None):
        agent_id = int(len(self.G.nodes()))
        self.G.add_node(agent_id)
        a = self.set_agent(agent_id, agent_type, state)
        a['visible'] = True
        return a

    def add_edge(self, agent1, agent2, start=None, **attrs):
        if hasattr(agent1, 'id'):
            agent1 = agent1.id
        if hasattr(agent2, 'id'):
            agent2 = agent2.id
        start = start or self.now
        return self.G.add_edge(agent1, agent2, **attrs)

    def log(self, message, *args, level=logging.INFO, **kwargs):
        if not self.logger.isEnabledFor(level):
            return
        message = message + " ".join(str(i) for i in args)
        message = " @{:>3}: {}".format(self.now, message)
        for k, v in kwargs:
            message += " {k}={v} ".format(k, v)
        extra = {}
        extra['now'] = self.now
        extra['unique_id'] = self.name
        return self.logger.log(level, message, extra=extra)

    def step(self):
        super().step()
        self.schedule.step()

    def run(self, until, *args, **kwargs):
        self._save_state()

        while self.schedule.next_time < until:
            self.step()
            utils.logger.debug(f'Simulation step {self.schedule.time}/{until}. Next: {self.schedule.next_time}')
        self.schedule.time = until
        self._history.flush_cache()

    def _save_state(self, now=None):
        serialization.logger.debug('Saving state @{}'.format(self.now))
        self._history.save_records(self.state_to_tuples(now=now))

    def __getitem__(self, key):
        if isinstance(key, tuple):
            self._history.flush_cache()
            return self._history[key]

        return self.environment_params[key]

    def __setitem__(self, key, value):
        if isinstance(key, tuple):
            k = Key(*key)
            self._history.save_record(*k,
                                      value=value)
            return
        self.environment_params[key] = value
        self._history.save_record(dict_id='env',
                                  t_step=self.now,
                                  key=key,
                                  value=value)

    def __contains__(self, key):
        return key in self.environment_params

    def get(self, key, default=None):
        '''
        Get the value of an environment attribute in a
        given point in the simulation (history).
        If key is an attribute name, this method returns
        the current value.
        To get values at other times, use a
        :meth: `soil.history.Key` tuple.
        '''
        return self[key] if key in self else default

    def get_agent(self, agent_id):
        return self.G.nodes[agent_id]['agent']

    def get_agents(self, nodes=None):
        if nodes is None:
            return self.agents
        return (self.G.nodes[i]['agent'] for i in nodes)

    def dump_csv(self, f):
        with utils.open_or_reuse(f, 'w') as f:
            cr = csv.writer(f)
            cr.writerow(('agent_id', 't_step', 'key', 'value'))
            for i in self.history_to_tuples():
                cr.writerow(i)

    def dump_gexf(self, f):
        G = self.history_to_graph()
        # Workaround for geometric models
        # See soil/soil#4
        for node in G.nodes():
            if 'pos' in G.nodes[node]:
                G.nodes[node]['viz'] = {"position": {"x": G.nodes[node]['pos'][0], "y": G.nodes[node]['pos'][1], "z": 0.0}}
                del (G.nodes[node]['pos'])

        nx.write_gexf(G, f, version="1.2draft")

    def dump(self, *args, formats=None, **kwargs):
        if not formats:
            return
        functions = {
            'csv': self.dump_csv,
            'gexf': self.dump_gexf
        }
        for f in formats:
            if f in functions:
                functions[f](*args, **kwargs)
            else:
                raise ValueError('Unknown format: {}'.format(f))

    def dump_sqlite(self, f):
        return self._history.dump(f)

    def state_to_tuples(self, now=None):
        if now is None:
            now = self.now
        for k, v in self.environment_params.items():
            yield Record(dict_id='env',
                         t_step=now,
                         key=k,
                         value=v)
        for agent in self.agents:
            for k, v in agent.state.items():
                yield Record(dict_id=agent.id,
                             t_step=now,
                             key=k,
                             value=v)

    def history_to_tuples(self):
        return self._history.to_tuples()

    def history_to_graph(self):
        G = nx.Graph(self.G)

        for agent in self.network_agents:

            attributes = {'agent': str(agent.__class__)}
            lastattributes = {}
            spells = []
            lastvisible = False
            laststep = None
            history = self[agent.id, None, None]
            if not history:
                continue
            for t_step, attribute, value in sorted(list(history)):
                if attribute == 'visible':
                    nowvisible = value
                    if nowvisible and not lastvisible:
                        laststep = t_step
                    if not nowvisible and lastvisible:
                        spells.append((laststep, t_step))

                    lastvisible = nowvisible
                    continue
                key = 'attr_' + attribute
                if key not in attributes:
                    attributes[key] = list()
                if key not in lastattributes:
                    lastattributes[key] = (value, t_step)
                elif lastattributes[key][0] != value:
                    last_value, laststep = lastattributes[key]
                    commit_value = (last_value, laststep, t_step)
                    if key not in attributes:
                        attributes[key] = list()
                    attributes[key].append(commit_value)
                    lastattributes[key] = (value, t_step)
            for k, v in lastattributes.items():
                attributes[k].append((v[0], v[1], None))
            if lastvisible:
                spells.append((laststep, None))
            if spells:
                G.add_node(agent.id, spells=spells, **attributes)
            else:
                G.add_node(agent.id, **attributes)

        return G

    def __getstate__(self):
        state = {}
        for prop in _CONFIG_PROPS:
            state[prop] = self.__dict__[prop]
        state['G'] = json_graph.node_link_data(self.G)
        state['environment_agents'] = self._env_agents
        state['history'] = self._history
        state['schedule'] = self.schedule
        return state

    def __setstate__(self, state):
        for prop in _CONFIG_PROPS:
            self.__dict__[prop] = state[prop]
        self._env_agents = state['environment_agents']
        self.G = json_graph.node_link_graph(state['G'])
        self._history = state['history']
        # self._env = None
        self.schedule = state['schedule']
        self._queue = []


SoilEnvironment = Environment
