__author__ = 'robert'

import numpy as np
import pandas as pd
import logging
import os # For path names working under Linux and Windows

from pypet import Environment, cartesian_product

def run_neuron(traj):
    """Runs a simulation of a model neuron.

    :param traj:

        Container with all parameters.

    :return:

        An estimate of the firing rate of the neuron

    """

    # Extract all parameters from `traj`
    V_init = traj.par.neuron.V_init
    I = traj.par.neuron.I
    tau_V = traj.par.neuron.tau_V
    tau_ref = traj.par.neuron.tau_ref
    dt = traj.par.simulation.dt
    duration = traj.par.simulation.duration

    steps = int(duration / float(dt))
    # Create some containers for the Euler integration
    V_array = np.zeros(steps)
    V_array[0] = V_init
    spiketimes = []  # List to collect all times of action potentials

    # Do the Euler integration:
    print('Starting Euler Integration')
    for step in range(1, steps):
        if V_array[step-1] >= 1:
            # The membrane potential crossed the threshold and we mark this as
            # an action potential
            V_array[step] = 0
            spiketimes.append((step-1)*dt)
        elif spiketimes and step * dt - spiketimes[-1] <= tau_ref:
            # We are in the refractory period, so we simply clamp the voltage
            # to 0
            V_array[step] = 0
        else:
            # Euler Integration step:
            dV = -1/tau_V * V_array[step-1] + I
            V_array[step] = V_array[step-1] + dV*dt

    print('Finished Euler Integration')

    # Add the voltage trace and spike times
    traj.f_add_result('neuron.$', V=V_array, nspikes=len(spiketimes),
                      comment='Contains the development of the membrane potential over time '
                              'as well as the number of spikes.')
    # This result will be renamed to `traj.results.neuron.run_XXXXXXXX`.

    # And finally we return the estimate of the firing rate
    return len(spiketimes) / float(traj.par.simulation.duration) *1000
    # *1000 since we have defined duration in terms of milliseconds

def neuron_postproc(traj, result_list):
    """Postprocessing, sorts computed firing rates into a table

    :param traj:

        Container for results and parameters

    :param result_list:

        List of tuples, where first entry is the run index and second is the actual
        result of the corresponding run.

    :return:
    """

    # Let's create a pandas DataFrame to sort the computed firing rate according to the
    # parameters. We could have also used a 2D numpy array.
    # But a pandas DataFrame has the advantage that we can index into directly with
    # the parameter values without translating these into integer indices.
    I_range = traj.par.neuron.f_get('I').f_get_range()
    ref_range = traj.par.neuron.f_get('tau_ref').f_get_range()

    I_index = sorted(set(I_range))
    ref_index = sorted(set(ref_range))
    rates_frame = pd.DataFrame(columns=ref_index, index=I_index)
    # This frame is basically a two dimensional table that we can index with our
    # parameters

    # Now iterate over the results. The result list is a list of tuples, with the
    # run index at first position and our result at the second
    for result_tuple in result_list:
        run_idx = result_tuple[0]
        firing_rates = result_tuple[1]
        I_val = I_range[run_idx]
        ref_val = ref_range[run_idx]
        rates_frame.loc[I_val, ref_val] = firing_rates # Put the firing rate into the
        # data frame

    # Finally we going to store our new firing rate table into the trajectory
    traj.f_add_result('summary.firing_rates', rates_frame=rates_frame,
                      comment='Contains a pandas data frame with all firing rates.')

def add_parameters(traj):
    """Adds all parameters to `traj`"""
    print('Adding Parameters')

    traj.f_add_parameter('neuron.V_init', 0.0,
                         comment='The initial condition for the '
                                    'membrane potential')
    traj.f_add_parameter('neuron.I', 0.0,
                         comment='The externally applied current.')
    traj.f_add_parameter('neuron.tau_V', 10.0,
                         comment='The membrane time constant in milliseconds')
    traj.f_add_parameter('neuron.tau_ref', 5.0,
                        comment='The refractory period in milliseconds '
                                'where the membrane potnetial '
                                'is clamped.')

    traj.f_add_parameter('simulation.duration', 1000.0,
                         comment='The duration of the experiment in '
                                'milliseconds.')
    traj.f_add_parameter('simulation.dt', 0.1,
                         comment='The step size of an Euler integration step.')

def add_exploration(traj):
    """Explores different values of `I` and `tau_ref`."""

    print('Adding exploration of I and tau_ref')

    explore_dict = {'neuron.I': np.arange(0, 1.01, 0.01).tolist(),
                    'neuron.tau_ref': [5.0, 7.5, 10.0]}

    explore_dict = cartesian_product(explore_dict, ('neuron.tau_ref', 'neuron.I'))
    # The second argument, the tuple, specifies the order of the cartesian product,
    # The variable on the right most side changes fastest and defines the
    # 'inner for-loop' of the cartesian product

    traj.f_explore(explore_dict)

def main():

    filename = os.path.join('hdf5', 'FiringRate.hdf5')
    env = Environment(trajectory='FiringRate',
                      comment='Experiment to measure the firing rate '
                            'of a leaky integrate and fire neuron. '
                            'Exploring different input currents, '
                            'as well as refractory periods',
                      add_time=False, # We don't want to add the current time to the name,
                      log_stdout=True,
                      log_config='DEFAULT',
                      multiproc=True,
                      ncores=2, #My laptop has 2 cores ;-)
                      wrap_mode='QUEUE',
                      filename=filename,
                      overwrite_file=True)

    traj = env.trajectory

    # Add parameters
    add_parameters(traj)

    # Let's explore
    add_exploration(traj)

    # Ad the postprocessing function
    env.add_postprocessing(neuron_postproc)

    # Run the experiment
    env.run(run_neuron)

    # Finally disable logging and close all log-files
    env.disable_logging()

if __name__ == '__main__':
    main()


"""Module for easy compartmental implementation of a BRIAN2 network.

Build parts of a network via subclassing :class:`~pypet.brian2.network.NetworkComponent` and
:class:`~pypet.brian2.network.NetworkAnalyser` for recording and statistical analysis.

Specify a :class:`~pypet.brian2.network.NetworkRunner` (subclassing optionally) that handles
the execution of your experiment in different subruns. Subruns can be defined
as :class:`~pypet.brian2.parameter.Brian2Parameter` instances in a particular
trajectory group. You must add to every parameter's :class:`~pypet.annotations.Annotations` the
attribute `order`. This order must be an integer specifying the index or order
the subrun should about to be executed in.

The creation and management of a BRIAN2 network is handled by the
:class:`~pypet.brian2.network.NetworkManager` (no need for subclassing). Pass your
components, analyser and your runner to the manager.

Pass the :func:`~pypet.brian2.network.run_network` function together with a
:class:`~pypet.brian2.network.NetworkManager` to your main environment function
:func:`~pypet.environment.Environment.run` to start a simulation and parallel
parameter exploration. Be aware that in case of a *pre-built* network,
successful parameter exploration
requires parallel processing (see :class:`~pypet.brian2.network.NetworkManager`).

"""

__author__ = 'Robert Meyer'

from brian2 import Network, second

from pypet.pypetlogging import HasLogger

class NetworkComponent(HasLogger):
    """Abstract class to define a component of a BRIAN2 network.

    Can be subclassed to define the construction of NeuronGroups or
    Synapses, for instance.

    """

    def add_parameters(self, traj):
        """Adds parameters to `traj`.

        Function called from the :class:`~pypet.brian2.network.NetworkManager` to
        define and add parameters to the trajectory container.

        """
        pass

    def pre_build(self, traj, brian_list, network_dict):
        """Builds network objects before the actual experimental runs.

        Function called from the :class:`~pypet.brian2.network.NetworkManager` if
        components can be built before the actual experimental runs or in
        case the network is pre-run.

        Parameters are the same as for the :func:`~pypet.brian2.network.NetworkComponent.build`
        method.

        """
        pass

    def build(self, traj, brian_list, network_dict):
        """Builds network objects at the beginning of each individual experimental run.

        Function called from the :class:`~pypet.brian2.network.NetworkManager`
        at the beginning of every experimental run,

        :param traj:

            Trajectory container

        :param brian_list:

            Add BRIAN2 network objects like NeuronGroups or Synapses to this list.
            These objects will be automatically added at the instantiation of the network
            in case the network was not pre-run
            via `Network(*brian_list)`.

        :param network_dict:

            Add any item to this dictionary that should be shared or accessed by all
            your components and which are not part of the trajectory container.
            It is recommended to also put all items from the `brian_list` into
            the dictionary for completeness.

        For convenience I recommend documenting the implementation of `build` and
        `pre-build` and so on in the subclass like the following. Use statements like `Adds`
        for items that are added to the list and the dict and statements like `Expects`
        for what is needed to be part of the `network_dict` in order to build the
        current component.

            brian_list:

                Adds:

                4 Connections, between all types of neurons (e->e, e->i, i->e, i->i)

            network_dict:

                Expects:

                'neurons_i': Inhibitory neuron group

                'neurons_e': Excitatory neuron group

                Adds:

                'connections':  List of 4 Connections,
                                between all types of neurons (e->e, e->i, i->e, i->i)

        """
        pass

    def add_to_network(self, traj, network, current_subrun, subrun_list, network_dict):
        """Can add network objects before a specific `subrun`.

        Called by a :class:`~pypet.brian2.network.NetworkRunner` before a the
        given `subrun`.

        Potentially one wants to add some BRIAN2 objects later to the network than
        at the very beginning of an experimental run. For example, a monitor might
        be added at the second subrun after an initial phase that is not supposed
        to be recorded.

        :param traj: Trajectoy container

        :param network:

            BRIAN2 network where elements could be added via `add(...)`.

        :param current_subrun:

            :class:`~pypet.brian2.parameter.Brian2Parameter` specifying the very next
            subrun to be simulated.

        :param subrun_list:

            List of :class:`~pypet.brian2.parameter.Brian2Parameter` objects that are to
            be run after the current subrun.

        :param network_dict:

            Dictionary of items shared by all components.

        """
        pass

    def remove_from_network(self, traj, network, current_subrun, subrun_list, network_dict):
        """Can remove network objects before a specific `subrun`.

        Called by a :class:`~pypet.brian2.network.NetworkRunner` after a
        given `subrun` and shortly after analysis (see
        :class:`~pypet.brian2.network.NetworkAnalyser`).

        :param traj: Trajectoy container

        :param network:

            BRIAN2 network where elements could be removed via `remove(...)`.

        :param current_subrun:

            :class:`~pypet.brian2.parameter.Brian2Parameter` specifying the current subrun
            that was executed shortly before.

        :param subrun_list:

            List of :class:`~pypet.brian2.parameter.Brian2Parameter` objects that are to
            be run after the current subrun.

        :param network_dict:

            Dictionary of items shared by all components.

        """
        pass

