import csv
import requests
import json
from pprint import pprint
from decouple import config

movies = []
result = []
api_key = config('API_KEY')

# Discover
# 최신 영화 60개를 result에 담기

for i in range(3):
# for i in range(1):

    page = i+1
    base_url = 'https://api.themoviedb.org/3/discover/movie?'
    url = base_url + f'api_key={api_key}&language=ko-KR&sort_by=popularity.desc&include_adult=false&include_video=false&page={page}'

    response = requests.get(url)
    response_dict = response.json()

    # len(response_dict['results']) => 20개
    for i in range(len(response_dict['results'])):
        movieId = response_dict['results'][i]['id']
        movies.append(movieId)

for i in range(len(movies)):
# for i in range(1):
    movie_id = movies[i]

    # model : Movie
    movie_tmp = {}
    movie_tmp['model'] = 'movies.movie'
    movie_tmp['pk'] = movies[i]
    base_url_movies = 'https://api.themoviedb.org/3/movie/'

    ## detail
    url_detail = base_url_movies + f'{movie_id}?api_key={api_key}&language=ko-KR'

    response = requests.get(url_detail)
    response_dict = response.json()

    title = response_dict["title"]
    original_title = response_dict["original_title"]
    summary = response_dict["overview"]
    poster_url = response_dict["poster_path"]
    backdrop_url = response_dict["backdrop_path"]
    come_out = response_dict["release_date"]
    audience = response_dict["popularity"] * 1000

    genres = []
    tmps = response_dict["genres"]
    for tmp in tmps:
        genres.append(tmp["id"])

    ## credits
    url_credits = base_url_movies + f'{movie_id}/credits?api_key={api_key}'

    response = requests.get(url_credits)
    response_dict = response.json()

    tmps = response_dict["crew"]
    director = ''

    for tmp in tmps:
        if tmp["job"] == "Director":
            director = tmp["name"]
            break

    ## videos
    base_url_video = 'https://api.themoviedb.org/3/movie/'
    url_video = base_url_video + f'{movie_id}/videos?api_key={api_key}&language=ko-KR'

    response = requests.get(url_video)
    response_dict = response.json()

    if response_dict["results"]:
        teaser = response_dict["results"][0]["key"]
    else:
        teaser = ""

    # ## actor
    # url_actor = f'http://api.themoviedb.org/3/movie/{movie_id}/casts?api_key={api_key}'

    # response = requests.get(url_actor)
    # response_dict = response.json
    # tmp_casts = response_dict["cast"]
    # cnt = 0
    # casts = []
    # # pprint(response_dict['cast'])
    # for cast in tmp_casts:
    #     if cnt == 5:
    #         break
    #     name = cast["name"]
    #     cnt += 1

    movie_tmp['fields'] = {
        'title': title,
        'original_title': original_title,
        'summary': summary,
        'poster_url': poster_url,
        'backdrop_url': backdrop_url,
        'teaser': teaser,
        'come_out': come_out,
        'audience': audience,
        'director': director,
        'genres': genres,
    }
    result.append(movie_tmp)

with open('movies.json', 'w', encoding='utf-8') as f:
    json.dump(result, f, ensure_ascii=False, indent=4)


# http://edwardlib.org/tutorials/supervised-regression

import ipdb
import numpy as np
import edward as ed
import tensorflow as tf
import matplotlib.pyplot as plt

from edward.models import Normal

def build_toy_dataset(N, w, noise_std=0.1):
  D = len(w)
  x = np.random.randn(N, D)
  y = np.dot(x, w) + np.random.normal(0, noise_std, size=N)
  return x, y

N = 40
D = 10
w_true = np.random.randn(D)
X_train, y_train = build_toy_dataset(N, w_true)
X_test, y_test = build_toy_dataset(N, w_true)

# Bayesian regression:
# p(w) ~ N(0, sigma_w^2 * I)
# p(b) ~ N(0, sigma_b^2)
# p(y|x,w,b) ~ N(x'w + b, sigma_y^2)
# p(Y|x,w,b) ~ Prod[ N(x'w + b, sigma_y^2) ]

# Build the compute graphs matching our Bayesian regression model:
X = tf.placeholder(tf.float32, [N, D])
w = Normal(loc=tf.zeros(D), scale=tf.ones(D))
b = Normal(loc=tf.zeros(1), scale=tf.ones(1))
y = Normal(loc=ed.dot(X, w), scale=tf.ones(N))

# Variational approximations - fully factorized w and b graphs:
# This doesn't really count as an approximation, but whatever.
qw = Normal(loc=tf.Variable(tf.random_normal([D])),
            scale=tf.nn.softplus(tf.Variable(tf.random_normal([D]))))
qb = Normal(loc=tf.Variable(tf.random_normal([1])),
            scale=tf.nn.softplus(tf.Variable(tf.random_normal([1]))))

# Run Kullback-Leibler divergence inference
# Minimize KL[q(w) || p(w|X)] where
#   q(w) ~ Normal
#   p(w|X) = p(X|w)p(w) / p(X)
inference = ed.KLqp({w: qw, b: qb}, data={X: X_train, y: y_train})
inference.run(n_samples=5, n_iter=250)

# "Criticism" - edward generates a new graph for y_post, swapping out
# w and qw whenever they appear in the graph
# y_post = Normal(loc=ed.dot(X, qw) + qb, scale=tf.ones(N))
y_post = ed.copy(y, {w: qw, b: qb})

# Some stored edward loss functions
print("Mean squared error on test data:")
print(ed.evaluate('mean_squared_error', data={X: X_test, y_post: y_test}))

print("Mean absolute error on test data:")
print(ed.evaluate('mean_absolute_error', data={X: X_test, y_post: y_test}))

def visualise(X_data, y_data, w, b, n_samples=10):
  w_samples = w.sample(n_samples)[0].eval()
  b_samples = b.sample(n_samples).eval()
  plt.scatter(X_data[:, 0], y_data)
  inputs = np.linspace(-8, 8, num=400)
  for ns in range(n_samples):
    output = inputs * w_samples[ns] + b_samples[ns]
    plt.plot(inputs, output)

plt.figure()
visualise(X_train, y_train, w, b)
plt.figure()
visualise(X_train, y_train, qw, qb)
plt.show()


"""Module for watch manipulation.

The watch has only hour and minute handle,
this module manipulates the GPIO pins which then
manipulate relays that turn on and off
electric motors on which are handles.
There are two GPIO pins, each for one handle,
according to that there are two electric motors.
"""

import datetime
import multiprocessing
from time import perf_counter

import RPi.GPIO as GPIO

from utils import delay

# Use Raspberry Pi 4B board pin numbers.
GPIO.setmode(GPIO.BCM)

# Ignore GPIO warnings.
GPIO.setwarnings(False)

# Set GPIO pins for watch handles.
MINUTE_HANDLE = 23
HOUR_HANDLE = 24

# set up GPIO pin modes
for pin in [MINUTE_HANDLE, HOUR_HANDLE]:
    GPIO.setup(pin, GPIO.OUT)

# Declare how long to move each handle
# and when to start moving handles.
MINUTE_HANDLE_DELAY = 2.99034  # [s]
HOUR_HANDLE_DELAY = 2.99034  # [s]

MINUTE_HANDLE_START = 57  # [s]
HOUR_HANDLE_START = 2  # [s]

ONE_SECOND = 1

def log_minute_handle_move() -> None:
    """
    Log that minute handle has moved.
    """
    with open("handles_movement_log.txt", "w") as txt_file:
        txt_file.write("minute_handle")

def log_hour_handle_move() -> None:
    """
    Log that hour handle has moved.
    """
    with open("handles_movement_log.txt", "w") as txt_file:
        txt_file.write("hour_handle")

def move_minute_handle() -> None:
    """
    This function sets GPIO pin to HIGH which turns on electric motor.
    Minute handle moves as long as the GPIO pin is in HIGH state.
    """
    print("-=--=- start moving: Minute handle  -=--=-")
    GPIO.output(MINUTE_HANDLE, GPIO.HIGH)
    delay(seconds=MINUTE_HANDLE_DELAY)
    GPIO.output(MINUTE_HANDLE, GPIO.LOW)
    print("-=--=- stop moving: Minute handle  -=--=-")
    log_minute_handle_move()

