import sys, re, os, multiprocessing
import os.path as osp
import json

import gym
from collections import defaultdict
from aril_utils import load_variables

import numpy as np

import tensorflow as tf
import joblib

from rarl_env import RarlEnv
from baselines import logger
from baselines.common.vec_env.vec_video_recorder import VecVideoRecorder
from baselines.common.tf_util import get_session, get_available_gpus, save_variables
from importlib import import_module

# import `exptools` stuff
from exptools.collections import AttrDict
from exptools.logging import logger as expLogger
from exptools.logging import context as expContext
from exptools.launching.variant import load_variant
from exptools.launching.affinity import affinity_from_code
expLogger.tb_writer = False # Due to tensorflow version, disable that


try:
    from mpi4py import MPI
except ImportError:
    MPI = None

try:
    import pybullet_envs
except ImportError:
    pybullet_envs = None

try:
    import roboschool
except ImportError:
    roboschool = None

_game_envs = defaultdict(set)
for env in gym.envs.registry.all():
    # TODO: solve this with regexes
    env_type = env.entry_point.split(':')[0].split('.')[-1]
    _game_envs[env_type].add(env.id)

def get_env_type(args):
    env_id = args.env

    if args.env_type is not None:
        return args.env_type, env_id

    # Re-parse the gym registry, since we could have new envs since last time.
    for env in gym.envs.registry.all():
        env_type = env.entry_point.split(':')[0].split('.')[-1]
        _game_envs[env_type].add(env.id)  # This is a set so add is idempotent

    if env_id in _game_envs.keys():
        env_type = env_id
        env_id = [g for g in _game_envs[env_type]][0]
    else:
        env_type = None
        for g, e in _game_envs.items():
            if env_id in e:
                env_type = g
                break
        if ':' in env_id:
            env_type = re.sub(r':.*', '', env_id)
        assert env_type is not None, 'env_id {} is not recognized in env types'.format(env_id, _game_envs.keys())

    return env_type, env_id

def build_env(args):
    env = RarlEnv(get_session(), args.env, 
        **args.env_kwargs,
    )
    return env

def get_alg_module(alg, submodule=None):
    submodule = submodule or alg
    try:
        # first try to import the alg module from baselines
        alg_module = import_module('.'.join(['baselines', alg, submodule]))
    except ImportError:
        # then from rl_algs
        alg_module = import_module('.'.join(['rl_' + 'algs', alg, submodule]))

    return alg_module

def get_learn_function(alg):
    return get_alg_module(alg).learn


def get_learn_function_defaults(alg, env_type):
    try:
        alg_defaults = get_alg_module(alg, 'defaults')
        kwargs = getattr(alg_defaults, env_type)()
    except (ImportError, AttributeError):
        kwargs = {}
    return kwargs

def get_default_network(env_type):
    if env_type in {'atari', 'retro'}:
        return 'cnn'
    else:
        return 'mlp'

def train(args, extra_args):
    env_type, env_id = get_env_type(args)
    print('env_type: {}'.format(env_type))

    total_timesteps = 0

    learn = get_learn_function(args.alg)
    alg_kwargs = get_learn_function_defaults(args.alg, env_type)
    alg_kwargs.update(extra_args)

    env = build_env(args)
    if args.save_video_interval != 0:
        env = VecVideoRecorder(env, osp.join(logger.get_dir(), "videos"), record_video_trigger=lambda x: x % args.save_video_interval == 0, video_length=args.save_video_length)

    if args.network:
        alg_kwargs['network'] = args.network
    else:
        if alg_kwargs.get('network') is None:
            alg_kwargs['network'] = get_default_network(env_type)

    with tf.variable_scope(args.attacker_name, reuse= False):
        attacker_model = learn(
            env=env.as_attacker_env(None, None), # just create a false env
            total_timesteps=total_timesteps,
            # epsilon=args.epsilon,
            **alg_kwargs
        )
    with tf.variable_scope(args.victim_name, reuse= False):
        victim_model = learn(
            env=env.as_victim_env(None, None), # just create a false env
            total_timesteps=total_timesteps,
            # epsilon=args.epsilon,
            **alg_kwargs
        )

    return env, attacker_model, victim_model

def contine_train(env, model_name, total_timesteps, args, extra_args):
    env_type, env_id = get_env_type(args)
    
    learn = get_learn_function(args.alg)
    alg_kwargs = get_learn_function_defaults(args.alg, env_type)
    alg_kwargs.update(extra_args)

    if args.network:
        alg_kwargs['network'] = args.network
    else:
        if alg_kwargs.get('network') is None:
            alg_kwargs['network'] = get_default_network(env_type)
    
    with tf.variable_scope(model_name, reuse= True):
        model = learn(
            env=env,
            total_timesteps=total_timesteps,
            # epsilon=args.epsilon,
            **alg_kwargs
        )

    return model


def main(log_dir, filename):
    with open(os.path.join(log_dir, "variant_config.json")) as f:
        args = AttrDict(json.load(f))
    np.set_printoptions(precision=3)

    # initialize agent and environment
    env, attacker_model, victim_model = train(args, args.algo_kwargs)

    # load victim_model parameters
    para = joblib.load(os.path.join(
        log_dir,
        "run_0",
        filename
    ))
    victim_model.load(os.path.join(
        log_dir,
        "run_0",
        filename
    ))
    # expert_pi_logstd = para['expert/ppo2_model/pi/logstd:0']
    # expert_pi_std = np.exp(expert_pi_logstd)

    attacker_env = env.as_attacker_env(
        victim_action_tensor= victim_model.act_model.action,
        victim_obs_ph= victim_model.act_model.X,
    )
    victim_env = env.as_victim_env(
        attacker_action_tensor= attacker_model.act_model.action,
        attacker_obs_ph= attacker_model.act_model.X,
    )

    env = env.env

    ave_episode_return = 0
    ave_timestep_reward = 0
    n_timesteps = 0
    for itr in range(100):
        done = False
        obs = env.reset()
        return_ = 0
        while not done:
            act = victim_model.step(obs)
            obs, reward, done, _ = env.step(act[0])
            n_timesteps += 1
            return_ += reward
            ave_timestep_reward += reward
        ave_episode_return += return_
    ave_timestep_reward = ave_timestep_reward / n_timesteps
    ave_episode_return = ave_episode_return / 100
    print("""Testing result without attack:
average return each episode: {}
average reward each timestep: {}
""".format(ave_episode_return, ave_timestep_reward))

if __name__ == '__main__':
    if False:
        # configuration for remote attach and debug
        import ptvsd
        import sys
        ip_address = ('0.0.0.0', 6789)
        print("Process: " + " ".join(sys.argv[:]))
        print("Is waiting for attach at address: %s:%d" % ip_address, flush= True)
        # Allow other computers to attach to ptvsd at this IP address and port.
        ptvsd.enable_attach(address=ip_address, redirect_output= True)
        # Pause the program until a remote debugger is attached
        ptvsd.wait_for_attach()
        print("Process attached, start running into experiment...", flush= True)
        ptvsd.break_into_debugger()

    main(
        "./data/slurm/rarl_experiment/20210216/Ant-v2/a1.00e-02",
        "snapshot-7"
    )