class NetworkAnalyser(NetworkComponent):
    """Specific NetworkComponent that analysis a network experiment.

    Can be subclassed to create components for statistical analysis of a network
    and network monitors.

    """
    def analyse(self, traj, network, current_subrun, subrun_list, network_dict):
        """Can perform statistical analysis on a given network.

        Called by a :class:`~pypet.brian2.network.NetworkRunner` directly after a
        given `subrun`.

        :param traj: Trajectoy container

        :param network: BRIAN2 network

        :param current_subrun:

            :class:`~pypet.brian2.parameter.Brian2Parameter` specifying the current subrun
            that was executed shortly before.

        :param subrun_list:

            List of :class:`~pypet.brian2.parameter.Brian2Parameter` objects that are to
            be run after the current subrun. Can be deleted or added to change the actual course
            of the experiment.

        :param network_dict:

            Dictionary of items shared by all components.

        """
        pass

class NetworkRunner(NetworkComponent):
    """Specific NetworkComponent to carry out the running of a BRIAN2 network experiment.

    A NetworRunner only handles the execution of a network simulation, the `BRIAN2 network` is
    created by a :class:`~pypet.brian2.network.NetworkManager`.

    Can potentially be subclassed to allow the adding of parameters via
    :func:`~pypet.brian2.network.NetworkComponent.add_parameters`. These parameters
    should specify an experimental run with a :class:~pypet.brian2.parameter.Brian2Parameter`
    to define the order and duration of network subruns. For the actual experimental runs,
    all subruns must be stored in a particular trajectory group.
    By default this `traj.parameters.simulation.durations`. For a pre-run
    the default is `traj.parameters.simulation.pre_durations`. These default group names
    can be changed at runner initialisation (see below).

    The network runner will look in the `v_annotations` property of each parameter
    in the specified trajectory group. It searches for the entry `order`
    to determine the order of subruns.

    :param report:

        How simulation progress should be reported, see also the parameters of
        run(...) in a BRIAN2 network.

    :param report_period:

        How often progress is reported. If not specified 10 seconds is chosen.

    :param durations_group_name:

        Name where to look for :class:`~pypet.brian2.parameter.Brian2Parameter` instances
        in the trajectory which specify the order and durations of subruns.

    :param pre_durations_group_name:

        As above, but for pre running a network.

    Moreover, in your subclass you can log messages with the private attribute `_logger`
    which is initialised in :func:`~pypet.pypetlogging.HasLogger._set_logger`.

    """
    def __init__(self, report='text', report_period=None,
                 durations_group_name='simulation.durations',
                 pre_durations_group_name='simulation.pre_durations'):

        if report_period is None:
            report_period = 10 * second

        self._report = report
        self._report_period = report_period

        self._durations_group_name = durations_group_name
        self._pre_durations_group_name = pre_durations_group_name

        self._set_logger()

    def execute_network_pre_run(self, traj, network, network_dict, component_list, analyser_list):
        """Runs a network before the actual experiment.

        Called by a :class:`~pypet.brian2.network.NetworkManager`.
        Similar to :func:`~pypet.brian2.network.NetworkRunner.run_network`.

        Subruns and their durations are extracted from the trajectory. All
        :class:`~pypet.brian2.parameter.Brian2Parameter` instances found under
        `traj.parameters.simulation.pre_durations` (default, you can change the
        name of the group where to search for durations at runner initialisation).
        The order is determined from
        the `v_annotations.order` attributes. There must be at least one subrun in the trajectory,
        otherwise an AttributeError is thrown. If two subruns equal in their order
        property a RuntimeError is thrown.

        :param traj: Trajectory container

        :param network: BRIAN2 network

        :param network_dict: Dictionary of items shared among all components

        :param component_list: List of :class:`~pypet.brian2.network.NetworkComponent` objects

        :param analyser_list: List of :class:`~pypet.brian2.network.NetworkAnalyser` objects

        """
        self._execute_network_run(traj, network, network_dict, component_list, analyser_list,
                                  pre_run=True)

    def execute_network_run(self, traj, network, network_dict, component_list, analyser_list):
        """Runs a network in an experimental run.

        Called by a :class:`~pypet.brian2.network.NetworkManager`.

        A network run is divided into several subruns which are defined as
        :class:`~pypet.brian2.parameter.Brian2Parameter` instances.

        These subruns are extracted from the trajectory. All
        :class:`~pypet.brian2.parameter.Brian2Parameter` instances found under
        `traj.parameters.simulation.durations` (default, you can change the
        name of the group where to search for durations at runner initialisation).
        The order is determined from
        the `v_annotations.order` attributes. An error is thrown if no orders attribute
        can be found or if two parameters have the same order.

        There must be at least one subrun in the trajectory,
        otherwise an AttributeError is thrown. If two subruns equal in their order
        property a RuntimeError is thrown.

        For every subrun the following steps are executed:

        1.  Calling :func:`~pypet.brian2.network.NetworkComponent.add_to_network` for every
            every :class:`~pypet.brian2.network.NetworkComponent` in the order as
            they were passed to the :class:`~pypet.brian2.network.NetworkManager`.

        2.  Calling :func:`~pypet.brian2.network.NetworkComponent.add_to_network` for every
            every :class:`~pypet.brian2.network.NetworkAnalyser` in the order as
            they were passed to the :class:`~pypet.brian2.network.NetworkManager`.

        3.  Calling :func:`~pypet.brian2.network.NetworkComponent.add_to_network` of the
            NetworkRunner itself (usually the network runner should not add or remove
            anything from the network, but this step is executed for completeness).

        4.  Running the BRIAN2 network for the duration of the current subrun by calling
            the network's `run` function.

        5.  Calling :func:`~pypet.brian2.network.NetworkAnalyser.analyse` for every
            every :class:`~pypet.brian2.network.NetworkAnalyser` in the order as
            they were passed to the :class:`~pypet.brian2.network.NetworkManager`.

        6.  Calling :func:`~pypet.brian2.network.NetworkComponent.remove_from_network` of the
            NetworkRunner itself (usually the network runner should not add or remove
            anything from the network, but this step is executed for completeness).

        7.  Calling :func:`~pypet.brian2.network.NetworkComponent.remove_from_network` for every
            every :class:`~pypet.brian2.network.NetworkAnalyser` in the order as
            they were passed to the :class:`~pypet.brian2.network.NetworkManager`

        8.  Calling :func:`~pypet.brian2.network.NetworkComponent.remove_from_network` for every
            every :class:`~pypet.brian2.network.NetworkComponent` in the order as
            they were passed to the :class:`~pypet.brian2.network.NetworkManager`.

        These 8 steps are repeated for every subrun in the `subrun_list`.
        The `subrun_list` passed to all `add_to_network`, `analyse` and
        `remove_from_network` methods can be modified
        within these functions to potentially alter the order of execution or
        even erase or add upcoming subruns if necessary.

        For example, a NetworkAnalyser checks
        for epileptic pathological activity and cancels all coming subruns in case
        of undesired network dynamics.

        :param traj: Trajectory container

        :param network: BRIAN2 network

        :param network_dict: Dictionary of items shared among all components

        :param component_list: List of :class:`~pypet.brian2.network.NetworkComponent` objects

        :param analyser_list: List of :class:`~pypet.brian2.network.NetworkAnalyser` objects

        """
        self._execute_network_run(traj, network, network_dict, component_list, analyser_list,
                                  pre_run=False)

    def _extract_subruns(self, traj, pre_run=False):
        """Extracts subruns from the trajectory.

        :param traj: Trajectory container

        :param pre_run: Boolean whether current run is regular or a pre-run

        :raises: RuntimeError if orders are duplicates or even missing

        """
        if pre_run:
            durations_list = traj.f_get_all(self._pre_durations_group_name)
        else:
            durations_list = traj.f_get_all(self._durations_group_name)

        subruns = {}
        orders = []

        for durations in durations_list:
            for duration_param in durations.f_iter_leaves(with_links=False):

                if 'order' in duration_param.v_annotations:
                    order = duration_param.v_annotations.order
                else:
                    raise RuntimeError('Your duration parameter %s has no order. Please add '
                                       'an order in `v_annotations.order`.' %
                                       duration_param.v_full_name)

                if order in subruns:
                    raise RuntimeError('Your durations must differ in their order, there are two '
                                       'with order %d.' % order)
                else:
                    subruns[order] = duration_param
                    orders.append(order)

        return [subruns[order] for order in sorted(orders)]

    def _execute_network_run(self, traj, network, network_dict, component_list,
                             analyser_list, pre_run=False):
        """Generic `execute_network_run` function, handles experimental runs as well as pre-runs.

        See also :func:`~pypet.brian2.network.NetworkRunner.execute_network_run` and
         :func:`~pypet.brian2.network.NetworkRunner.execute_network_pre_run`.

        """

        # Initially extract the `subrun_list`
        subrun_list = self._extract_subruns(traj, pre_run=pre_run)

        # counter for subruns
        subrun_number = 0

        # Execute all subruns in order
        while len(subrun_list) > 0:

            # Get the next subrun
            current_subrun = subrun_list.pop(0)

            # 1. Call `add` of all normal components
            for component in component_list:
                component.add_to_network(traj, network, current_subrun, subrun_list,
                                         network_dict)

            # 2. Call `add` of all analyser components
            for analyser in analyser_list:
                analyser.add_to_network(traj, network, current_subrun, subrun_list,
                                        network_dict)

            # 3. Call `add` of the network runner itself
            self.add_to_network(traj, network, current_subrun, subrun_list,
                                network_dict)

            # 4. Run the network
            self._logger.info('STARTING subrun `%s` (#%d) lasting %s.' %
                             (current_subrun.v_name, subrun_number, str(current_subrun.f_get())))
            network.run(duration=current_subrun.f_get(), report=self._report,
                              report_period=self._report_period)

            # 5. Call `analyse` of all analyser components
            for analyser in analyser_list:
                analyser.analyse(traj, network, current_subrun, subrun_list,
                                 network_dict)

            # 6. Call `remove` of the network runner itself
            self.remove_from_network(traj, network, current_subrun, subrun_list,
                                     network_dict)

            # 7. Call `remove` for all analyser components
            for analyser in analyser_list:
                analyser.remove_from_network(traj, network, current_subrun, subrun_list,
                                             network_dict)

            # 8. Call `remove` for all normal components
            for component in component_list:
                component.remove_from_network(traj, network, current_subrun, subrun_list,
                                              network_dict)

            subrun_number += 1