def move_hour_handle() -> None:
    """
    This function sets GPIO pin to HIGH which turns on electric motor.
    Hour handle moves as long as the GPIO pin is in HIGH state.
    """
    print("-=- start moving: Hour handle -=-")
    GPIO.output(HOUR_HANDLE, GPIO.HIGH)
    delay(HOUR_HANDLE_DELAY)
    GPIO.output(HOUR_HANDLE, GPIO.LOW)
    print("-=- stop moving: Hour handle  -=-")
    log_hour_handle_move()

def log_last_watch_time(date_time: datetime.datetime) -> None:
    """
    Log recieved time to text file.
    """
    with open("watch_time.txt", "w") as txt_file:
        txt_file.write(f"{date_time.strftime('%H:%M:%S')}")

def get_last_watch_time() -> datetime.datetime:
    """
    Get last watch time from text file.
    """
    with open("watch_time.txt", "r") as txt_file:
        last_watch_time = (
            datetime.datetime.strptime(txt_file.readline(), "%H:%M:%S")
            .time()
            .replace(second=0)
        )
    return last_watch_time

def handles_synchronized() -> bool:
    """
    This function checks the last movement of handles and returns True
    if handles are synchronized and False if not.

    Handle are not synchronized if some problem occurred that caused
    the program to stop after moving the minute handle and
    before moving hour handle.
    In this case the hour handle did not move and now must move
    so the handles stay synchronized.
    """
    try:
        with open("handles_movement_log.txt", "r") as txt_file:
            last_movement = txt_file.readline()
        if last_movement == "hour_handle":
            return True
        elif last_movement == "minute_handle":
            return False
        else:
            return True
    except FileNotFoundError:
        return True

def minute_handle_last_moved() -> bool:
    """
    This function checks if minute handle last moved.
    """
    try:
        with open("handles_movement_log.txt", "r") as txt_file:
            last_movement = txt_file.readline()
        if last_movement == "minute_handle":
            return True
        else:
            return False
    except FileNotFoundError:
        return True

def watch_setup(
    minutes: int,
    last_watch_datetime: datetime.datetime,
    watch_setup_queue: multiprocessing.Queue,
) -> None:
    """
    This function is called when there is difference between current and
    watch time, this function calls moving hour and minute handles as many
    times as the watch is late (in minutes).
    """
    iterations = 0

    # First check if handles moved identically in last handle movement.
    if not handles_synchronized():
        # If they not, move hour handle to synchronize them.
        print("Hour handle has not moved last time!")
        print(f"      # 0. watch setup iteration (only hour handle moves).")
        move_hour_handle()
        log_last_watch_time(
            date_time=last_watch_datetime.replace(second=0)
            + datetime.timedelta(minutes=1)
        )
        minutes = minutes - 1
        if minutes == 0:
            print("   ** Watch setup is done. **")
            watch_setup_queue.put("Watch setup is done!")
            return

    # If difference is more than 12 hours, substract 12 hours.
    if minutes > 720:  # 12 * 60 minutes = 720 minutes
        iterations = minutes - 720
    else:
        iterations = minutes

    print(f"--- Required {iterations} iterations. ---")

    for iteration in range(iterations):
        print(f"      # {iteration + 1}. watch setup iteration.")

        # Move minute handle.
        minute_handle_process = multiprocessing.Process(target=move_minute_handle)
        minute_handle_process.start()
        minute_handle_process.join()

        # Move hour handle.
        hour_handle_process = multiprocessing.Process(target=move_hour_handle)
        hour_handle_process.start()
        hour_handle_process.join()

        # Log new watch time.
        log_last_watch_time(
            date_time=last_watch_datetime.replace(second=0)
            + datetime.timedelta(minutes=iteration + 1)
        )

    print("   ** Watch setup is done. **")
    watch_setup_queue.put("Watch setup is done!")

def wait_until_next_second(counter_from_which_it_is_waiting: float) -> None:
    """
    Wait one second from retrieved perf counter state.
    """
    execution_time = perf_counter() - counter_from_which_it_is_waiting
    if execution_time < ONE_SECOND:
        while perf_counter() < counter_from_which_it_is_waiting + ONE_SECOND:
            pass

def watch(
    current_datetime_for_display: multiprocessing.Queue,
    current_datetime_for_bells: multiprocessing.Queue,
    current_datetime_for_buttons: multiprocessing.Queue,
    message_queue_for_display: multiprocessing.Queue,
    message_queue_in: multiprocessing.Queue,
    message_queue_for_manual_watch_setup: multiprocessing.Queue,
) -> None:
    """This function is main watch function.

    It sends tick to other processes every second and
    it starts moving handles when it is time for it.
    Also it checks if watch time is correct, if not
    calls watch setup.
    """
    # Flag which indicates that watch is current in setup mode.
    watch_is_setting = False
    # Queue for communication with watch setup process.
    watch_setup_queue = multiprocessing.Queue()
    # Flag which indicates that app is on startup.
    app_on_startup = True
    # Flag which indicates that minute handle has moved in previous iteration.
    minute_handle_moved = True
    """
    When watch setup ends after minute handle movement and before
    hour handle movement, hour handle moves but minute handle did not move,
    this flag is used to track this.
    """

    while True:
        # Get current perf counter state, date and time.
        loop_start_time = perf_counter()
        current_datetime = datetime.datetime.now()

        # Send tick to other processes.
        current_datetime_message = current_datetime.strftime("%d/%m/%y %H:%M:%S")
        current_datetime_for_display.put(current_datetime_message)
        current_datetime_for_bells.put(current_datetime_message)
        current_datetime_for_buttons.put(current_datetime_message)

        if not message_queue_in.empty():
            recieved_message = message_queue_in.get()
            if recieved_message == "Manual watch setup started.":
                watch_is_setting = True
            elif recieved_message == "Manual watch setup done.":
                watch_is_setting = False

        if watch_is_setting:
            if watch_setup_queue.empty():
                # Wait and go to next loop iteration if watch is in setup mode.
                wait_until_next_second(counter_from_which_it_is_waiting=loop_start_time)
                continue
            else:
                recieved_message = watch_setup_queue.get()
                if recieved_message == "Watch setup is done!":
                    # Continue with normal work, go further in this loop.
                    watch_is_setting = False
                    app_on_startup = False
                    message_queue_for_display.put("Watch setup is done!")
                    message_queue_for_manual_watch_setup.put("Watch setup is done!")

        # Get last watch time.
        last_watch_time = get_last_watch_time()

        # Convert it to datetime so that current datetime and last watch time can be compared.
        last_watch_time = datetime.datetime.combine(
            current_datetime.date(), last_watch_time
        )

        # Calculate the time difference.
        if current_datetime < last_watch_time:
            # If current_datetime is earlier in the day, assume that last_watch_time is from yesterday.
            last_watch_time = datetime.datetime.combine(
                current_datetime.date() - datetime.timedelta(days=1),
                last_watch_time.time(),
            )
        time_delta = current_datetime - last_watch_time

        # Convert time difference to minutes.
        time_delta = int(time_delta.total_seconds() / 60)

        if app_on_startup and time_delta > 0:
            watch_is_setting = True
            multiprocessing.Process(
                target=watch_setup,
                args=(time_delta, last_watch_time, watch_setup_queue),
            ).start()
            message_queue_for_display.put("Watch setup started!")
            message_queue_for_manual_watch_setup.put("Watch setup started!")
            wait_until_next_second(counter_from_which_it_is_waiting=loop_start_time)
            continue

        # If there is time delta go to watch setup mode,
        # ignore time delta if watch handles are moving right now in normal mode.
        if (
            time_delta > 0
            and current_datetime.second > HOUR_HANDLE_START + HOUR_HANDLE_DELAY + 1
            and current_datetime.second < MINUTE_HANDLE_START - 1
        ):
            print(f"Current watch time is: {last_watch_time.strftime('%H:%M:%S')}")
            print(f"Current time is: {current_datetime.strftime('%H:%M:%S')}")
            print(
                f"Difference between curret time and watch time is {time_delta} minutes!"
            )
            print("   ** Turning to watch setup mode. **")
            watch_is_setting = True
            multiprocessing.Process(
                target=watch_setup,
                args=(time_delta, last_watch_time, watch_setup_queue),
            ).start()
            message_queue_for_display.put("Watch setup started!")
            message_queue_for_manual_watch_setup.put("Watch setup started!")
            wait_until_next_second(counter_from_which_it_is_waiting=loop_start_time)
            continue

        # Check if is it time for minute handle moving, if it is, move minute handle.
        if int(current_datetime.strftime("%S")) == MINUTE_HANDLE_START:
            minute_handle_process = multiprocessing.Process(target=move_minute_handle)
            minute_handle_process.start()

        # Check if is it time for hour handle moving, if it is, move hour handle.
        if int(current_datetime.strftime("%S")) == HOUR_HANDLE_START:
            if not minute_handle_last_moved():
                minute_handle_moved = False
            hour_handle_process = multiprocessing.Process(target=move_hour_handle)
            hour_handle_process.start()
            log_last_watch_time(
                date_time=last_watch_time.replace(second=0)
                + datetime.timedelta(minutes=1)
            )

        if (
            not minute_handle_moved
            and int(current_datetime.strftime("%S"))
            > HOUR_HANDLE_START + HOUR_HANDLE_DELAY + 1
            and int(current_datetime.strftime("%S"))
            < MINUTE_HANDLE_START - MINUTE_HANDLE_DELAY - 1
        ):
            # If hour handle moved but minute handle did not move, move minute handle
            # after the hour handle has has moved.
            minute_handle_process = multiprocessing.Process(target=move_minute_handle)
            minute_handle_process.start()
            minute_handle_moved = True
            # Override minute handle move log with hour handle move log so the algorithm
            # won't assume that handles are not synchronized.
            log_hour_handle_move()

        wait_until_next_second(counter_from_which_it_is_waiting=loop_start_time)


