from abc import ABC, abstractmethod
import pickle

import cloudpickle


class AlreadySteppingError(Exception):
    """
    Raised when an asynchronous step is running while
    step_async() is called again.
    """
    
    def __init__(self):
        msg = 'already running an async step'
        Exception.__init__(self, msg)


class NotSteppingError(Exception):
    """
    Raised when an asynchronous step is not running but
    step_wait() is called.
    """
    
    def __init__(self):
        msg = 'not running an async step'
        Exception.__init__(self, msg)


class VecEnv(ABC):
    """
    An abstract asynchronous, vectorized environment.

    :param num_envs: (int) the number of environments
    :param observation_space: (Gym Space) the observation space
    :param action_space: (Gym Space) the action space
    """
    
    def __init__(self, num_envs, observation_space, action_space):
        self.num_envs = num_envs
        self.observation_space = observation_space
        self.action_space = action_space
    
    @abstractmethod
    def reset(self):
        """
        Reset all the environments and return an array of
        observations, or a dict of observation arrays (for goal envs).

        If step_async is still doing work, that work will
        be cancelled and step_wait() should not be called
        until step_async() is invoked again.

        :return: ([int] or [float] or dict) observation
        """
        pass
    
    @abstractmethod
    def step_async(self, actions):
        """
        Tell all the environments to start taking a step
        with the given actions.
        Call step_wait() to get the results of the step.

        You should not call this if a step_async run is
        already pending.
        """
        pass
    
    @abstractmethod
    def step_wait(self):
        """
        Wait for the step taken with step_async().

        :return: ([int] or [float] or dict, [float], [bool], dict) observation, reward, done, information
        """
        pass
    
    @abstractmethod
    def close(self):
        """
        Clean up the environment's resources.
        """
        pass
    
    def step(self, actions):
        """
        Step the environments with the given action

        :param actions: ([int] or [float]) the action
        :return: ([int] or [float] or dict, [float], [bool], dict) observation, reward, done, information
        """
        self.step_async(actions)
        return self.step_wait()
    
    def get_images(self):
        """
        Return RGB images from each environment
        """
        raise NotImplementedError
    
    def render(self, *args, **kwargs):
        """
        Gym environment rendering

        :param mode: (str) the rendering type
        """
        pass
    
    @property
    def unwrapped(self):
        if isinstance(self, VecEnvWrapper):
            return self.venv.unwrapped
        else:
            return self
    
    def _get_indices(self, indices):
        """
        Convert a flexibly-typed reference to environment indices to an implied list of indices.
        :param indices: (None,int,Iterable) refers to indices of envs.
        :return: (list) the implied list of indices.
        """
        if indices is None:
            indices = range(self.num_envs)
        elif isinstance(indices, int):
            indices = [indices]
        return indices


class VecEnvWrapper(VecEnv):
    """
    Vectorized environment base class

    :param venv: (VecEnv) the vectorized environment to wrap
    :param observation_space: (Gym Space) the observation space (can be None to load from venv)
    :param action_space: (Gym Space) the action space (can be None to load from venv)
    """
    
    def __init__(self, venv, observation_space=None, action_space=None):
        self.venv = venv
        VecEnv.__init__(self, num_envs=venv.num_envs, observation_space=observation_space or venv.observation_space,
                        action_space=action_space or venv.action_space)
    
    def step_async(self, actions):
        self.venv.step_async(actions)
    
    @abstractmethod
    def reset(self):
        pass
    
    @abstractmethod
    def step_wait(self):
        pass
    
    def close(self):
        return self.venv.close()
    
    def render(self, *args, **kwargs):
        return self.venv.render(*args, **kwargs)
    
    def get_images(self):
        return self.venv.get_images()


class CloudpickleWrapper(object):
    def __init__(self, var):
        """
        Uses cloudpickle to serialize contents (otherwise multiprocessing tries to use pickle)

        :param var: (Any) the variable you wish to wrap for pickling with cloudpickle
        """
        self.var = var
    
    def __getstate__(self):
        return cloudpickle.dumps(self.var)
    
    def __setstate__(self, obs):
        self.var = pickle.loads(obs)