class NetworkManager(HasLogger):
    """Manages a BRIAN2 network experiment and creates the network.

    An experiment consists of

    :param network_runner:  A :class:`~pypet.brian2.network.NetworkRunner`

        Special component that handles the execution of several subruns.
        A NetworkRunner can be subclassed to implement the
        :func:`~pypet.brian2.network.NetworkComponent.add_parameters` method to add
        :class:`~pypet.brian2.parameter.Brian2Parameter` instances defining the
        order and duration of subruns.

    :param component_list:

        List of :class:`~pypet.brian2.network.NetworkComponents` instances to create
        and manage individual parts of a network.
        They are build and added to the network in the order defined in the list.

        :class:`~pypet.brian2.network.NetworkComponent` always needs to be sublcassed and
        defines only an abstract interface. For instance, one could create her or his
        own subclass called NeuronGroupComponent that creates NeuronGroups, Whereas
        a SynapseComponent creates Synapses between the before built NeuronGroups.
        Accordingly, the SynapseComponent instance is listed after
        the NeuronGroupComponent.

    :param analyser_list:

        List of :class:`~pypet.brian2.network.NetworkAnalyser` instances for recording and
        statistical evaluation of a BRIAN2 network. They should be used to add monitors
        to a network and to do further processing of the monitor data.

    This division allows to create compartmental network models where one can easily
    replace parts of a network simulation.
    :param network_constructor:

        If you have a custom network constructor apart from the Brian one,
        pass it here.

    """
    def __init__(self, network_runner, component_list, analyser_list=(), network_constructor=None):
        self.components = component_list
        self.network_runner = network_runner
        self.analysers = analyser_list
        self._network_dict = {}
        self._brian_list = []
        self._set_logger()
        self._pre_built = False
        self._pre_run = False
        self._network = None
        if network_constructor is None:
            self._network_constructor = Network
        else:
            self._network_constructor = network_constructor

    def add_parameters(self, traj):
        """Adds parameters for a network simulation.

        Calls :func:`~pypet.brian2.network.NetworkComponent.add_parameters` for all components,
        analyser, and the network runner (in this order).

        :param traj:  Trajectory container

        """
        self._logger.info('Adding Parameters of Components')

        for component in self.components:
            component.add_parameters(traj)

        if self.analysers:
            self._logger.info('Adding Parameters of Analysers')

            for analyser in self.analysers:
                analyser.add_parameters(traj)

        self._logger.info('Adding Parameters of Runner')

        self.network_runner.add_parameters(traj)

    def pre_build(self, traj):
        """Pre-builds network components.

        Calls :func:`~pypet.brian2.network.NetworkComponent.pre_build` for all components,
        analysers, and the network runner.

        `pre_build` is not automatically called but either needs to be executed manually
        by the user, either calling it directly or by using
        :func:`~pypet.brian2.network.NetworkManager.pre_run`.

        This function does not create a `BRIAN2 network`, but only it's components.

        :param traj: Trajectory container

        """
        self._logger.info('Pre-Building Components')

        for component in self.components:
            component.pre_build(traj, self._brian_list, self._network_dict)

        if self.analysers:

            self._logger.info('Pre-Building Analysers')

            for analyser in self.analysers:
                analyser.pre_build(traj, self._brian_list, self._network_dict)

        self._logger.info('Pre-Building NetworkRunner')
        self.network_runner.pre_build(traj, self._brian_list, self._network_dict)

        self._pre_built = True

    def build(self, traj):
        """Pre-builds network components.

        Calls :func:`~pypet.brian2.network.NetworkComponent.build` for all components,
        analysers and the network runner.

        `build` does not need to be called by the user. If `~pypet.brian2.network.run_network`
        is passed to an :class:`~pypet.environment.Environment` with this Network manager,
        `build` is automatically called for each individual experimental run.

        :param traj: Trajectory container

        """
        self._logger.info('Building Components')

        for component in self.components:
            component.build(traj, self._brian_list, self._network_dict)

        if self.analysers:

            self._logger.info('Building Analysers')

            for analyser in self.analysers:
                analyser.build(traj, self._brian_list, self._network_dict)

        self._logger.info('Building NetworkRunner')
        self.network_runner.build(traj, self._brian_list, self._network_dict)

    def pre_run_network(self, traj):
        """Starts a network run before the individual run.

        Useful if a network needs an initial run that can be shared by all individual
        experimental runs during parameter exploration.

        Needs to be called by the user. If `pre_run_network` is started by the user,
        :func:`~pypet.brian2.network.NetworkManager.pre_build` will be automatically called
        from this function.

        This function will create a new BRIAN2 network which is run by
        the :class:`~pypet.brian2.network.NetworkRunner` and it's
        :func:`~pypet.brian2.network.NetworkRunner.execute_network_pre_run`.

        To see how a network run is structured also take a look at
        :func:`~pypet.brian2.network.NetworkRunner.run_network`.

        :param traj: Trajectory container

        """
        self.pre_build(traj)

        self._logger.info('\n------------------------\n'
                          'Pre-Running the Network\n'
                          '------------------------')

        self._network = self._network_constructor(*self._brian_list)
        self.network_runner.execute_network_pre_run(traj, self._network, self._network_dict,
                                                    self.components, self.analysers)

        self._logger.info('\n-----------------------------\n'
                          'Network Simulation successful\n'
                          '-----------------------------')

        self._pre_run = True
        if hasattr(self._network, 'store'):
            self._network.store('pre_run')

    def run_network(self, traj):
        """Top-level simulation function, pass this to the environment

        Performs an individual network run during parameter exploration.

        `run_network` does not need to be called by the user. If this
        method (not this one of the NetworkManager)
        is passed to an :class:`~pypet.environment.Environment` with this NetworkManager,
        `run_network` and :func:`~pypet.brian2.network.NetworkManager.build`
        are automatically called for each individual experimental run.

        This function will create a new BRIAN2 network in case one was not pre-run.
        The execution of the network run is carried out by
        the :class:`~pypet.brian2.network.NetworkRunner` and it's
        :func:`~pypet.brian2.network.NetworkRunner.execute_network_run` (also take
        a look at this function's documentation to see the structure of a network run).

        :param traj: Trajectory container

        """

        # Check if the network was pre-built
        if self._pre_built:
            if self._pre_run and hasattr(self._network, 'restore'):
                self._network.restore('pre_run')
                # Temprorary fix for https://github.com/brian-team/brian2/issues/681
                self._network.store('pre_run')
            self._run_network(traj)
        else:
            self._run_network(traj)

    def _pretty_print_explored_parameters(self, traj):
        print_statement = '\n-------------------\n' +\
                          'Running the Network\n' +\
                          '-------------------\n' +\
                          '      with\n'

        explore_dict = traj.f_get_explored_parameters(copy=False)
        for full_name in explore_dict:
            parameter = explore_dict[full_name]

            print_statement += '%s = %s\n' % (parameter.v_full_name, parameter.f_val_to_str())

        print_statement += '-------------------'

        self._logger.info(print_statement)

    def _run_network(self, traj):
        """Starts a single run carried out by a NetworkRunner.

        Called from the public function :func:`~pypet.brian2.network.NetworkManger.run_network`.

        :param traj: Trajectory container

        """
        self.build(traj)

        self._pretty_print_explored_parameters(traj)

        # We need to construct a network object in case one was not pre-run
        if not self._pre_run:

            self._network = self._network_constructor(*self._brian_list)

        # Start the experimental run
        self.network_runner.execute_network_run(traj, self._network, self._network_dict,
                                                self.components, self.analysers)

        self._logger.info('\n-----------------------------\n'
                          'Network Simulation successful\n'
                          '-----------------------------')


""" Module that shows how to wrap *pypet* around an existing project

Thanks to *pypet* the module is now very flexible.
You can immediately start exploring different sets
of parameters, like different seeds or cell numbers.
Accordingly, you can simply change ``exp_dict`` to explore different sets.

On the contrary, this is tedious in the original code
and requires some effort of refactoring.

"""

__author__ = 'Robert Meyer'

import os
import logging

from pypet import Environment, cartesian_product, progressbar, Parameter

# Lets import the stuff we already have:
from original import cellular_automaton_1D, make_initial_state, plot_pattern

def make_filename(traj):
    """ Function to create generic filenames based on what has been explored """
    explored_parameters = traj.f_get_explored_parameters()
    filename = ''
    for param in explored_parameters.values():
        short_name = param.v_name
        val = param.f_get()
        filename += '%s_%s__' % (short_name, str(val))

    return filename[:-2] + '.png' # get rid of trailing underscores and add file type

def wrap_automaton(traj):
    """ Simple wrapper function for compatibility with *pypet*.

    We will call the original simulation functions with data extracted from ``traj``.

    The resulting automaton patterns wil also be stored into the trajectory.

    :param traj: Trajectory container for data

    """
    # Make initial state
    initial_state = make_initial_state(traj.initial_name, traj.ncells, traj.seed)
    # Run simulation
    pattern = cellular_automaton_1D(initial_state, traj.rule_number, traj.steps)
    # Store the computed pattern
    traj.f_add_result('pattern', pattern, comment='Development of CA over time')

def main():
    """ Main *boilerplate* function to start simulation """
    # Now let's make use of logging
    logger = logging.getLogger()

    # Create folders for data and plots
    folder = os.path.join(os.getcwd(), 'experiments', 'ca_patterns_pypet')
    if not os.path.isdir(folder):
        os.makedirs(folder)
    filename = os.path.join(folder, 'all_patterns.hdf5')

    # Create an environment
    env = Environment(trajectory='cellular_automata',
                      multiproc=True,
                      ncores=4,
                      wrap_mode='QUEUE',
                      filename=filename,
                      overwrite_file=True)

    # extract the trajectory
    traj = env.traj

    traj.par.ncells = Parameter('ncells', 400, 'Number of cells')
    traj.par.steps = Parameter('steps', 250, 'Number of timesteps')
    traj.par.rule_number = Parameter('rule_number', 30, 'The ca rule')
    traj.par.initial_name = Parameter('initial_name', 'random', 'The type of initial state')
    traj.par.seed = Parameter('seed', 100042, 'RNG Seed')

    # Explore
    exp_dict = {'rule_number' : [10, 30, 90, 110, 184],
                'initial_name' : ['single', 'random'],}
    # # You can uncomment the ``exp_dict`` below to see that changing the
    # # exploration scheme is now really easy:
    # exp_dict = {'rule_number' : [10, 30, 90, 110, 184],
    #             'ncells' : [100, 200, 300],
    #             'seed': [333444555, 123456]}
    exp_dict = cartesian_product(exp_dict)
    traj.f_explore(exp_dict)

    # Run the simulation
    logger.info('Starting Simulation')
    env.run(wrap_automaton)

    # Load all data
    traj.f_load(load_data=2)

    logger.info('Printing data')
    for idx, run_name in enumerate(traj.f_iter_runs()):
        # Plot all patterns
        filename = os.path.join(folder, make_filename(traj))
        plot_pattern(traj.crun.pattern, traj.rule_number, filename)
        progressbar(idx, len(traj), logger=logger)

    # Finally disable logging and close all log-files
    env.disable_logging()