from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import collections
from absl import logging

import enum
import numpy as np
import six
from pysc2.lib import actions
from pysc2.lib import colors
from pysc2.lib import named_array
from pysc2.lib import point
from pysc2.lib import static_data
from pysc2.lib import stopwatch
from pysc2.lib import transform

from s2clientprotocol import raw_pb2 as sc_raw
from pysc2.lib import unit_controls
from s2clientprotocol import sc2api_pb2 as sc_pb

sw = stopwatch.sw

class FeatureType(enum.Enum):
  SCALAR = 1
  CATEGORICAL = 2

class PlayerRelative(enum.IntEnum):
  """The values for the `player_relative` feature layers."""
  NONE = 0
  SELF = 1
  ALLY = 2
  NEUTRAL = 3
  ENEMY = 4

class Visibility(enum.IntEnum):
  """Values for the `visibility` feature layers."""
  HIDDEN = 0
  SEEN = 1
  VISIBLE = 2

class Effects(enum.IntEnum):
  """Values for the `effects` feature layer."""
  # pylint: disable=invalid-name
  PsiStorm = 1
  GuardianShield = 2
  TemporalFieldGrowing = 3
  TemporalField = 4
  ThermalLance = 5
  ScannerSweep = 6
  NukeDot = 7
  LiberatorDefenderZoneSetup = 8
  LiberatorDefenderZone = 9
  BlindingCloud = 10
  CorrosiveBile = 11
  LurkerSpines = 12
  # pylint: enable=invalid-name

class ScoreCumulative(enum.IntEnum):
  """Indices into the `score_cumulative` observation."""
  score = 0
  idle_production_time = 1
  idle_worker_time = 2
  total_value_units = 3
  total_value_structures = 4
  killed_value_units = 5
  killed_value_structures = 6
  collected_minerals = 7
  collected_vespene = 8
  collection_rate_minerals = 9
  collection_rate_vespene = 10
  spent_minerals = 11
  spent_vespene = 12

class Player(enum.IntEnum):
  """Indices into the `player` observation."""
  player_id = 0
  minerals = 1
  vespene = 2
  food_used = 3
  food_cap = 4
  food_army = 5
  food_workers = 6
  idle_worker_count = 7
  army_count = 8
  warp_gate_count = 9
  larva_count = 10

class UnitLayer(enum.IntEnum):
  """Indices into the unit layers in the observations."""
  unit_type = 0
  player_relative = 1
  health = 2
  shields = 3
  energy = 4
  transport_slots_taken = 5
  build_progress = 6

class FeatureUnit(enum.IntEnum):
  """Indices for the `feature_unit` observations."""
  unit_type = 0
  alliance = 1
  health = 2
  shield = 3
  energy = 4
  cargo_space_taken = 5
  build_progress = 6
  health_ratio = 7
  shield_ratio = 8
  energy_ratio = 9
  display_type = 10
  owner = 11
  x = 12
  y = 13
  facing = 14
  radius = 15
  cloak = 16
  is_selected = 17
  is_blip = 18
  is_powered = 19
  mineral_contents = 20
  vespene_contents = 21
  cargo_space_max = 22
  assigned_harvesters = 23
  ideal_harvesters = 24
  weapon_cooldown = 25

class Feature(collections.namedtuple(
    "Feature", ["index", "name", "layer_set", "full_name", "scale", "type",
                "palette", "clip"])):
  """Define properties of a feature layer.

  Attributes:
    index: Index of this layer into the set of layers.
    name: The name of the layer within the set.
    layer_set: Which set of feature layers to look at in the observation proto.
    full_name: The full name including for visualization.
    scale: Max value (+1) of this layer, used to scale the values.
    type: A FeatureType for scalar vs categorical.
    palette: A color palette for rendering.
    clip: Whether to clip the values for coloring.
  """
  __slots__ = ()

  dtypes = {
      1: np.uint8,
      8: np.uint8,
      16: np.uint16,
      32: np.int32,
  }

  def unpack(self, obs):
    """Return a correctly shaped numpy array for this feature."""
    planes = getattr(obs.feature_layer_data, self.layer_set)
    plane = getattr(planes, self.name)
    return self.unpack_layer(plane)

  @staticmethod
  @sw.decorate
  def unpack_layer(plane):
    """Return a correctly shaped numpy array given the feature layer bytes."""
    size = point.Point.build(plane.size)
    if size == (0, 0):
      # New layer that isn't implemented in this SC2 version.
      return None
    data = np.frombuffer(plane.data, dtype=Feature.dtypes[plane.bits_per_pixel])
    if plane.bits_per_pixel == 1:
      data = np.unpackbits(data)
      if data.shape[0] != size.x * size.y:
        # This could happen if the correct length isn't a multiple of 8, leading
        # to some padding bits at the end of the string which are incorrectly
        # interpreted as data.
        data = data[:size.x * size.y]
    return data.reshape(size.y, size.x)

  @staticmethod
  @sw.decorate
  def unpack_rgb_image(plane):
    """Return a correctly shaped numpy array given the image bytes."""
    assert plane.bits_per_pixel == 24, "{} != 24".format(plane.bits_per_pixel)
    size = point.Point.build(plane.size)
    data = np.frombuffer(plane.data, dtype=np.uint8)
    return data.reshape(size.y, size.x, 3)

  @sw.decorate
  def color(self, plane):
    if self.clip:
      plane = np.clip(plane, 0, self.scale - 1)
    return self.palette[plane]

class ScreenFeatures(collections.namedtuple("ScreenFeatures", [
    "height_map", "visibility_map", "creep", "power", "player_id",
    "player_relative", "unit_type", "selected", "unit_hit_points",
    "unit_hit_points_ratio", "unit_energy", "unit_energy_ratio", "unit_shields",
    "unit_shields_ratio", "unit_density", "unit_density_aa", "effects"])):
  """The set of screen feature layers."""
  __slots__ = ()

  def __new__(cls, **kwargs):
    feats = {}
    for name, (scale, type_, palette, clip) in six.iteritems(kwargs):
      feats[name] = Feature(
          index=ScreenFeatures._fields.index(name),
          name=name,
          layer_set="renders",
          full_name="screen " + name,
          scale=scale,
          type=type_,
          palette=palette(scale) if callable(palette) else palette,
          clip=clip)
    return super(ScreenFeatures, cls).__new__(cls, **feats)  # pytype: disable=missing-parameter

class MinimapFeatures(collections.namedtuple("MinimapFeatures", [
    "height_map", "visibility_map", "creep", "camera", "player_id",
    "player_relative", "selected", "unit_type", "alerts", "pathable",
    "buildable"])):
  """The set of minimap feature layers."""
  __slots__ = ()

  def __new__(cls, **kwargs):
    feats = {}
    for name, (scale, type_, palette) in six.iteritems(kwargs):
      feats[name] = Feature(
          index=MinimapFeatures._fields.index(name),
          name=name,
          layer_set="minimap_renders",
          full_name="minimap " + name,
          scale=scale,
          type=type_,
          palette=palette(scale) if callable(palette) else palette,
          clip=False)
    return super(MinimapFeatures, cls).__new__(cls, **feats)  # pytype: disable=missing-parameter