if __name__ == '__main__':
    main()


__author__ = 'Robert Meyer'

import numpy as np
import itertools as itools
import hashlib
from collections import deque

class Universe(object):
    """Contains everything"""
    def __contains__(self, item):
        return True

class IteratorChain(object):
    """Helper class that chains arbitrary generators and iterators and iterables.

    Preferably used over itertools.chain to avoid recursive calls.

    You can already pass some `iterators` on creation.

    """
    def __init__(self, *iterables):
        # Deque containing the iterators to come
        self._chain = deque()
        # The current iterator providing the next elements
        self._current = iter([])

        self.add(*iterables)

    def add(self, *iterables):
        """Adds `iterables` to the chain"""
        self._chain.extend(iterables)

    def next(self):
        """Returns next element from chain.

        More precisely, it returns the next element of the
        foremost iterator. If this iterator is empty it moves iteratively
        along the chain of available iterators to pick the new foremost one.

        Raises StopIteration if there are no elements left.

        """
        while True:
            # We need this loop because some iterators may already be empty.
            # We keep on popping from the left until next succeeds and as long
            # as there are iterators available
            try:
                return next(self._current)
            except StopIteration:
                try:
                    self._current = iter(self._chain.popleft())
                except IndexError:
                    # If we run out of iterators we are sure that
                    # there can be no more element
                    raise StopIteration('Reached end of iterator chain')

    def __next__(self):
        """For python 3 compatibility"""
        return self.next()

    def __iter__(self):
        while True:
            yield self.next()

class ChainMap(object):
    """Combine multiple mappings for sequential lookup.
    """

    def __init__(self, *maps):
        self._maps = maps

    def __getitem__(self, key):
        for mapping in self._maps:
            try:
                return mapping[key]
            except KeyError:
                pass
        raise KeyError(key)

    def __len__(self):
        length = 0

        for mapping in self._maps:
            length += len(mapping)

        return length

    def __iter__(self):
        iter_list = [mapping.keys() for mapping in self._maps]
        return itools.chain(*iter_list)

class HashArray(object):
    """Hashable wrapper for numpy arrays"""

    def __init__(self, ndarray):
        """Creates a new hashable object encapsulating an ndarray.

            :param ndarray: The wrapped ndarray.

        """
        self._ndarray = ndarray

    def __eq__(self, other):

        try:
            return np.all(self._ndarray == other._ndarray)
        except AttributeError:
            return False

    def __hash__(self):
        return int(hashlib.sha1(self._ndarray.view(np.uint8)).hexdigest(), 16)

class TrajectoryMock(object):
    """Helper class that mocks properties of a trajectory.

    The full trajectory is not needed to rename a log file.
    In order to avoid copying the full trajectory during pickling
    this class is used.

    """
    def __init__(self, traj):
        self.v_environment_name = traj.v_environment_name
        self.v_name = traj.v_name
        self.v_crun_ = traj.v_crun_
        self.v_crun = traj.v_crun
        self.v_idx = traj.v_idx


__author__ = 'Robert Meyer'

import os
from pypet.trajectory import load_trajectory

def merge_all_in_folder(folder, ext='.hdf5',
                        dynamic_imports=None,
                        storage_service=None,
                        force=False,
                        ignore_data=(),
                        move_data=False,
                        delete_other_files=False,
                        keep_info=True,
                        keep_other_trajectory_info=True,
                        merge_config=True,
                        backup=True):
    """Merges all files in a given folder.

    IMPORTANT: Does not check if there are more than 1 trajectory in a file. Always
    uses the last trajectory in file and ignores the other ones.

    Trajectories are merged according to the alphabetical order of the files,
    i.e. the resulting merged trajectory is found in the first file
    (according to lexicographic ordering).

    :param folder: folder (not recursive) where to look for files
    :param ext: only files with the given extension are used
    :param dynamic_imports: Dynamic imports for loading
    :param storage_service: storage service to use, leave `None` to use the default one
    :param force: If loading should be forced.
    :param delete_other_files: Deletes files of merged trajectories

    All other parameters as in `f_merge_many` of the trajectory.

    :return: The merged traj

    """
    in_dir = os.listdir(folder)
    all_files = []
    # Find all files with matching extension
    for file in in_dir:
        full_file = os.path.join(folder, file)
        if os.path.isfile(full_file):
            _, extension = os.path.splitext(full_file)
            if extension == ext:
                all_files.append(full_file)
    all_files = sorted(all_files)

    # Open all trajectories
    trajs = []
    for full_file in all_files:
        traj = load_trajectory(index=-1,
                               storage_service=storage_service,
                               filename=full_file,
                               load_data=0,
                               force=force,
                               dynamic_imports=dynamic_imports)
        trajs.append(traj)

    # Merge all trajectories
    first_traj = trajs.pop(0)
    first_traj.f_merge_many(trajs,
                ignore_data=ignore_data,
                move_data=move_data,
                delete_other_trajectory=False,
                keep_info=keep_info,
                keep_other_trajectory_info=keep_other_trajectory_info,
                merge_config=merge_config,
                backup=backup)

    if delete_other_files:
        # Delete all but the first file
        for file in all_files[1:]:
            os.remove(file)

    return first_traj


"""Module that handles KeyboardInterrupt to exit gracefully"""

__author__ = 'Robert Meyer'

import signal
import sys

class _SigintHandler(object):

    SIGINT = '__SIGINT__'

    def __init__(self):
        self.original_handler = signal.getsignal(signal.SIGINT)
        self.hit = False  # variable to signal if SIGINT has been encountered before
        self.started = False

    def start(self):
        if not self.started:
            signal.signal(signal.SIGINT, self._handle_sigint)
            self.started = True

    def finalize(self):
        self.hit = False
        self.started = False
        signal.signal(signal.SIGINT, self.original_handler)

    def _handle_sigint(self, signum, frame):
        """Handler of SIGINT

        Does nothing if SIGINT is encountered once but raises a KeyboardInterrupt in case it
        is encountered twice.
        immediatly.

        """
        if self.hit:
            prompt = 'Exiting immediately!'
            raise KeyboardInterrupt(prompt)
        else:
            self.hit = True
            prompt = ('\nYou killed the process(es) via `SIGINT` (`CTRL+C`). '
                      'I am trying to exit '
                      'gracefully. Using `SIGINT` (`CTRL+C`) '
                      'again will cause an immediate exit.\n')
            sys.stderr.write(prompt)

sigint_handling = _SigintHandler()


''' Python Code for Communication with the Ecobee Thermostat '''
import requests
import json
import os
import logging

from requests.exceptions import RequestException;

logger = logging.getLogger('pyecobee')

def config_from_file(filename, config=None):
    ''' Small configuration file management function'''
    if config:
        # We're writing configuration
        try:
            with open(filename, 'w') as fdesc:
                fdesc.write(json.dumps(config))
        except IOError as error:
            logger.exception(error)
            return False
        return True
    else:
        # We're reading config
        if os.path.isfile(filename):
            try:
                with open(filename, 'r') as fdesc:
                    return json.loads(fdesc.read())
            except IOError as error:
                return False
        else:
            return {}

class Ecobee(object):
    ''' Class for storing Ecobee Thermostats and Sensors '''

    def __init__(self, config_filename=None, api_key=None, config=None):
        self.thermostats = list()
        self.pin = None
        self.authenticated = False

        if config is None:
            self.file_based_config = True
            if config_filename is None:
                if api_key is None:
                    logger.error("Error. No API Key was supplied.")
                    return
                jsonconfig = {"API_KEY": api_key}
                config_filename = 'ecobee.conf'
                config_from_file(config_filename, jsonconfig)
            config = config_from_file(config_filename)
        else:
            self.file_based_config = False
        self.api_key = config['API_KEY']
        self.config_filename = config_filename

        if 'ACCESS_TOKEN' in config:
            self.access_token = config['ACCESS_TOKEN']
        else:
            self.access_token = ''

        if 'AUTHORIZATION_CODE' in config:
            self.authorization_code = config['AUTHORIZATION_CODE']
        else:
            self.authorization_code = ''

        if 'REFRESH_TOKEN' in config:
            self.refresh_token = config['REFRESH_TOKEN']
        else:
            self.refresh_token = ''
            self.request_pin()
            return

        self.update()

    def request_pin(self):
        ''' Method to request a PIN from ecobee for authorization '''
        url = 'https://api.ecobee.com/authorize'
        params = {'response_type': 'ecobeePin',
                  'client_id': self.api_key, 'scope': 'smartWrite'}
        try:
            request = requests.get(url, params=params)
        except RequestException:
            logger.warn("Error connecting to Ecobee.  Possible connectivity outage."
                        "Could not request pin.")
            return
        self.authorization_code = request.json()['code']
        self.pin = request.json()['ecobeePin']
        logger.error('Please authorize your ecobee developer app with PIN code '
              + self.pin + '\nGoto https://www.ecobee.com/consumerportal'
              '/index.html, click\nMy Apps, Add application, Enter Pin'
              ' and click Authorize.\nAfter authorizing, call request_'
              'tokens() method.')

    def request_tokens(self):
        ''' Method to request API tokens from ecobee '''
        url = 'https://api.ecobee.com/token'
        params = {'grant_type': 'ecobeePin', 'code': self.authorization_code,
                  'client_id': self.api_key}
        try:
            request = requests.post(url, params=params)
        except RequestException:
            logger.warn("Error connecting to Ecobee.  Possible connectivity outage."
                        "Could not request token.")
            return
        if request.status_code == requests.codes.ok:
            self.access_token = request.json()['access_token']
            self.refresh_token = request.json()['refresh_token']
            self.write_tokens_to_file()
            self.pin = None
        else:
            logger.warn('Error while requesting tokens from ecobee.com.'
                  ' Status code: ' + str(request.status_code))
            return

    def refresh_tokens(self):
        ''' Method to refresh API tokens from ecobee '''
        url = 'https://api.ecobee.com/token'
        params = {'grant_type': 'refresh_token',
                  'refresh_token': self.refresh_token,
                  'client_id': self.api_key}
        request = requests.post(url, params=params)
        if request.status_code == requests.codes.ok:
            self.access_token = request.json()['access_token']
            self.refresh_token = request.json()['refresh_token']
            self.write_tokens_to_file()
            return True
        else:
            self.request_pin()

    def get_thermostats(self):
        ''' Set self.thermostats to a json list of thermostats from ecobee '''
        url = 'https://api.ecobee.com/1/thermostat'
        header = {'Content-Type': 'application/json;charset=UTF-8',
                  'Authorization': 'Bearer ' + self.access_token}
        params = {'json': ('{"selection":{"selectionType":"registered",'
                            '"includeRuntime":"true",'
                            '"includeSensors":"true",'
                            '"includeProgram":"true",'
                            '"includeEquipmentStatus":"true",'
                            '"includeEvents":"true",'
                            '"includeWeather":"true",'
                            '"includeSettings":"true"}}')}
        try:
            request = requests.get(url, headers=header, params=params)
        except RequestException:
            logger.warn("Error connecting to Ecobee.  Possible connectivity outage.")
            return None
        if request.status_code == requests.codes.ok:
            self.authenticated = True
            self.thermostats = request.json()['thermostatList']
            return self.thermostats
        else:
            self.authenticated = False
            logger.info("Error connecting to Ecobee while attempting to get "
                  "thermostat data.  Refreshing tokens and trying again.")
            if self.refresh_tokens():
                return self.get_thermostats()
            else:
                return None

    def get_thermostat(self, index):
        ''' Return a single thermostat based on index '''
        return self.thermostats[index]

    def get_remote_sensors(self, index):
        ''' Return remote sensors based on index '''
        return self.thermostats[index]['remoteSensors']

    def write_tokens_to_file(self):
        ''' Write api tokens to a file '''
        config = dict()
        config['API_KEY'] = self.api_key
        config['ACCESS_TOKEN'] = self.access_token
        config['REFRESH_TOKEN'] = self.refresh_token
        config['AUTHORIZATION_CODE'] = self.authorization_code
        if self.file_based_config:
            config_from_file(self.config_filename, config)
        else:
            self.config = config

    def update(self):
        ''' Get new thermostat data from ecobee '''
        self.get_thermostats()

    def make_request(self, body, log_msg_action):
        url = 'https://api.ecobee.com/1/thermostat'
        header = {'Content-Type': 'application/json;charset=UTF-8',
                  'Authorization': 'Bearer ' + self.access_token}
        params = {'format': 'json'}
        try:
            request = requests.post(url, headers=header, params=params, json=body)
        except RequestException:
            logger.warn("Error connecting to Ecobee.  Possible connectivity outage.")
            return None
        if request.status_code == requests.codes.ok:
            return request
        else:
            logger.info("Error connecting to Ecobee while attempting to %s.  "
                        "Refreshing tokens and trying again.", log_msg_action)
            if self.refresh_tokens():
                return self.make_request(body, log_msg_action)
            else:
                return None

    def set_hvac_mode(self, index, hvac_mode):
        ''' possible hvac modes are auto, auxHeatOnly, cool, heat, off '''
        body = {"selection": {"selectionType": "thermostats",
                              "selectionMatch": self.thermostats[index]['identifier']},
                              "thermostat": {
                                  "settings": {
                                      "hvacMode": hvac_mode
                                  }
                              }}
        log_msg_action = "set HVAC mode"
        return self.make_request(body, log_msg_action)

    def set_fan_min_on_time(self, index, fan_min_on_time):
        ''' The minimum time, in minutes, to run the fan each hour. Value from 1 to 60 '''
        body = {"selection": {"selectionType": "thermostats",
                        "selectionMatch": self.thermostats[index]['identifier']},
                        "thermostat": {
                            "settings": {
                                "fanMinOnTime": fan_min_on_time
                            }
                        }}
        log_msg_action = "set fan minimum on time."
        return self.make_request(body, log_msg_action)

    def set_fan_mode(self, index, fan_mode, cool_temp, heat_temp, hold_type="nextTransition"):
        ''' Set fan mode. Values: auto, minontime, on '''
        body = {"selection": {
                    "selectionType": "thermostats",
                    "selectionMatch": self.thermostats[index]['identifier']},
                "functions": [{"type": "setHold", "params": {
                    "holdType": hold_type,
                    "coolHoldTemp": int(cool_temp * 10),
                    "heatHoldTemp": int(heat_temp * 10),
                    "fan": fan_mode
                }}]}
        log_msg_action = "set fan mode"
        return self.make_request(body, log_msg_action)

    def set_hold_temp(self, index, cool_temp, heat_temp,
                      hold_type="nextTransition"):
        ''' Set a hold '''
        body = {"selection": {
                    "selectionType": "thermostats",
                    "selectionMatch": self.thermostats[index]['identifier']},
                "functions": [{"type": "setHold", "params": {
                    "holdType": hold_type,
                    "coolHoldTemp": int(cool_temp * 10),
                    "heatHoldTemp": int(heat_temp * 10)
                }}]}
        log_msg_action = "set hold temp"
        return self.make_request(body, log_msg_action)

    def set_climate_hold(self, index, climate, hold_type="nextTransition"):
        ''' Set a climate hold - ie away, home, sleep '''
        body = {"selection": {
                    "selectionType": "thermostats",
                    "selectionMatch": self.thermostats[index]['identifier']},
                "functions": [{"type": "setHold", "params": {
                    "holdType": hold_type,
                    "holdClimateRef": climate
                }}]}
        log_msg_action = "set climate hold"
        return self.make_request(body, log_msg_action)

    def delete_vacation(self, index, vacation):
        ''' Delete the vacation with name vacation '''
        body = {"selection": {
                    "selectionType": "thermostats",
                    "selectionMatch": self.thermostats[index]['identifier']},
                "functions": [{"type": "deleteVacation", "params": {
                    "name": vacation
                }}]}

        log_msg_action = "delete a vacation"
        return self.make_request(body, log_msg_action)

    def resume_program(self, index, resume_all=False):
        ''' Resume currently scheduled program '''
        body = {"selection": {
                    "selectionType": "thermostats",
                    "selectionMatch": self.thermostats[index]['identifier']},
                "functions": [{"type": "resumeProgram", "params": {
                    "resumeAll": resume_all
                }}]}

        log_msg_action = "resume program"
        return self.make_request(body, log_msg_action)

    def send_message(self, index, message="Hello from python-ecobee!"):
        ''' Send a message to the thermostat '''
        body = {"selection": {
                    "selectionType": "thermostats",
                    "selectionMatch": self.thermostats[index]['identifier']},
                "functions": [{"type": "sendMessage", "params": {
                    "text": message[0:500]
                }}]}

        log_msg_action = "send message"
        return self.make_request(body, log_msg_action)

    def set_humidity(self, index, humidity):
        ''' Set humidity level'''
        body = {"selection": {"selectionType": "thermostats",
                              "selectionMatch": self.thermostats[index]['identifier']},
                              "thermostat": {
                                  "settings": {
                                      "humidity": int(humidity)
                                  }
                              }}

        log_msg_action = "set humidity level"
        return self.make_request(body, log_msg_action)

    def set_mic_mode(self, index, mic_enabled):
        '''Enable/disable Alexa mic (only for Ecobee 4)
        Values: True, False
        '''

        body = {
            'selection': {
                'selectionType': 'thermostats',
                'selectionMatch': self.thermostats[index]['identifier']},
            'thermostat': {
                'audio': {
                    'microphoneEnabled': mic_enabled}}}

        log_msg_action = 'set mic mode'
        return self.make_request(body, log_msg_action)

    def set_occupancy_modes(self, index, auto_away=None, follow_me=None):
        '''Enable/disable Smart Home/Away and Follow Me modes
        Values: True, False
        '''

        body = {
            'selection': {
                'selectionType': 'thermostats',
                'selectionMatch': self.thermostats[index]['identifier']},
            'thermostat': {
                'settings': {
                    'autoAway': auto_away,
                    'followMeComfort': follow_me}}}

        log_msg_action = 'set occupancy modes'
        return self.make_request(body, log_msg_action)

    def set_dst_mode(self, index, dst):
        '''Enable/disable daylight savings
        Values: True, False
        '''

        body = {
            'selection': {
                'selectionType': 'thermostats',
                'selectionMatch': self.thermostats[index]['identifier']},
            'thermostat': {
                'location': {
                    'isDaylightSaving': dst}}}

        log_msg_action = 'set dst mode'
        return self.make_request(body, log_msg_action)


# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab 2
# Copyright 2016, 2017 juga (juga at riseup dot net), MIT license.
"""Timers for the DHCP client implementation of the Anonymity Profile
([:rfc:`7844`])."""
from __future__ import absolute_import

import logging
import random
from datetime import datetime, timedelta

from .constants import (DT_PRINT_FORMAT, MAX_DELAY_SELECTING, REBIND_PERC,
                        RENEW_PERC)

logger = logging.getLogger(__name__)

def future_dt_str(dt, td):
    """."""
    if isinstance(td, str):
        td = float(td)
    td = timedelta(seconds=td)
    future_dt = dt + td
    return future_dt.strftime(DT_PRINT_FORMAT)

def nowutc():
    """."""
    # NOTE: Not using UTC, as all the timers are set in reference to local time
    # now = datetime.utcnow().replace(tzinfo=utc)
    now = datetime.now()
    return now

def gen_delay_selecting():
    """Generate the delay in seconds in which the DISCOVER will be sent.

    [:rfc:`2131#section-4.4.1`]::

        The client SHOULD wait a random time between one and ten seconds to
        desynchronize the use of DHCP at startup.

    """
    delay = float(random.randint(0, MAX_DELAY_SELECTING))
    logger.debug('Delay to enter in SELECTING %s.', delay)
    logger.debug('SELECTING will happen on %s',
                 future_dt_str(nowutc(), delay))
    return delay