SCREEN_FEATURES = ScreenFeatures(
    height_map=(256, FeatureType.SCALAR, colors.winter, False),
    visibility_map=(4, FeatureType.CATEGORICAL,
                    colors.VISIBILITY_PALETTE, False),
    creep=(2, FeatureType.CATEGORICAL, colors.CREEP_PALETTE, False),
    power=(2, FeatureType.CATEGORICAL, colors.POWER_PALETTE, False),
    player_id=(17, FeatureType.CATEGORICAL,
               colors.PLAYER_ABSOLUTE_PALETTE, False),
    player_relative=(5, FeatureType.CATEGORICAL,
                     colors.PLAYER_RELATIVE_PALETTE, False),
    unit_type=(max(static_data.UNIT_TYPES) + 1, FeatureType.CATEGORICAL,
               colors.unit_type, False),
    selected=(2, FeatureType.CATEGORICAL, colors.SELECTED_PALETTE, False),
    unit_hit_points=(1600, FeatureType.SCALAR, colors.hot, True),
    unit_hit_points_ratio=(256, FeatureType.SCALAR, colors.hot, False),
    unit_energy=(1000, FeatureType.SCALAR, colors.hot, True),
    unit_energy_ratio=(256, FeatureType.SCALAR, colors.hot, False),
    unit_shields=(1000, FeatureType.SCALAR, colors.hot, True),
    unit_shields_ratio=(256, FeatureType.SCALAR, colors.hot, False),
    unit_density=(16, FeatureType.SCALAR, colors.hot, True),
    unit_density_aa=(256, FeatureType.SCALAR, colors.hot, False),
    effects=(16, FeatureType.CATEGORICAL, colors.effects, False),
)

MINIMAP_FEATURES = MinimapFeatures(
    height_map=(256, FeatureType.SCALAR, colors.winter),
    visibility_map=(4, FeatureType.CATEGORICAL, colors.VISIBILITY_PALETTE),
    creep=(2, FeatureType.CATEGORICAL, colors.CREEP_PALETTE),
    camera=(2, FeatureType.CATEGORICAL, colors.CAMERA_PALETTE),
    player_id=(17, FeatureType.CATEGORICAL, colors.PLAYER_ABSOLUTE_PALETTE),
    player_relative=(5, FeatureType.CATEGORICAL,
                     colors.PLAYER_RELATIVE_PALETTE),
    selected=(2, FeatureType.CATEGORICAL, colors.winter),
    unit_type=(max(static_data.UNIT_TYPES) + 1, FeatureType.CATEGORICAL,
               colors.unit_type),
    alerts=(2, FeatureType.CATEGORICAL, colors.winter),
    pathable=(2, FeatureType.CATEGORICAL, colors.winter),
    buildable=(2, FeatureType.CATEGORICAL, colors.winter),
)

def _to_point(dims):
  """Convert (width, height) or size -> point.Point."""
  assert dims

  if isinstance(dims, (tuple, list)):
    if len(dims) != 2:
      raise ValueError(
          "A two element tuple or list is expected here, got {}.".format(dims))
    else:
      width = int(dims[0])
      height = int(dims[1])
      if width <= 0 or height <= 0:
        raise ValueError("Must specify +ve dims, got {}.".format(dims))
      else:
        return point.Point(width, height)
  else:
    size = int(dims)
    if size <= 0:
      raise ValueError(
          "Must specify a +ve value for size, got {}.".format(dims))
    else:
      return point.Point(size, size)

class Dimensions(object):
  """Screen and minimap dimensions configuration.

  Both screen and minimap must be specified. Sizes must be positive.
  Screen size must be greater than or equal to minimap size in both dimensions.

  Args:
    screen: A (width, height) int tuple or a single int to be used for both.
    minimap: A (width, height) int tuple or a single int to be used for both.
  """

  def __init__(self, screen=None, minimap=None):
    if not screen or not minimap:
      raise ValueError(
          "screen and minimap must both be set, screen={}, minimap={}".format(
              screen, minimap))

    self._screen = _to_point(screen)
    self._minimap = _to_point(minimap)

    if self._screen.x < self._minimap.x or self._screen.y < self._minimap.y:
      raise ValueError(
          "Screen (%s) can't be smaller than the minimap (%s)." % (
              self._screen, self._minimap))

  @property
  def screen(self):
    return self._screen

  @property
  def minimap(self):
    return self._minimap

  def __repr__(self):
    return "Dimensions(screen={}, minimap={})".format(self.screen, self.minimap)

class AgentInterfaceFormat(object):
  """Observation and action interface format specific to a particular agent."""

  def __init__(
      self,
      feature_dimensions=None,
      rgb_dimensions=None,
      action_space=None,
      camera_width_world_units=None,
      use_feature_units=False,
      hide_specific_actions=True):
    """Initializer.

    Args:
      feature_dimensions: Feature layer `Dimension`s. Either this or
          rgb_dimensions (or both) must be set.
      rgb_dimensions: RGB `Dimension`. Either this or feature_dimensions
          (or both) must be set.
      action_space: If you pass both feature and rgb sizes, then you must also
          specify which you want to use for your actions as an ActionSpace enum.
      camera_width_world_units: The width of your screen in world units. If your
          feature_dimensions.screen=(64, 48) and camera_width is 24, then each
          px represents 24 / 64 = 0.375 world units in each of x and y.
          It'll then represent a camera of size (24, 0.375 * 48) = (24, 18)
          world units.
      use_feature_units: Whether to include feature unit data in observations.
      hide_specific_actions: [bool] Some actions (eg cancel) have many
          specific versions (cancel this building, cancel that spell) and can
          be represented in a more general form. If a specific action is
          available, the general will also be available. If you set
          `hide_specific_actions` to False, the specific versions will also be
          available, but if it's True, the specific ones will be hidden.
          Similarly, when transforming back, a specific action will be returned
          as the general action. This simplifies the action space, though can
          lead to some actions in replays not being exactly representable using
          only the general actions.

    Raises:
      ValueError: if the parameters are inconsistent.
    """

    if not feature_dimensions and not rgb_dimensions:
      raise ValueError("Must set either the feature layer or rgb dimensions.")

    if action_space:
      if not isinstance(action_space, actions.ActionSpace):
        raise ValueError("action_space must be of type ActionSpace.")

      if ((action_space == actions.ActionSpace.FEATURES and
           not feature_dimensions) or
          (action_space == actions.ActionSpace.RGB and
           not rgb_dimensions)):
        raise ValueError(
            "Action space must match the observations, action space={}, "
            "feature_dimensions={}, rgb_dimensions={}".format(
                action_space, feature_dimensions, rgb_dimensions))
    else:
      if feature_dimensions and rgb_dimensions:
        raise ValueError(
            "You must specify the action space if you have both screen and "
            "rgb observations.")
      elif feature_dimensions:
        action_space = actions.ActionSpace.FEATURES
      else:
        action_space = actions.ActionSpace.RGB

    self._feature_dimensions = feature_dimensions
    self._rgb_dimensions = rgb_dimensions
    self._action_space = action_space
    self._camera_width_world_units = camera_width_world_units or 24
    self._use_feature_units = use_feature_units
    self._hide_specific_actions = hide_specific_actions

    if action_space == actions.ActionSpace.FEATURES:
      self._action_dimensions = feature_dimensions
    else:
      self._action_dimensions = rgb_dimensions

  @property
  def feature_dimensions(self):
    return self._feature_dimensions

  @property
  def rgb_dimensions(self):
    return self._rgb_dimensions

  @property
  def action_space(self):
    return self._action_space

  @property
  def camera_width_world_units(self):
    return self._camera_width_world_units

  @property
  def use_feature_units(self):
    return self._use_feature_units

  @property
  def hide_specific_actions(self):
    return self._hide_specific_actions

  @property
  def action_dimensions(self):
    return self._action_dimensions

def parse_agent_interface_format(
    feature_screen=None,
    feature_minimap=None,
    rgb_screen=None,
    rgb_minimap=None,
    action_space=None,
    camera_width_world_units=None,
    use_feature_units=False):
  """Creates an AgentInterfaceFormat object from keyword args.

  Convenient when using dictionaries or command-line arguments for config.

  Note that the feature_* and rgb_* properties define the respective spatial
  observation dimensions and accept:
      * None or 0 to disable that spatial observation.
      * A single int for a square observation with that side length.
      * A (int, int) tuple for a rectangular (width, height) observation.

  Args:
    feature_screen: If specified, so must feature_minimap be.
    feature_minimap: If specified, so must feature_screen be.
    rgb_screen: If specified, so must rgb_minimap be.
    rgb_minimap: If specified, so must rgb_screen be.
    action_space: ["FEATURES", "RGB"].
    camera_width_world_units: An int.
    use_feature_units: A boolean, defaults to False.

  Returns:
    An `AgentInterfaceFormat` object.

  Raises:
    ValueError: If an invalid parameter is specified.
  """
  if feature_screen or feature_minimap:
    feature_dimensions = Dimensions(
        screen=feature_screen,
        minimap=feature_minimap)
  else:
    feature_dimensions = None

  if rgb_screen or rgb_minimap:
    rgb_dimensions = Dimensions(
        screen=rgb_screen,
        minimap=rgb_minimap)
  else:
    rgb_dimensions = None

  return AgentInterfaceFormat(
      feature_dimensions=feature_dimensions,
      rgb_dimensions=rgb_dimensions,
      action_space=(action_space and actions.ActionSpace[action_space.upper()]),
      camera_width_world_units=camera_width_world_units,
      use_feature_units=use_feature_units
  )

def features_from_game_info(
    game_info,
    use_feature_units=False,
    action_space=None,
    hide_specific_actions=True):
  """Construct a Features object using data extracted from game info.

  Args:
    game_info: A `sc_pb.ResponseGameInfo` from the game.
    use_feature_units: Whether to include the feature unit observation.
    action_space: If you pass both feature and rgb sizes, then you must also
        specify which you want to use for your actions as an ActionSpace enum.
    hide_specific_actions: [bool] Some actions (eg cancel) have many
        specific versions (cancel this building, cancel that spell) and can
        be represented in a more general form. If a specific action is
        available, the general will also be available. If you set
        `hide_specific_actions` to False, the specific versions will also be
        available, but if it's True, the specific ones will be hidden.
        Similarly, when transforming back, a specific action will be returned
        as the general action. This simplifies the action space, though can
        lead to some actions in replays not being exactly representable using
        only the general actions.

  Returns:
    A features object matching the specified parameterisation.

  """

  if game_info.options.HasField("feature_layer"):
    fl_opts = game_info.options.feature_layer
    feature_dimensions = Dimensions(
        screen=(fl_opts.resolution.x, fl_opts.resolution.y),
        minimap=(fl_opts.minimap_resolution.x, fl_opts.minimap_resolution.y))
  else:
    feature_dimensions = None

  if game_info.options.HasField("render"):
    rgb_opts = game_info.options.render
    rgb_dimensions = Dimensions(
        screen=(rgb_opts.resolution.x, rgb_opts.resolution.y),
        minimap=(rgb_opts.minimap_resolution.x, rgb_opts.minimap_resolution.y))
  else:
    rgb_dimensions = None

  map_size = game_info.start_raw.map_size
  camera_width_world_units = game_info.options.feature_layer.width

  return Features(
      agent_interface_format=AgentInterfaceFormat(
          feature_dimensions=feature_dimensions,
          rgb_dimensions=rgb_dimensions,
          use_feature_units=use_feature_units,
          camera_width_world_units=camera_width_world_units,
          action_space=action_space,
          hide_specific_actions=hide_specific_actions),
      map_size=map_size)

def _init_valid_functions(action_dimensions):
  """Initialize ValidFunctions and set up the callbacks."""
  sizes = {
      "screen": tuple(int(i) for i in action_dimensions.screen),
      "screen2": tuple(int(i) for i in action_dimensions.screen),
      "minimap": tuple(int(i) for i in action_dimensions.minimap),
  }

  types = actions.Arguments(*[
      actions.ArgumentType.spec(t.id, t.name, sizes.get(t.name, t.sizes))
      for t in actions.TYPES])

  functions = actions.Functions([
      actions.Function.spec(f.id, f.name, tuple(types[t.id] for t in f.args))
      for f in actions.FUNCTIONS])

  return actions.ValidActions(types, functions)