def gen_timeout_resend(attempts):
    """Generate the time in seconds in which DHCPDISCOVER wil be retransmited.

    [:rfc:`2131#section-3.1`]::

        might retransmit the
        DHCPREQUEST message four times, for a total delay of 60 seconds

    [:rfc:`2131#section-4.1`]::

        For example, in a 10Mb/sec Ethernet
        internetwork, the delay before the first retransmission SHOULD be 4
        seconds randomized by the value of a uniform random number chosen
        from the range -1 to +1.  Clients with clocks that provide resolution
        granularity of less than one second may choose a non-integer
        randomization value.  The delay before the next retransmission SHOULD
        be 8 seconds randomized by the value of a uniform number chosen from
        the range -1 to +1.  The retransmission delay SHOULD be doubled with
        subsequent retransmissions up to a maximum of 64 seconds.

    """
    timeout = 2 ** (attempts + 1) + random.uniform(-1, +1)
    logger.debug('next timeout resending will happen on %s',
                 future_dt_str(nowutc(), timeout))
    return timeout

def gen_timeout_request_renew(lease):
    """Generate time in seconds to retransmit DHCPREQUEST.

    [:rfc:`2131#section-4..4.5`]::

        In both RENEWING and REBINDING states,
        if the client receives no response to its DHCPREQUEST
        message, the client SHOULD wait one-half of the remaining
        time until T2 (in RENEWING state) and one-half of the
        remaining lease time (in REBINDING state), down to a
        minimum of 60 seconds, before retransmitting the
        DHCPREQUEST message.

    """
    time_left = (lease.rebinding_time - lease.renewing_time) * RENEW_PERC
    if time_left < 60:
        time_left = 60
    logger.debug('Next request in renew will happen on %s',
                 future_dt_str(nowutc(), time_left))
    return time_left

def gen_timeout_request_rebind(lease):
    """."""
    time_left = (lease.lease_time - lease.rebinding_time) * RENEW_PERC
    if time_left < 60:
        time_left = 60
    logger.debug('Next request on rebinding will happen on %s',
                 future_dt_str(nowutc(), time_left))
    return time_left

def gen_renewing_time(lease_time, elapsed=0):
    """Generate RENEWING time.

    [:rfc:`2131#section-4.4.5`]::

        T1
        defaults to (0.5 * duration_of_lease).  T2 defaults to (0.875 *
        duration_of_lease).  Times T1 and T2 SHOULD be chosen with some
        random "fuzz" around a fixed value, to avoid synchronization of
        client reacquisition.

    """
    renewing_time = int(lease_time) * RENEW_PERC - elapsed
    # FIXME:80 [:rfc:`2131#section-4.4.5`]: the chosen "fuzz" could fingerprint
    # the implementation
    # NOTE: here using same "fuzz" as systemd?
    range_fuzz = int(lease_time) * REBIND_PERC - renewing_time
    logger.debug('rebinding fuzz range %s', range_fuzz)
    fuzz = random.uniform(-(range_fuzz),
                          +(range_fuzz))
    renewing_time += fuzz
    logger.debug('Renewing time %s.', renewing_time)
    return renewing_time

def gen_rebinding_time(lease_time, elapsed=0):
    """."""
    rebinding_time = int(lease_time) * REBIND_PERC - elapsed
    # FIXME:90 [:rfc:`2131#section-4.4.5`]: the chosen "fuzz" could fingerprint
    # the implementation
    # NOTE: here using same "fuzz" as systemd?
    range_fuzz = int(lease_time) - rebinding_time
    logger.debug('rebinding fuzz range %s', range_fuzz)
    fuzz = random.uniform(-(range_fuzz),
                          +(range_fuzz))
    rebinding_time += fuzz
    logger.debug('Rebinding time %s.', rebinding_time)
    return rebinding_time


# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab
# Copyright 2016, 2017 juga (juga at riseup dot net), MIT license.
"""DCHP client implementation of the Anonymity Profiles [:rfc:`7844`]."""
from __future__ import absolute_import, unicode_literals

import logging

from netaddr import AddrFormatError
from scapy.arch import get_if_raw_hwaddr
from scapy.automaton import ATMT, Automaton
from scapy.config import conf
from scapy.sendrecv import sendp
from scapy.utils import str2mac

from .clientscript import ClientScript
from .constants import (CLIENT_PORT, DELAY_SELECTING, FSM_ATTRS, LEASE_TIME,
                        MAX_ATTEMPTS_DISCOVER, MAX_ATTEMPTS_REQUEST,
                        MAX_OFFERS_COLLECTED, REBINDING_TIME, RENEWING_TIME,
                        SERVER_PORT, STATE_BOUND, STATE_END, STATE_ERROR,
                        STATE_INIT, STATE_PREINIT, STATE_REBINDING,
                        STATE_RENEWING, STATE_REQUESTING, STATE_SELECTING,
                        STATES2NAMES, TIMEOUT_REQUEST_REBINDING,
                        TIMEOUT_REQUEST_RENEWING, TIMEOUT_REQUESTING,
                        TIMEOUT_SELECTING)
from .dhcpcap import DHCPCAP
from .dhcpcaputils import isack, isnak, isoffer
from .timers import (gen_delay_selecting, gen_timeout_request_rebind,
                     gen_timeout_request_renew, gen_timeout_resend, nowutc)
from .netutils import set_net

logger = logging.getLogger(__name__)