class Features(object):
  """Render feature layers from SC2 Observation protos into numpy arrays.

  This has the implementation details of how to render a starcraft environment.
  It translates between agent action/observation formats and starcraft
  action/observation formats, which should not be seen by agent authors. The
  starcraft protos contain more information than they should have access to.

  This is outside of the environment so that it can also be used in other
  contexts, eg a supervised dataset pipeline.
  """

  def __init__(self, agent_interface_format=None, map_size=None):
    """Initialize a Features instance matching the specified interface format.

    Args:
      agent_interface_format: See the documentation for `AgentInterfaceFormat`.
      map_size: The size of the map in world units, needed for feature_units.

    Raises:
      ValueError: if agent_interface_format isn't specified.
      ValueError: if map_size isn't specified when use_feature_units is.
    """
    if not agent_interface_format:
      raise ValueError("Please specify agent_interface_format")

    self._agent_interface_format = agent_interface_format

    if agent_interface_format.use_feature_units:
      self._init_camera(
          agent_interface_format.feature_dimensions,
          map_size,
          agent_interface_format.camera_width_world_units)

    self._valid_functions = _init_valid_functions(
        agent_interface_format.action_dimensions)

  def _init_camera(
      self, feature_dimensions, map_size, camera_width_world_units):
    """Initialize the feature_units camera."""
    if not map_size or not camera_width_world_units:
      raise ValueError(
          "Either pass the game_info with raw enabled, or map_size and "
          "camera_width_world_units in order to use feature_units.")
    map_size = point.Point.build(map_size)
    self._world_to_world_tl = transform.Linear(point.Point(1, -1),
                                               point.Point(0, map_size.y))
    self._world_tl_to_world_camera_rel = transform.Linear(offset=-map_size / 4)
    world_camera_rel_to_feature_screen = transform.Linear(
        feature_dimensions.screen / camera_width_world_units,
        feature_dimensions.screen / 2)
    self._world_to_feature_screen_px = transform.Chain(
        self._world_to_world_tl,
        self._world_tl_to_world_camera_rel,
        world_camera_rel_to_feature_screen,
        transform.PixelToCoord())

  def _update_camera(self, camera_center):
    """Update the camera transform based on the new camera center."""
    self._world_tl_to_world_camera_rel.offset = (
        -self._world_to_world_tl.fwd_pt(camera_center) *
        self._world_tl_to_world_camera_rel.scale)

  def observation_spec(self):
    """The observation spec for the SC2 environment.

    It's worth noting that the image-like observations are in y,x/row,column
    order which is different than the actions which are in x,y order. This is
    due to conflicting conventions, and to facilitate printing of the images.

    Returns:
      The dict of observation names to their tensor shapes. Shapes with a 0 can
      vary in length, for example the number of valid actions depends on which
      units you have selected.
    """
    obs_spec = named_array.NamedDict({
        "action_result": (0,),  # See error.proto: ActionResult.
        "alerts": (0,),  # See sc2api.proto: Alert.
        "available_actions": (0,),
        "build_queue": (0, len(UnitLayer)),  # pytype: disable=wrong-arg-types
        "cargo": (0, len(UnitLayer)),  # pytype: disable=wrong-arg-types
        "cargo_slots_available": (1,),
        "control_groups": (10, 2),
        "game_loop": (1,),
        "last_actions": (0,),
        "multi_select": (0, len(UnitLayer)),  # pytype: disable=wrong-arg-types
        "player": (len(Player),),  # pytype: disable=wrong-arg-types
        "score_cumulative": (len(ScoreCumulative),),  # pytype: disable=wrong-arg-types
        "single_select": (0, len(UnitLayer)),  # Only (n, 7) for n in (0, 1).  # pytype: disable=wrong-arg-types
        # "units": [n],  units have an array of object Unit which define in unit_control.py
    })

    aif = self._agent_interface_format

    if aif.feature_dimensions:
      obs_spec["feature_screen"] = (len(SCREEN_FEATURES),
                                    aif.feature_dimensions.screen.y,
                                    aif.feature_dimensions.screen.x)

      obs_spec["feature_minimap"] = (len(MINIMAP_FEATURES),
                                     aif.feature_dimensions.minimap.y,
                                     aif.feature_dimensions.minimap.x)
    if aif.rgb_dimensions:
      obs_spec["rgb_screen"] = (aif.rgb_dimensions.screen.y,
                                aif.rgb_dimensions.screen.x,
                                3)
      obs_spec["rgb_minimap"] = (aif.rgb_dimensions.minimap.y,
                                 aif.rgb_dimensions.minimap.x,
                                 3)
    if aif.use_feature_units:
      obs_spec["feature_units"] = (0, len(FeatureUnit))  # pytype: disable=wrong-arg-types
    return obs_spec

  def action_spec(self):
    """The action space pretty complicated and fills the ValidFunctions."""
    return self._valid_functions

  @sw.decorate
  def transform_obs(self, obs):
    """Render some SC2 observations into something an agent can handle."""
    empty = np.array([], dtype=np.int32).reshape((0, 7))
    out = named_array.NamedDict({  # Fill out some that are sometimes empty.
        "single_select": empty,
        "multi_select": empty,
        "build_queue": empty,
        "cargo": empty,
        "cargo_slots_available": np.array([0], dtype=np.int32),
    })

    def or_zeros(layer, size):
      if layer is not None:
        return layer.astype(np.int32, copy=False)
      else:
        return np.zeros((size.y, size.x), dtype=np.int32)

    aif = self._agent_interface_format

    if aif.feature_dimensions:
      out["feature_screen"] = named_array.NamedNumpyArray(
          np.stack(or_zeros(f.unpack(obs.observation),
                            aif.feature_dimensions.screen)
                   for f in SCREEN_FEATURES),
          names=[ScreenFeatures, None, None])
      out["feature_minimap"] = named_array.NamedNumpyArray(
          np.stack(or_zeros(f.unpack(obs.observation),
                            aif.feature_dimensions.minimap)
                   for f in MINIMAP_FEATURES),
          names=[MinimapFeatures, None, None])

    if aif.rgb_dimensions:
      out["rgb_screen"] = Feature.unpack_rgb_image(
          obs.observation.render_data.map).astype(np.int32)
      out["rgb_minimap"] = Feature.unpack_rgb_image(
          obs.observation.render_data.minimap).astype(np.int32)

    out["last_actions"] = np.array(
        [self.reverse_action(a).function for a in obs.actions],
        dtype=np.int32)

    out["action_result"] = np.array([o.result for o in obs.action_errors],
                                    dtype=np.int32)

    out["alerts"] = np.array(obs.observation.alerts, dtype=np.int32)

    out["game_loop"] = np.array([obs.observation.game_loop], dtype=np.int32)

    score_details = obs.observation.score.score_details
    out["score_cumulative"] = named_array.NamedNumpyArray([
        obs.observation.score.score,
        score_details.idle_production_time,
        score_details.idle_worker_time,
        score_details.total_value_units,
        score_details.total_value_structures,
        score_details.killed_value_units,
        score_details.killed_value_structures,
        score_details.collected_minerals,
        score_details.collected_vespene,
        score_details.collection_rate_minerals,
        score_details.collection_rate_vespene,
        score_details.spent_minerals,
        score_details.spent_vespene,
    ], names=ScoreCumulative, dtype=np.int32)

    player = obs.observation.player_common
    out["player"] = named_array.NamedNumpyArray([
        player.player_id,
        player.minerals,
        player.vespene,
        player.food_used,
        player.food_cap,
        player.food_army,
        player.food_workers,
        player.idle_worker_count,
        player.army_count,
        player.warp_gate_count,
        player.larva_count,
    ], names=Player, dtype=np.int32)

    def unit_vec(u):
      return np.array((
          u.unit_type,
          u.player_relative,
          u.health,
          u.shields,
          u.energy,
          u.transport_slots_taken,
          int(u.build_progress * 100),  # discretize
      ), dtype=np.int32)

    ui = obs.observation.ui_data

    with sw("ui"):
      groups = np.zeros((10, 2), dtype=np.int32)
      for g in ui.groups:
        groups[g.control_group_index, :] = (g.leader_unit_type, g.count)
      out["control_groups"] = groups

      if ui.single:
        out["single_select"] = named_array.NamedNumpyArray(
            [unit_vec(ui.single.unit)], [None, UnitLayer])

      if ui.multi and ui.multi.units:
        out["multi_select"] = named_array.NamedNumpyArray(
            [unit_vec(u) for u in ui.multi.units], [None, UnitLayer])

      if ui.cargo and ui.cargo.passengers:
        out["single_select"] = named_array.NamedNumpyArray(
            [unit_vec(ui.single.unit)], [None, UnitLayer])
        out["cargo"] = named_array.NamedNumpyArray(
            [unit_vec(u) for u in ui.cargo.passengers], [None, UnitLayer])
        out["cargo_slots_available"] = np.array([ui.cargo.slots_available],
                                                dtype=np.int32)

      if ui.production and ui.production.build_queue:
        out["single_select"] = named_array.NamedNumpyArray(
            [unit_vec(ui.production.unit)], [None, UnitLayer])
        out["build_queue"] = named_array.NamedNumpyArray(
            [unit_vec(u) for u in ui.production.build_queue],
            [None, UnitLayer])

    def feature_unit_vec(u):
      screen_pos = self._world_to_feature_screen_px.fwd_pt(
          point.Point.build(u.pos))
      screen_radius = self._world_to_feature_screen_px.fwd_dist(u.radius)
      return np.array((
          # Match unit_vec order
          u.unit_type,
          u.alliance,  # Self = 1, Ally = 2, Neutral = 3, Enemy = 4
          u.health,
          u.shield,
          u.energy,
          u.cargo_space_taken,
          int(u.build_progress * 100),  # discretize

          # Resume API order
          int(u.health / u.health_max * 255) if u.health_max > 0 else 0,
          int(u.shield / u.shield_max * 255) if u.shield_max > 0 else 0,
          int(u.energy / u.energy_max * 255) if u.energy_max > 0 else 0,
          u.display_type,  # Visible = 1, Snapshot = 2, Hidden = 3
          u.owner,  # 1-15, 16 = neutral
          screen_pos.x,
          screen_pos.y,
          u.facing,
          screen_radius,
          u.cloak,  # Cloaked = 1, CloakedDetected = 2, NotCloaked = 3
          u.is_selected,
          u.is_blip,
          u.is_powered,
          u.mineral_contents,
          u.vespene_contents,

          # Not populated for enemies or neutral
          u.cargo_space_max,
          u.assigned_harvesters,
          u.ideal_harvesters,
          u.weapon_cooldown,
      ), dtype=np.int32)

    raw = obs.observation.raw_data

    if aif.use_feature_units:
      with sw("feature_units"):
        # Update the camera location so we can calculate world to screen pos
        self._update_camera(point.Point.build(raw.player.camera))
        feature_units = []
        for u in raw.units:
          if u.is_on_screen and u.display_type != sc_raw.Hidden:
            feature_units.append(feature_unit_vec(u))
        out["feature_units"] = named_array.NamedNumpyArray(
            feature_units, [None, FeatureUnit], dtype=np.int32)

    out["available_actions"] = np.array(self.available_actions(obs.observation),
                                        dtype=np.int32)
    out["units"] = self.transform_unit_control(raw)
    out["raw_data"] = raw
    return out

  @sw.decorate
  def transform_unit_control(self, raw):
    units = []
    for u in raw.units:
      units.append(unit_controls.Unit(u=u))
    return units

  @sw.decorate
  def available_actions(self, obs):
    """Return the list of available action ids."""
    available_actions = set()
    hide_specific_actions = self._agent_interface_format.hide_specific_actions
    for i, func in six.iteritems(actions.FUNCTIONS_AVAILABLE):
      if func.avail_fn(obs):
        available_actions.add(i)
    for a in obs.abilities:
      if a.ability_id not in actions.ABILITY_IDS:
        logging.warning("Unknown ability %s seen as available.", a.ability_id)
        continue
      for func in actions.ABILITY_IDS[a.ability_id]:
        if func.function_type in actions.POINT_REQUIRED_FUNCS[a.requires_point]:
          if func.general_id == 0 or not hide_specific_actions:
            available_actions.add(func.id)
          if func.general_id != 0:  # Always offer generic actions.
            for general_func in actions.ABILITY_IDS[func.general_id]:
              if general_func.function_type is func.function_type:
                # Only the right type. Don't want to expose the general action
                # to minimap if only the screen version is available.
                available_actions.add(general_func.id)
                break
    return list(available_actions)

  @sw.decorate
  def transform_action(self, obs, func_call, skip_available=False):
    """Tranform an agent-style action to one that SC2 can consume.

    Args:
      obs: a `sc_pb.Observation` from the previous frame.
      func_call: a `FunctionCall` to be turned into a `sc_pb.Action`.
      skip_available: If True, assume the action is available. This should only
          be used for testing or if you expect to make actions that weren't
          valid at the last observation.

    Returns:
      a corresponding `sc_pb.Action`.

    Raises:
      ValueError: if the action doesn't pass validation.
    """
    func_id = func_call.function
    try:
      func = actions.FUNCTIONS[func_id]
    except KeyError:
      raise ValueError("Invalid function id: %s." % func_id)

    # Available?
    if not (skip_available or func_id in self.available_actions(obs)):
      raise ValueError("Function %s/%s is currently not available" % (
          func_id, func.name))

    # Right number of args?
    if len(func_call.arguments) != len(func.args):
      raise ValueError(
          "Wrong number of arguments for function: %s, got: %s" % (
              func, func_call.arguments))

    # Args are valid?
    aif = self._agent_interface_format
    for t, arg in zip(func.args, func_call.arguments):
      if t.name in ("screen", "screen2"):
        sizes = aif.action_dimensions.screen
      elif t.name == "minimap":
        sizes = aif.action_dimensions.minimap
      else:
        sizes = t.sizes

      if len(sizes) != len(arg):
        raise ValueError(
            "Wrong number of values for argument of %s, got: %s" % (
                func, func_call.arguments))

      for s, a in zip(sizes, arg):
        if not 0 <= a < s:
          raise ValueError("Argument is out of range for %s, got: %s" % (
              func, func_call.arguments))

    # Convert them to python types.
    kwargs = {type_.name: type_.fn(a)
              for type_, a in zip(func.args, func_call.arguments)}

    # Call the right callback to get an SC2 action proto.
    sc2_action = sc_pb.Action()
    kwargs["action"] = sc2_action
    kwargs["action_space"] = aif.action_space
    if func.ability_id:
      kwargs["ability_id"] = func.ability_id
    actions.FUNCTIONS[func_id].function_type(**kwargs)
    return sc2_action

  @sw.decorate
  def reverse_action(self, action):
    """Transform an SC2-style action into an agent-style action.

    This should be the inverse of `transform_action`.

    Args:
      action: a `sc_pb.Action` to be transformed.

    Returns:
      A corresponding `actions.FunctionCall`.

    Raises:
      ValueError: if it doesn't know how to transform this action.
    """
    FUNCTIONS = actions.FUNCTIONS  # pylint: disable=invalid-name

    aif = self._agent_interface_format

    def func_call_ability(ability_id, cmd_type, *args):
      """Get the function id for a specific ability id and action type."""
      if ability_id not in actions.ABILITY_IDS:
        logging.warning("Unknown ability_id: %s. This is probably dance or "
                        "cheer, or some unknown new or map specific ability. "
                        "Treating it as a no-op.", ability_id)
        return FUNCTIONS.no_op()

      if aif.hide_specific_actions:
        general_id = next(iter(actions.ABILITY_IDS[ability_id])).general_id
        if general_id:
          ability_id = general_id

      for func in actions.ABILITY_IDS[ability_id]:
        if func.function_type is cmd_type:
          return FUNCTIONS[func.id](*args)
      raise ValueError("Unknown ability_id: %s, type: %s. Likely a bug." % (
          ability_id, cmd_type.__name__))

    if action.HasField("action_ui"):
      act_ui = action.action_ui
      if act_ui.HasField("multi_panel"):
        return FUNCTIONS.select_unit(act_ui.multi_panel.type - 1,
                                     act_ui.multi_panel.unit_index)
      if act_ui.HasField("control_group"):
        return FUNCTIONS.select_control_group(
            act_ui.control_group.action - 1,
            act_ui.control_group.control_group_index)
      if act_ui.HasField("select_idle_worker"):
        return FUNCTIONS.select_idle_worker(act_ui.select_idle_worker.type - 1)
      if act_ui.HasField("select_army"):
        return FUNCTIONS.select_army(act_ui.select_army.selection_add)
      if act_ui.HasField("select_warp_gates"):
        return FUNCTIONS.select_warp_gates(
            act_ui.select_warp_gates.selection_add)
      if act_ui.HasField("select_larva"):
        return FUNCTIONS.select_larva()
      if act_ui.HasField("cargo_panel"):
        return FUNCTIONS.unload(act_ui.cargo_panel.unit_index)
      if act_ui.HasField("production_panel"):
        return FUNCTIONS.build_queue(act_ui.production_panel.unit_index)
      if act_ui.HasField("toggle_autocast"):
        return func_call_ability(act_ui.toggle_autocast.ability_id,
                                 actions.autocast)

    if (action.HasField("action_feature_layer") or
        action.HasField("action_render")):
      act_sp = actions.spatial(action, aif.action_space)
      if act_sp.HasField("camera_move"):
        coord = point.Point.build(act_sp.camera_move.center_minimap)
        return FUNCTIONS.move_camera(coord)
      if act_sp.HasField("unit_selection_point"):
        select_point = act_sp.unit_selection_point
        coord = point.Point.build(select_point.selection_screen_coord)
        return FUNCTIONS.select_point(select_point.type - 1, coord)
      if act_sp.HasField("unit_selection_rect"):
        select_rect = act_sp.unit_selection_rect
        # TODO(tewalds): After looking at some replays we should decide if
        # this is good enough. Maybe we need to simulate multiple actions or
        # merge the selection rects into a bigger one.
        tl = point.Point.build(select_rect.selection_screen_coord[0].p0)
        br = point.Point.build(select_rect.selection_screen_coord[0].p1)
        return FUNCTIONS.select_rect(select_rect.selection_add, tl, br)
      if act_sp.HasField("unit_command"):
        cmd = act_sp.unit_command
        queue = int(cmd.queue_command)
        if cmd.HasField("target_screen_coord"):
          coord = point.Point.build(cmd.target_screen_coord)
          return func_call_ability(cmd.ability_id, actions.cmd_screen,
                                   queue, coord)
        elif cmd.HasField("target_minimap_coord"):
          coord = point.Point.build(cmd.target_minimap_coord)
          return func_call_ability(cmd.ability_id, actions.cmd_minimap,
                                   queue, coord)
        else:
          return func_call_ability(cmd.ability_id, actions.cmd_quick, queue)

    if action.HasField("action_raw") or action.HasField("action_render"):
      raise ValueError("Unknown action:\n%s" % action)

    return FUNCTIONS.no_op()


#-------------------------------------------------------------------------------
# Name:        module1
# Purpose:
#
# Author:      user
#
# Created:     11.03.2022
# Copyright:   (c) user 2022
# Licence:     <your licence>
#-------------------------------------------------------------------------------

"""An example of embedding a RichJupyterWidget in a PyQT Application.
This uses a normal kernel launched as a subprocess. It shows how to shutdown
the kernel cleanly when the application quits.
To run:
    python3 embed_qtconsole.py
"""
import os, sys
#from pyqt import QtWidgets
from PyQt5 import QtWidgets

from jupyter_core.paths import jupyter_runtime_dir

from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtconsole.manager import QtKernelManager

# The ID of an installed kernel, e.g. 'bash' or 'ir'.
USE_KERNEL = 'python3'

def _new_connection_file() -> str:
    cf = ""
    while not cf:
        # we don't need a 128b id to distinguish kernels, use more readable
        # 48b node segment (12 hex chars).  Users running more than 32k simultaneous
        # kernels can subclass.
        cf = os.path.join(jupyter_runtime_dir(), "kernel-%s.json" % os.getpid())
        # only keep if it's actually new.  Protect against unlikely collision
        # in 48b random search space
        cf = cf if not os.path.exists(cf) else ""
    return cf

def make_jupyter_widget_with_kernel():
    """Start a kernel, connect to it, and create a RichJupyterWidget to use it
    """
    kernel_manager = QtKernelManager(
        kernel_name=USE_KERNEL,
        connection_file=_new_connection_file())
    kernel_manager.start_kernel()

    print(kernel_manager.connection_file)
    kernel_client = kernel_manager.client()
    kernel_client.start_channels()

    jupyter_widget = RichJupyterWidget()
    jupyter_widget.kernel_manager = kernel_manager
    jupyter_widget.kernel_client = kernel_client
    return jupyter_widget