class DHCPCAPFSM(Automaton):
    """DHCP client Finite State Machine (FSM)."""

    def dict_self(self):
        """Return the self object attributes not inherited as dict."""
        return {k: v for k, v in self.__dict__.items() if k in FSM_ATTRS}

    def __str__(self):
        return str(self.dict_self())

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

    def reset(self, iface=None, client_mac=None, xid=None, scriptfile=None):
        """Reset object attributes when state is INIT."""
        logger.debug('Reseting attributes.')
        if iface is None:
            iface = conf.iface
        if client_mac is None:
            # scapy for python 3 returns byte, not tuple
            tempmac = get_if_raw_hwaddr(iface)
            if isinstance(tempmac, tuple) and len(tempmac) == 2:
                mac = tempmac[1]
            else:
                mac = tempmac
            client_mac = str2mac(mac)
        self.client = DHCPCAP(iface=iface, client_mac=client_mac, xid=xid)
        if scriptfile is not None:
            self.script = ClientScript(scriptfile)
        else:
            self.script = None
        self.time_sent_request = None
        self.discover_attempts = 0
        self.request_attempts = 0
        self.current_state = STATE_PREINIT
        self.offers = list()

    def __init__(self, iface=None, server_port=None,
                 client_port=None, client_mac=None, xid=None,
                 scriptfile=None, delay_selecting=False,
                 delay_before_selecting=None,
                 timeout_select=None, debug_level=5, *args, **kargs):
        """Overwrites Automaton __init__ method.

        [ :rfc:`7844#section-3.4` ] ::
            If the hardware address is reset to a new
            randomized value, the DHCP client SHOULD use the new randomized
            value in the DHCP messages

        """
        logger.debug('Inizializating FSM.')
        super(DHCPCAPFSM, self).__init__(*args, **kargs)
        self.debug_level = debug_level
        self.delay_selecting = delay_selecting
        self.delay_before_selecting = delay_before_selecting
        self.timeout_select = timeout_select
        self.reset(iface, client_mac, xid, scriptfile)
        self.client.server_port = server_port or SERVER_PORT
        self.client.client_port = client_port or CLIENT_PORT
        self.socket_kargs = {
            'filter': 'udp and src port {0} and dst port {1}'
                      ' and ether dst {2}'.
                      format(self.client.server_port,
                             self.client.client_port,
                             self.client.client_mac)
        }
        if self.script is not None:
            self.script.script_init(self.client.lease, self.current_state)
            self.script.script_go()
        logger.debug('FSM thread id: %s.', self.threadid)

    def get_timeout(self, state, function):
        """Workaround to get timeout in the ATMT.timeout class method."""
        state = STATES2NAMES[state]
        for timeout_fn_t in self.timeout[state]:
            # access the function name
            if timeout_fn_t[1] is not None and \
               timeout_fn_t[1].atmt_condname == function.atmt_condname:
                logger.debug('Timeout for state %s, function %s, is %s',
                             state, function.atmt_condname, timeout_fn_t[0])
                return timeout_fn_t[0]
        return None

    def set_timeout(self, state, function, newtimeout):
        """
        Workaround to change timeout values in the ATMT.timeout class method.

        self.timeout format is::

            {'STATE': [
                (TIMEOUT0, <function foo>),
                (TIMEOUT1, <function bar>)),
                (None, None)
                ],
            }

        """
        state = STATES2NAMES[state]
        for timeout_fn_t in self.timeout[state]:
            # access the function name
            if timeout_fn_t[1] is not None and \
               timeout_fn_t[1].atmt_condname == function.atmt_condname:
                # convert list to tuple to make it mutable
                timeout_l = list(timeout_fn_t)
                # modify the timeout
                timeout_l[0] = newtimeout
                # set the new timeoute to self.timeout
                i = self.timeout[state].index(timeout_fn_t)
                self.timeout[state][i] = tuple(timeout_l)
                logger.debug('Set state %s, function %s, to timeout %s',
                             state, function.atmt_condname, newtimeout)

    def send_discover(self):
        """Send discover."""
        assert self.client
        assert self.current_state == STATE_INIT or \
            self.current_state == STATE_SELECTING
        pkt = self.client.gen_discover()
        sendp(pkt)
        # FIXME:20 check that this is correct,: all or only discover?
        if self.discover_attempts < MAX_ATTEMPTS_DISCOVER:
            self.discover_attempts += 1
        timeout = gen_timeout_resend(self.discover_attempts)
        self.set_timeout(self.current_state,
                         self.timeout_selecting,
                         timeout)
        # logger.info('DHCPDISCOVER on %s to %s port %s' %
        #             (self.client.iface, self.client.server_mac,
        #              self.client.server_port)))

    def select_offer(self):
        """Select an offer from the offers received.

        [:rfc:`2131#section-4.2`]::

            DHCP clients are free to use any strategy in selecting a DHCP
            server among those from which the client receives a DHCPOFFER.

        [:rfc:`2131#section-4.4.1`]::

            The time
            over which the client collects messages and the mechanism used to
            select one DHCPOFFER are implementation dependent.

        Nor [:rfc:`7844`] nor [:rfc:`2131`] specify the algorithm.
        Here, currently the first offer is selected.

        .. todo::

           - Check other implementations algorithm to select offer.

        """
        logger.debug('Selecting offer.')
        pkt = self.offers[0]
        self.client.handle_offer(pkt)

    def send_request(self):
        """Send request.

        [:rfc:`2131#section-3.1`]::

        a client retransmitting as described in section 4.1 might retransmit
        the DHCPREQUEST message four times, for a total delay of 60 seconds

        .. todo::

           - The maximum number of retransmitted REQUESTs is per state or in
             total?
           - Are the retransmitted REQUESTs independent to the retransmitted
             DISCOVERs?

        """
        assert self.client
        if self.current_state == STATE_BOUND:
            pkt = self.client.gen_request_unicast()
        else:
            pkt = self.client.gen_request()
        sendp(pkt)
        logger.debug('Modifying FSM obj, setting time_sent_request.')
        self.time_sent_request = nowutc()
        logger.info('DHCPREQUEST of %s on %s to %s port %s',
                    self.client.iface, self.client.client_ip,
                    self.client.server_ip, self.client.server_port)

        # NOTE: see previous TODO, maybe the MAX_ATTEMPTS_REQUEST needs to be
        # calculated per state.
        if self.request_attempts < MAX_ATTEMPTS_REQUEST:
            self.request_attempts *= 2
            logger.debug('Increased request attempts to %s',
                         self.request_attempts)
        if self.current_state == STATE_RENEWING:
            timeout_renewing = gen_timeout_request_renew(self.client.lease)
            self.set_timeout(self.current_state,
                             self.timeout_request_renewing,
                             timeout_renewing)
        elif self.current_state == STATE_REBINDING:
            timeout_rebinding = gen_timeout_request_rebind(self.client.lease)
            self.set_timeout(self.current_state,
                             self.timeout_request_rebinding,
                             timeout_rebinding)
        else:
            timeout_requesting = \
                gen_timeout_resend(self.request_attempts)
            self.set_timeout(self.current_state,
                             self.timeout_requesting,
                             timeout_requesting)

    def set_timers(self):
        """Set renewal, rebinding times."""
        logger.debug('setting timeouts')
        self.set_timeout(self.current_state,
                         self.renewing_time_expires,
                         self.client.lease.renewal_time)
        self.set_timeout(self.current_state,
                         self.rebinding_time_expires,
                         self.client.lease.rebinding_time)

    def process_received_ack(self, pkt):
        """Process a received ACK packet.

        Not specifiyed in [:rfc:`7844`].
        Probe the offered IP in [:rfc:`2131#section-2.2.`]::

            the allocating
            server SHOULD probe the reused address before allocating the
            address, e.g., with an ICMP echo request, and the client SHOULD
            probe the newly received address, e.g., with ARP.

            The client SHOULD broadcast an ARP
            reply to announce the client's new IP address and clear any
            outdated ARP cache entries in hosts on the client's subnet.

        It is also not specifiyed in [:rfc:`7844`] nor [:rfc:`2131`] how to
        check that the offered IP is valid.

        .. todo::
           - Check that nor ``dhclient`` nor ``systemd-networkd`` send an ARP.
           - Check how other implementations check that the ACK paremeters
             are valid, ie, if the ACK fields match the fields in the OFFER.
           - Check to which state the client should go back to when the
             offered parameters are not valid.

        """
        if isack(pkt):
            try:
                self.event = self.client.handle_ack(pkt,
                                                    self.time_sent_request)
            except AddrFormatError as err:
                logger.error(err)
                # NOTE: see previous TODO, maybe should go back to other state.
                raise self.SELECTING()
            # NOTE: see previous TODO, not checking address with ARP.
            logger.info('DHCPACK of %s from %s' %
                        (self.client.client_ip, self.client.server_ip))
            return True
        return False

    def process_received_nak(self, pkt):
        """Process a received NAK packet."""
        if isnak(pkt):
            logger.info('DHCPNAK of %s from %s',
                        self.client.client_ip, self.client.server_ip)
            return True
        return False

    #################################################################
    # State machine
    #################################################################

    # STATES
    #########

    @ATMT.state(initial=1)
    def INIT(self):
        """INIT state.

        [:rfc:`2131#section-4.4.1`]::

            The client SHOULD wait a random time between one and ten
            seconds to desynchronize the use of DHCP at startup

        .. todo::
           - The initial delay is implemented, but probably is not in other
             implementations. Check what other implementations do.
        """
        # NOTE: in case INIT is reached from other state, initialize attributes
        # reset all variables.
        logger.debug('In state: INIT')
        if self.current_state is not STATE_PREINIT:
            self.reset()
        self.current_state = STATE_INIT
        # NOTE: see previous TODO, maybe this is not needed.
        if self.delay_selecting:
            if self.delay_before_selecting is None:
                delay_before_selecting = gen_delay_selecting()
            else:
                delay_before_selecting = self.delay_before_selecting
        else:
            delay_before_selecting = 0
        self.set_timeout(self.current_state,
                         self.timeout_delay_before_selecting,
                         delay_before_selecting)
        if self.timeout_select is not None:
            self.set_timeout(STATE_SELECTING,
                             self.timeout_selecting,
                             self.timeout_select)

    @ATMT.state()
    def SELECTING(self):
        """SELECTING state."""
        logger.debug('In state: SELECTING')
        self.current_state = STATE_SELECTING

    @ATMT.state()
    def REQUESTING(self):
        """REQUESTING state."""
        logger.debug('In state: REQUESTING')
        self.current_state = STATE_REQUESTING

    @ATMT.state()
    def BOUND(self):
        """BOUND state."""
        logger.debug('In state: BOUND')
        logger.info('(%s) state changed %s -> bound', self.client.iface,
                    STATES2NAMES[self.current_state])
        self.current_state = STATE_BOUND
        self.client.lease.info_lease()
        if self.script is not None:
            self.script.script_init(self.client.lease, self.current_state)
            self.script.script_go()
        else:
            try:
                set_net(self.client.lease)
            except Exception as e:
                logger.error('Can not set IP', exc_info=True)
                # raise self.END()
        # TODO: go daemon?

    @ATMT.state()
    def RENEWING(self):
        """RENEWING state."""
        logger.debug('In state: RENEWING')
        self.current_state = STATE_RENEWING
        if self.script is not None:
            self.script.script_init(self.client.lease, self.current_state)
            self.script.script_go()
        else:
            set_net(self.client.lease)

    @ATMT.state()
    def REBINDING(self):
        """REBINDING state."""
        logger.debug('In state: REBINDING')
        self.current_state = STATE_REBINDING
        if self.script is not None:
            self.script.script_init(self.client.lease, self.current_state)
            self.script.script_go()
        else:
            set_net(self.client.lease)

    @ATMT.state(final=1)
    def END(self):
        """END state."""
        logger.debug('In state: END')
        self.current_state = STATE_END
        if self.script is not None:
            self.script.script_init(self.client.lease, self.current_state)
            self.script.script_go()
        else:
            set_net(self.client.lease)
        return

    @ATMT.state(error=1)
    def ERROR(self):
        """ERROR state."""
        logger.debug('In state: ERROR')
        self.current_state = STATE_ERROR
        if self.script is not None:
            self.script.script_init(self.client.lease, self.current_state)
            self.script.script_go()
        set_net(self.client.lease)
        raise self.INIT()

    # TIMEOUTS
    ###########

    # TIMEOUTS: retransmissions
    # ----------------------------
    @ATMT.timeout(INIT, DELAY_SELECTING)
    def timeout_delay_before_selecting(self):
        """Timeout delay selecting in INIT state."""
        logger.debug('C1:T. In %s, timeout delay selecting, raise SELECTING',
                     self.current_state)
        raise self.SELECTING()

    @ATMT.timeout(SELECTING, TIMEOUT_SELECTING)
    def timeout_selecting(self):
        """Timeout of selecting on SELECTING state.

        Not specifiyed in [:rfc:`7844`].
        See comments in :func:`dhcpcapfsm.DHCPCAPFSM.timeout_request`.

        """
        logger.debug('C2.1: T In %s, timeout receiving response to select.',
                     self.current_state)

        if len(self.offers) >= MAX_OFFERS_COLLECTED:
            logger.debug('C2.2: T Maximum number of offers reached, '
                         'raise REQUESTING.')
            raise self.REQUESTING()

        if self.discover_attempts >= MAX_ATTEMPTS_DISCOVER:
            logger.debug('C2.3: T Maximum number of discover retries is %s'
                         ' and already sent %s.',
                         MAX_ATTEMPTS_DISCOVER, self.discover_attempts)
            if len(self.offers) <= 0:
                logger.debug('C2.4: T. But no OFFERS where received, '
                             'raise ERROR.')
                raise self.ERROR()
            logger.debug('C2.4: F. But there is some OFFERS, '
                         'raise REQUESTING.')
            raise self.REQUESTING()

        logger.debug('C2.2: F. Still not received all OFFERS, but not '
                     'max # attemps reached, raise SELECTING.')
        raise self.SELECTING()

    @ATMT.timeout(REQUESTING, TIMEOUT_REQUESTING)
    def timeout_requesting(self):
        """Timeout requesting in REQUESTING state.

        Not specifiyed in [:rfc:`7844`]

        [:rfc:`2131#section-3.1`]::

            might retransmit the
            DHCPREQUEST message four times, for a total delay of 60 seconds

        """
        logger.debug("C3.2: T. In %s, timeout receiving response to request, ",
                     self.current_state)
        if self.discover_requests >= MAX_ATTEMPTS_REQUEST:
            logger.debug('C2.3: T. Maximum number %s of REQUESTs '
                         'reached, already sent %s, raise ERROR.',
                         MAX_ATTEMPTS_REQUEST, self.disover_requests)
            raise self.ERROR()
        logger.debug("C2.3: F. Maximum number of REQUESTs retries not reached,"
                     "raise REQUESTING.")
        raise self.REQUESTING()

    @ATMT.timeout(RENEWING, TIMEOUT_REQUEST_RENEWING)
    def timeout_request_renewing(self):
        """Timeout of renewing on RENEWING state.

        Same comments as in
        :func:`dhcpcapfsm.DHCPCAPFSM.timeout_requesting`.

        """
        logger.debug("C5.2:T In %s, timeout receiving response to request.",
                     self.current_state)
        if self.request_attempts >= MAX_ATTEMPTS_REQUEST:
            logger.debug('C2.3: T Maximum number %s of REQUESTs '
                         'reached, already sent %s, wait to rebinding time.',
                         MAX_ATTEMPTS_REQUEST, self.disover_requests)
            # raise self.ERROR()
        logger.debug("C2.3: F. Maximum number of REQUESTs retries not reached,"
                     "raise RENEWING.")
        raise self.RENEWING()

    @ATMT.timeout(REBINDING, TIMEOUT_REQUEST_REBINDING)
    def timeout_request_rebinding(self):
        """Timeout of request rebinding on REBINDING state.

        Same comments as in
        :func:`dhcpcapfsm.DHCPCAPFSM.timeout_requesting`.

        """
        logger.debug("C6.2:T In %s, timeout receiving response to request.",
                     self.current_state)
        if self.request_attempts >= MAX_ATTEMPTS_REQUEST:
            logger.debug('C.2.3: T. Maximum number %s of REQUESTs '
                         'reached, already sent %s, wait lease time expires.',
                         MAX_ATTEMPTS_REQUEST, self.disover_requests)
            # raise self.ERROR()
        logger.debug("C2.3: F. Maximum number of REQUESTs retries not reached,"
                     "raise REBINDING.")
        raise self.REBINDING()

    # TIMEOUTS: timers
    # -----------------

    @ATMT.timeout(BOUND, RENEWING_TIME)
    def renewing_time_expires(self):
        """Timeout renewing time (T1), transition to RENEWING."""
        logger.debug("C4. Timeout renewing time, in BOUND state, "
                     "raise RENEWING.")
        raise self.RENEWING()

    @ATMT.timeout(RENEWING, REBINDING_TIME)
    def rebinding_time_expires(self):
        """Timeout rebinding time (T2), transition to REBINDING."""
        logger.debug("C5.3. Timeout rebinding time, in RENEWING state, "
                     "raise REBINDING.")
        raise self.REBINDING()

    @ATMT.timeout(REBINDING, LEASE_TIME)
    def lease_expires(self):
        """Timeout lease time, transition to INIT.

        Not sending DHCPRELEASE to minimize deanonymization

        [:rfc:`2131#section-4.4.6`]::

            Note that the correct operation
            of DHCP does not depend on the transmission of DHCPRELEASE.

        """
        logger.debug("C6.3. Timeout lease time, in REBINDING state, "
                     "raise INIT.")
        raise self.STATE_INIT()

    # RECEIVE CONDITIONS
    ####################

    @ATMT.receive_condition(SELECTING)
    def receive_offer(self, pkt):
        """Receive offer on SELECTING state."""
        logger.debug("C2. Received OFFER?, in SELECTING state.")
        if isoffer(pkt):
            logger.debug("C2: T, OFFER received")
            self.offers.append(pkt)
            if len(self.offers) >= MAX_OFFERS_COLLECTED:
                logger.debug("C2.5: T, raise REQUESTING.")
                self.select_offer()
                raise self.REQUESTING()
            logger.debug("C2.5: F, raise SELECTING.")
            raise self.SELECTING()

    # same as:, but would can not be overloaded
    # @ATMT.receive_condition(RENEWING)
    # @ATMT.receive_condition(REBINDING)
    @ATMT.receive_condition(REQUESTING)
    def receive_ack_requesting(self, pkt):
        """Receive ACK in REQUESTING state."""
        logger.debug("C3. Received ACK?, in REQUESTING state.")
        if self.process_received_ack(pkt):
            logger.debug("C3: T. Received ACK, in REQUESTING state, "
                         "raise BOUND.")
            raise self.BOUND()

    # same as:, but would can not be overloaded
    # @ATMT.receive_condition(RENEWING)
    # @ATMT.receive_condition(REBINDING)
    @ATMT.receive_condition(REQUESTING)
    def receive_nak_requesting(self, pkt):
        """Receive NAK in REQUESTING state."""
        logger.debug("C3.1. Received NAK?, in REQUESTING state.")
        if self.process_received_nak(pkt):
            logger.debug("C3.1: T. Received NAK, in REQUESTING state, "
                         "raise INIT.")
            raise self.INIT()

    @ATMT.receive_condition(RENEWING)
    def receive_ack_renewing(self, pkt):
        """Receive ACK in RENEWING state."""
        logger.debug("C3. Received ACK?, in RENEWING state.")
        if self.process_received_ack(pkt):
            logger.debug("C3: T. Received ACK, in RENEWING state, "
                         "raise BOUND.")
            raise self.BOUND()

    @ATMT.receive_condition(RENEWING)
    def receive_nak_renewing(self, pkt):
        """Receive NAK in RENEWING state."""
        logger.debug("C3.1. Received NAK?, in RENEWING state.")
        if self.process_received_nak(pkt):
            logger.debug("C3.1: T. Received NAK, in RENEWING state, "
                         " raise INIT.")
            raise self.INIT()

    @ATMT.receive_condition(REBINDING)
    def receive_ack_rebinding(self, pkt):
        """Receive ACK in REBINDING state."""
        logger.debug("C3. Received ACK?, in REBINDING state.")
        if self.process_received_ack(pkt):
            logger.debug("C3: T. Received ACK, in REBINDING state, "
                         "raise BOUND.")
            raise self.BOUND()

    @ATMT.receive_condition(REBINDING)
    def receive_nak_rebinding(self, pkt):
        """Receive NAK in REBINDING state."""
        logger.debug("C3.1. Received NAK?, in RENEWING state.")
        if self.process_received_nak(pkt):
            logger.debug("C3.1: T. Received NAK, in RENEWING state, "
                         "raise INIT.")
            raise self.INIT()

    # ACTIONS
    ##########

    # ACTIONS: on timeouts
    # -----------------------

    @ATMT.action(timeout_delay_before_selecting)
    @ATMT.action(timeout_selecting)
    def action_transmit_discover(self):
        """Action on timeout, send DISCOVER."""
        logger.debug('Action on timeout, in state %s: send DISCOVER.',
                     self.current_state)
        self.send_discover()

    @ATMT.action(timeout_requesting)
    @ATMT.action(timeout_request_rebinding)
    @ATMT.action(rebinding_time_expires)
    @ATMT.action(receive_offer)
    @ATMT.action(timeout_request_renewing)
    @ATMT.action(renewing_time_expires)
    def action_transmit_request(self):
        """Action on X: send REQUEST."""
        logger.debug('Action on timeout X/receive X, in state %s: '
                     'send REQUEST.', self.current_state)
        self.send_request()

    # ACTIONS: on receive conditions
    # -------------------------------

    @ATMT.action(receive_ack_rebinding)
    @ATMT.action(receive_ack_requesting)
    def on_ack_requesting(self):
        """Action on receive ACK requesting in REQUESTING state: set timers."""
        # NOTE: not recording lease
        logger.debug('Action on receive ACK in REQUESTING state: set timers.')
        self.set_timers()

    @ATMT.action(receive_ack_renewing)
    def on_renewing(self):
        """Action on renewing on RENEWING state.

        Not recording lease, but restarting timers.

        """
        self.client.lease.sanitize_net_values()
        self.client.lease.set_times(self.time_sent_request)
        self.set_timers()


# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab 2
# Copyright 2016, 2017 juga (juga at riseup dot net), MIT license.
"""Util functions for the DHCP client implementation of the Anonymity Profile
([:rfc:`7844`])."""
from __future__ import absolute_import

import logging
import random

from scapy.arch.linux import get_if_list
from scapy.layers.dhcp import DHCP, DHCPTypes

from .constants import XID_MIN, XID_MAX

logger = logging.getLogger(__name__)

def isoffer(packet):
    """."""
    if DHCP in packet and (DHCPTypes.get(packet[DHCP].options[0][1]) ==
                           'offer' or packet[DHCP].options[0][1] == "offer"):
        logger.debug('Packet is Offer.')
        return True
    return False

def isnak(packet):
    """."""
    if DHCP in packet and (DHCPTypes.get(packet[DHCP].options[0][1]) ==
                           'nak' or packet[DHCP].options[0][1] == 'nak'):
        logger.debug('Packet is NAK.')
        return True
    return False

def isack(packet):
    """."""
    if DHCP in packet and (DHCPTypes.get(packet[DHCP].options[0][1]) ==
                           'ack' or packet[DHCP].options[0][1] == 'ack'):
        logger.debug('Packet is ACK.')
        return True
    return False

def gen_xid():
    return random.randint(XID_MIN, XID_MAX)

def discover_ifaces():
    ifaces = get_if_list()
    ifaces.remove('lo')
    logger.debug('Disovered interfaces %s.', ifaces)
    return ifaces

def detect_speed_network():
    # 100 Mbps = 100 Mb/s
    with open('/sys/class/net/eth0/speed') as fd:
        speed = fd.read()
    logger.debug('Net speed %s', speed)
    return speed

# TODO
def detect_initial_network():
    pass


import django
from django.utils import six

if six.PY3:
    from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode
else:
    from urlparse import urlparse, parse_qsl, urlunparse
    from urllib import urlencode

class Qurl(object):
    """
    Initialized with a url, provides a few chainable methods
    to manipulate its querystring.
    eg:
        >>> Qurl('http://www.sophilabs.co/?page=1&tags=python')\
                .inc('page')\
                .add('tags', 'django')\
                .add('tags', 'web')\
                .remove('tags', 'python')\
                .get()
        http://www.sophilabs.co/?page=2&tags=django&tags=web
    """

    def __init__(self, url):
        self.url = url
        self._qsl = parse_qsl(urlparse(url).query)

    def set(self, name, value):
        """ Assign a value, remove if it's None """
        clone = self._clone()
        if django.VERSION[0] <= 1 and django.VERSION[1] <= 4:
            value = value or None
        clone._qsl = [(q, v) for (q, v) in self._qsl if q != name]
        if value is not None:
            clone._qsl.append((name, value))
        return clone

    def add(self, name, value):
        """ Append a value to multiple value parameter. """
        clone = self._clone()
        clone._qsl = [p for p in self._qsl
                      if not(p[0] == name and p[1] == value)]
        clone._qsl.append((name, value,))
        return clone

    def remove(self, name, value):
        """ Remove a value from multiple value parameter. """
        clone = self._clone()
        clone._qsl = [qb for qb in self._qsl if qb != (name, str(value))]
        return clone

    def inc(self, name, value=1):
        """ Increment value """
        clone = self._clone()
        clone._qsl = [(q, v) if q != name else (q, int(v) + value)
                      for (q, v) in self._qsl]
        if name not in dict(clone._qsl).keys():
            clone._qsl.append((name, value))
        return clone

    def dec(self, name, value=1):
        """ Decrement value """
        return self._clone().inc(name, -value)

    def _clone(self):
        return Qurl(self.get())

    def get(self):
        parsed = list(urlparse(self.url))
        parsed[4] = urlencode(self._qsl)
        return urlunparse(parsed)