class MainWindow(QtWidgets.QMainWindow):
    """A window that contains a single Qt console."""
    def __init__(self):
        super().__init__()
        self.jupyter_widget = make_jupyter_widget_with_kernel()
        self.setCentralWidget(self.jupyter_widget)

    def shutdown_kernel(self):
        print('Shutting down kernel...')
        self.jupyter_widget.kernel_client.stop_channels()
        self.jupyter_widget.kernel_manager.shutdown_kernel()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.aboutToQuit.connect(window.shutdown_kernel)
    sys.exit(app.exec_())


"""deca_wallet_main URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from .views import RegisterUser, Login, Wallets, Fund, Withdrawal, WithdrawalAwaitingApproval, Approve, Promote, Demote

urlpatterns = [
    path('register', RegisterUser.as_view(), name='register_user'),
    path('login', Login.as_view(), name='login_user'),
    path('create_wallet', Wallets.as_view(), name='add_wallet'),
    path('get_wallet', Wallets.as_view(), name='get_user_wallet'),
    path('fund_wallet', Fund.as_view(), name='fund_user_wallet'),
    path('withdraw', Withdrawal.as_view(), name='withdrawal'),
    path('awaiting_approval', WithdrawalAwaitingApproval.as_view(), name='approve_withdrawal'),
    path('approve_withdrawal', Approve.as_view(), name='approve_withdrawal'),
    path('promote_user', Promote.as_view(), name='promote_user'),
    path('demote_user', Demote.as_view(), name='demote_user'),
]


MOV_TYPES = ['LIGHT', 'MEDIUM_1', 'MEDIUM_2', 'STRONG']
UP_TYPE = 'UP'
DOWN_TYPE = 'DOWN'

def calculate_awake_time_score(data_df):
    aux = data_df[data_df['type'].isin([UP_TYPE, DOWN_TYPE])]
    aux = aux[~(
        (aux['type'] == DOWN_TYPE) & (aux['type'].shift() == DOWN_TYPE) |
        (aux['type'] == UP_TYPE) & (aux['type'].shift(-1) == UP_TYPE)
    )]
    aux['diff'] = aux.index - aux.index.array.shift()
    aux = aux[aux['type'] == DOWN_TYPE]
    # aplicamos multiplicadores a las diff segun la etapa del sueno
    # primera 1.5 horas
    aux.loc[(aux.index >= 0) & (aux.index < 5400), 'diff'] *= 1.5 / 60.0
    # 1.5 -> 2.5 horas
    aux.loc[(aux.index >= 5400) & (aux.index < 9000), 'diff'] *= 1.5 / 60.0
    # 2.5 -> 3.5 horas
    aux.loc[(aux.index >= 9000) & (aux.index < 12600), 'diff'] *= 2.0 / 60.0
    # 3.5 -> 4.5 horas
    aux.loc[(aux.index >= 12600) & (aux.index < 16200), 'diff'] *= 2.0 / 60.0
    # 4.5 -> 5.5 horas
    aux.loc[(aux.index >= 16200) & (aux.index < 19800), 'diff'] *= 1.5 / 60.0
    # 5.5 -> 6.5 horas
    aux.loc[(aux.index >= 19800) & (aux.index < 23400), 'diff'] *= 1.5 / 60.0
    # 6.5 -> 7.5 horas
    aux.loc[(aux.index >= 23400) & (aux.index < 27000), 'diff'] *= 1.0 / 60.0
    # 7.5 -> 8 horas
    aux.loc[(aux.index >= 27000) & (aux.index < 28800), 'diff'] *= 0.5 / 60.0
    # Calculamos el out of bed factor
    out_of_bed_factor = aux['diff'].sum()
    # puntuacion_tiempo_despierto = ((-15*outOfBedFactor)/15)+15 (por 15 entre 15 ??)
    awake_time_score = max([0, ((-15 * out_of_bed_factor) / 15) + 15])

    return awake_time_score

def calculate_latency_time_score(start_time, sleep_start_time):
    diff = (sleep_start_time - start_time).seconds / 60
    latency_time_score = max([0, ((-5 * diff) / 60) + 5])

    return latency_time_score

def calculate_movements_score(data_df):
    # Calculo de las penalizaciones por movimiento
    # primera 1.5 horas
    data_df.loc[(data_df.index >= 0) & (data_df.index < 5400) & (data_df['type'] == 'MEDIUM_2'), 'sqi'] = 3
    data_df.loc[(data_df.index >= 0) & (data_df.index < 5400) & (data_df['type'] == 'STRONG'), 'sqi'] = 5
    # 1.5 -> 2.5 horas
    data_df.loc[(data_df.index >= 5400) & (data_df.index < 9000) & (data_df['type'] == 'MEDIUM_2'), 'sqi'] = 3
    data_df.loc[(data_df.index >= 5400) & (data_df.index < 9000) & (data_df['type'] == 'STRONG'), 'sqi'] = 4
    # 2.5 -> 3.5 horas
    data_df.loc[(data_df.index >= 9000) & (data_df.index < 12600) & (data_df['type'] == 'MEDIUM_2'), 'sqi'] = 2
    data_df.loc[(data_df.index >= 9000) & (data_df.index < 12600) & (data_df['type'] == 'STRONG'), 'sqi'] = 4
    # 3.5 -> 4.5 horas
    data_df.loc[(data_df.index >= 12600) & (data_df.index < 16200) & (data_df['type'] == 'MEDIUM_2'), 'sqi'] = 2
    data_df.loc[(data_df.index >= 12600) & (data_df.index < 16200) & (data_df['type'] == 'STRONG'), 'sqi'] = 3
    # 4.5 -> 5.5 horas
    data_df.loc[(data_df.index >= 16200) & (data_df.index < 19800) & (data_df['type'] == 'MEDIUM_2'), 'sqi'] = 1
    data_df.loc[(data_df.index >= 16200) & (data_df.index < 19800) & (data_df['type'] == 'STRONG'), 'sqi'] = 3
    # 5.5 -> 6.5 horas
    data_df.loc[(data_df.index >= 19800) & (data_df.index < 23400) & (data_df['type'] == 'MEDIUM_2'), 'sqi'] = 1
    data_df.loc[(data_df.index >= 19800) & (data_df.index < 23400) & (data_df['type'] == 'STRONG'), 'sqi'] = 2
    # 6.5 -> 7.5 horas
    data_df.loc[(data_df.index >= 23400) & (data_df.index < 27000) & (data_df['type'] == 'STRONG'), 'sqi'] = 2
    # 7.5 -> 8 horas
    data_df.loc[(data_df.index >= 27000) & (data_df.index < 28800) & (data_df['type'] == 'STRONG'), 'sqi'] = 1
    # Calculo HORAS dormidas (last_move en php)
    hours = data_df.index.max() / 3600
    # calculo sqi total
    sqi = data_df['sqi'].sum() / hours
    movements_score = max([0, ((-8 * sqi) / 3) + 80])

    return movements_score

def calculate_sleep_hours_factor(data_df):
    hours = data_df.index.max() / 3600
    sleep_hours_factor = min(1, 0.2 + ((1 - 0.2) * (hours / 8)))

    return sleep_hours_factor

def preprocess_data(sleep_start_time, movement_df):
    data_df = movement_df[['timestamp', 'type']].copy()
    data_df = data_df.set_index('timestamp')
    # filtramos despues de dormir
    # creamos una columna para apuntar el sqi de cada movimiento. Por defecto 0
    data_df['sqi'] = 0
    data_df.index = (data_df.index - sleep_start_time).total_seconds()
    data_df = data_df[data_df.index > 0]

    return data_df

def calculate_sqi(start_time, sleep_start_time, movement_df):
    # entre 0-100
    data_df = preprocess_data(sleep_start_time, movement_df)
    awake_time_score = calculate_awake_time_score(data_df)
    latency_time_score = calculate_latency_time_score(start_time, sleep_start_time)
    movements_score = calculate_movements_score(data_df)
    sleep_hours_factor = calculate_sleep_hours_factor(data_df)
    final_sqi = awake_time_score + latency_time_score + (movements_score * sleep_hours_factor)

    return final_sqi