from cerberus.validator import DocumentError
from flask import abort
from flask import current_app as app

from eve.auth import requires_auth
from eve.methods.common import (build_response_document,
                                marshal_write_response, oplog_push, parse,
                                payload, pre_event, ratelimit,
                                resolve_document_etag, resolve_embedded_fields,
                                resolve_sub_resource_path,
                                resolve_user_restricted_access, resource_link,
                                store_media_files, utcnow)
from eve.utils import config, debug_error_message, parse_request
from eve.versioning import (insert_versioning_documents,
                            resolve_document_version)

@ratelimit()
@requires_auth("resource")
@pre_event
def post(resource, payl=None):
    """
    Default function for handling POST requests, it has decorators for
    rate limiting, authentication and for raising pre-request events. After the
    decorators are applied forwards to call to :func:`post_internal`

    .. versionchanged:: 0.5
       Split original post() into post/post_internal combo.
    """
    return post_internal(resource, payl, skip_validation=False)

def post_internal(resource, payl=None, skip_validation=False):
    """
    Intended for internal post calls, this method is not rate limited,
    authentication is not checked and pre-request events are not raised.
    Adds one or more documents to a resource. Each document is validated
    against the domain schema. If validation passes the document is inserted
    and ID_FIELD, LAST_UPDATED and DATE_CREATED along with a link to the
    document are returned. If validation fails, a list of validation issues
    is returned.

    :param resource: name of the resource involved.
    :param payl: alternative payload. When calling post() from your own code
                 you can provide an alternative payload. This can be useful,
                 for example, when you have a callback function hooked to a
                 certain endpoint, and want to perform additional post() calls
                 from there.

                 Please be advised that in order to successfully use this
                 option, a request context must be available.

                 See https://github.com/pyeve/eve/issues/74 for a
                 discussion, and a typical use case.
    :param skip_validation: skip payload validation before write (bool)

    .. versionchanged:: 0.7
       Add support for Location header. Closes #795.

    .. versionchanged:: 0.6
       Fix: since v0.6, skip_validation = True causes a 422 response (#726).

    .. versionchanged:: 0.6
       Initialize DELETED field when soft_delete is enabled.

    .. versionchanged:: 0.5
       Back to resolving default values after validation as now the validator
       can properly validate dependency even when some have default values. See
       #353.
       Push updates to the OpLog.
       Original post() has been split into post() and post_internal().
       ETAGS are now stored with documents (#369).

    .. versionchanged:: 0.4
       Resolve default values before validation is performed. See #353.
       Support for document versioning.

    .. versionchanged:: 0.3
       Return 201 if at least one document has been successfully inserted.
       Fix #231 auth field not set if resource level authentication is set.
       Support for media fields.
       When IF_MATCH is disabled, no etag is included in the payload.
       Support for new validation format introduced with Cerberus v0.5.

    .. versionchanged:: 0.2
       Use the new STATUS setting.
       Use the new ISSUES setting.
       Raise 'on_pre_<method>' event.
       Explicitly resolve default values instead of letting them be resolved
       by common.parse. This avoids a validation error when a read-only field
       also has a default value.
       Added ``on_inserted*`` events after the database insert

    .. versionchanged:: 0.1.1
       auth.request_auth_value is now used to store the auth_field value.

    .. versionchanged:: 0.1.0
       More robust handling of auth_field.
       Support for optional HATEOAS.

    .. versionchanged: 0.0.9
       Event hooks renamed to be more robust and consistent: 'on_posting'
       renamed to 'on_insert'.
       You can now pass a pre-defined custom payload to the funcion.

    .. versionchanged:: 0.0.9
       Storing self.app.auth.userid in auth_field when 'user-restricted
       resource access' is enabled.

    .. versionchanged: 0.0.7
       Support for Rate-Limiting.
       Support for 'extra_response_fields'.

       'on_posting' and 'on_posting_<resource>' events are raised before the
       documents are inserted into the database. This allows callback functions
       to arbitrarily edit/update the documents being stored.

    .. versionchanged:: 0.0.6
       Support for bulk inserts.

       Please note: validation constraints are checked against the database,
       and not between the payload documents themselves. This causes an
       interesting corner case: in the event of a multiple documents payload
       where two or more documents carry the same value for a field where the
       'unique' constraint is set, the payload will validate successfully, as
       there are no duplicates in the database (yet). If this is an issue, the
       client can always send the documents once at a time for insertion, or
       validate locally before submitting the payload to the API.

    .. versionchanged:: 0.0.5
       Support for 'application/json' Content-Type .
       Support for 'user-restricted resource access'.

    .. versionchanged:: 0.0.4
       Added the ``requires_auth`` decorator.

    .. versionchanged:: 0.0.3
       JSON links. Superflous ``response`` container removed.
    """

    date_utc = utcnow()
    resource_def = app.config["DOMAIN"][resource]
    schema = resource_def["schema"]
    validator = (
        None
        if skip_validation
        else app.validator(
            schema, resource=resource, allow_unknown=resource_def["allow_unknown"]
        )
    )

    documents = []
    results = []
    failures = 0
    id_field = resource_def["id_field"]

    if config.BANDWIDTH_SAVER is True:
        embedded_fields = []
    else:
        req = parse_request(resource)
        embedded_fields = resolve_embedded_fields(resource, req)

    # validation, and additional fields
    if payl is None:
        payl = payload()

    if isinstance(payl, dict):
        payl = [payl]

    if not payl:
        # empty bulk insert
        abort(400, description=debug_error_message("Empty bulk insert"))

    if len(payl) > 1 and not config.DOMAIN[resource]["bulk_enabled"]:
        abort(400, description=debug_error_message("Bulk insert not allowed"))

    for value in payl:
        document = []
        doc_issues = {}
        try:
            document = parse(value, resource)
            resolve_sub_resource_path(document, resource)
            if skip_validation:
                validation = True
            else:
                validation = validator.validate(document)
            if validation:  # validation is successful
                # validator might be not available if skip_validation. #726.
                if validator:
                    # Apply coerced values
                    document = validator.document

                # Populate meta and default fields
                document[config.LAST_UPDATED] = document[config.DATE_CREATED] = date_utc

                if config.DOMAIN[resource]["soft_delete"] is True:
                    document[config.DELETED] = False

                resolve_user_restricted_access(document, resource)
                store_media_files(document, resource)
                resolve_document_version(document, resource, "POST")
            else:
                # validation errors added to list of document issues
                doc_issues = validator.errors
        except DocumentError as e:
            doc_issues["validation exception"] = str(e)
        except Exception as e:
            # most likely a problem with the incoming payload, report back to
            # the client as if it was a validation issue
            app.logger.exception(e)
            doc_issues["exception"] = str(e)

        if doc_issues:
            document = {config.STATUS: config.STATUS_ERR, config.ISSUES: doc_issues}
            failures += 1

        documents.append(document)

    if failures:
        # If at least one document got issues, the whole request fails and a
        # ``422 Bad Request`` status is return.
        for document in documents:
            if (
                config.STATUS in document
                and document[config.STATUS] == config.STATUS_ERR
            ):
                results.append(document)
            else:
                results.append({config.STATUS: config.STATUS_OK})

        return_code = config.VALIDATION_ERROR_STATUS
    else:
        # notify callbacks
        getattr(app, "on_insert")(resource, documents)
        getattr(app, "on_insert_%s" % resource)(documents)

        # compute etags here as documents might have been updated by callbacks.
        resolve_document_etag(documents, resource)

        # bulk insert
        ids = app.data.insert(resource, documents)

        # update oplog if needed
        oplog_push(resource, documents, "POST")

        # assign document ids
        for document in documents:
            # either return the custom ID_FIELD or the id returned by
            # data.insert().
            id_ = document.get(id_field, ids.pop(0))
            document[id_field] = id_

            # build the full response document
            result = document
            build_response_document(result, resource, embedded_fields, document)

            # add extra write meta data
            result[config.STATUS] = config.STATUS_OK

            # limit what actually gets sent to minimize bandwidth usage
            result = marshal_write_response(result, resource)
            results.append(result)

        # insert versioning docs
        insert_versioning_documents(resource, documents)

        # notify callbacks
        getattr(app, "on_inserted")(resource, documents)
        getattr(app, "on_inserted_%s" % resource)(documents)
        # request was received and accepted; at least one document passed
        # validation and was accepted for insertion.

        return_code = 201

    if len(results) == 1:
        response = results.pop(0)
    else:
        response = {
            config.STATUS: config.STATUS_ERR if failures else config.STATUS_OK,
            config.ITEMS: results,
        }

    if failures:
        response[config.ERROR] = {
            "code": return_code,
            "message": "Insertion failure: %d document(s) contain(s) error(s)"
            % failures,
        }

    location_header = (
        None
        if return_code != 201 or not documents
        else [("Location", "%s/%s" % (resource_link(), documents[0][id_field]))]
    )

    return response, None, None, return_code, location_header


import copy
import math
import numpy as np
import scipy.stats
from typing import List, Dict, Tuple, Set, Sequence

Player = str
PlayerIdx = int

ELO_PER_STRENGTH = 400.0 * math.log10(math.exp(1.0))
P1_ADVANTAGE_NAME = "P1Advantage"

class EloInfo:
  """Summarizes maximum likelihood Elos and uncertainties for a group of players."""

  def __init__(
      self,
      players: List[Player],
      elo: Dict[Player,float],
      elo_stderr: Dict[Player,float],
      elo_covariance: Dict[Tuple[Player,Player],float],
      effective_game_count: Dict[Player,float],
  ):
    self.players = players
    self.elo = elo
    self.elo_stderr = elo_stderr
    self.elo_covariance = elo_covariance
    self.effective_game_count = effective_game_count

    self.players = sorted(self.players, key=(lambda player: -elo[player] if player != P1_ADVANTAGE_NAME else 1e50))

  def get_players(self) -> List[Player]:
    return self.players

  def get_elo(self, p1: Player) -> float:
    """Returns the maximum likelihood Elo of p1"""
    return self.elo[p1]

  def get_approx_elo_stderr(self, p1: Player) -> float:
    """Returns an approximation of the standard error on the Elo of p1, ASSUMING all other players Elos are equal to their maximum likelihood value.
    This approximation may underestimate if the amount of data is very small."""
    return self.elo_stderr[p1]

  def get_elo_difference(self, p1: Player, p2: Player) -> float:
    """Returns the maximum likelhood difference in Elo between p1 and p2"""
    return self.elo[p1] - self.elo[p2]

  def get_approx_elo_difference_stderr(self, p1: Player, p2: Player) -> float:
    """Returns an approximation of the standard error on difference in Elo between p1 and p2.
    This approximation may underestimate if the amount of data is very small."""
    return math.sqrt(
      self.elo_covariance[(p1,p1)] - self.elo_covariance[(p1,p2)] - self.elo_covariance[(p2,p1)] + self.elo_covariance[(p2,p2)]
    )

  def get_approx_likelihood_of_superiority(self, p1: Player, p2: Player) -> float:
    """Returns an approximation of the likelihood that elo(p1) - elo(p2) > 0, given the data.
    This approximation may be overconfident or inaccurate if the amount of data is very small."""
    if p1 == p2:
      return 0.5
    mean = self.get_elo_difference(p1,p2)
    stderr = self.get_approx_elo_difference_stderr(p1,p2)
    return scipy.stats.t.cdf(mean/stderr,df=self.effective_game_count[p1]-1)

  def __str__(self) -> str:
    lines = []
    for player in self.players:
      lines.append(f"{player:20s}: {self.elo[player]:8.2f} +/- {self.elo_stderr[player]:5.2f}")
    return "\n".join(lines)

class Likelihood:
  """Summarizes the information in an observed datapoint about player Elos, or a prior about them.

  Represents that sum_{p in playercombo} Strength(p)*playercombo[p] + offset has likelihood function f raised to the power of weight.
  If kind is SIGMOID_KIND, f is the sigmoid function.
  If kind is GAUSSIAN_KIND, f is the pdf of a unit gaussian

  where strength is such that 1 unit of strength is e:1 odds of winning in a head-to-head game.
  """

  SIGMOID_KIND = 1
  GAUSSIAN_KIND = 2

  def __init__(
      self,
      playercombo: Dict[Player,float],
      offset: float,
      weight: float,
      gamecount: float,
      kind: int,
  ):
    self.playercombo = playercombo
    self.offset = offset
    self.weight = weight
    self.gamecount = gamecount
    self.kind = kind
    assert kind == Likelihood.SIGMOID_KIND or kind == Likelihood.GAUSSIAN_KIND, "invalid kind"

  def add_idxs(self, player_to_idx: Dict[Player,PlayerIdx]):
    self.pidxcombo : List[Tuple[PlayerIdx,float]] = [(player_to_idx[player],coeff) for (player,coeff) in self.playercombo.items()]

  LOG_ONE_OVER_SQRT_TWO_PI = math.log(1.0 / math.sqrt(2.0 * math.pi))

  def get_loglikelihood(self, strengths: np.ndarray) -> float:
    strength_total = self.offset + sum(strengths[pidx] * coeff for (pidx,coeff) in self.pidxcombo)
    if self.kind == Likelihood.SIGMOID_KIND:
      if strength_total < -40:
        return strength_total
      return -self.weight * math.log(1.0 + math.exp(-strength_total))
    else:
      return self.weight * (Likelihood.LOG_ONE_OVER_SQRT_TWO_PI - 0.5 * strength_total * strength_total)

  def accum_dloglikelihood_dstrength(self, strengths: np.ndarray, accum: np.ndarray):
    strength_total = self.offset + sum(strengths[pidx] * coeff for (pidx,coeff) in self.pidxcombo)
    if self.kind == Likelihood.SIGMOID_KIND:
      dloglikelihood_dstrength_total = self.weight / (1.0 + math.exp(strength_total))
    else:
      dloglikelihood_dstrength_total = -self.weight * strength_total
    for (pidx,coeff) in self.pidxcombo:
      accum[pidx] += coeff * dloglikelihood_dstrength_total

  def accum_d2loglikelihood_dstrength2(self, strengths: np.ndarray, accum: np.ndarray):
    strength_total = self.offset + sum(strengths[pidx] * coeff for (pidx,coeff) in self.pidxcombo)
    if self.kind == Likelihood.SIGMOID_KIND:
      denom = math.exp(-0.5 * strength_total) + math.exp(0.5 * strength_total)
      d2loglikelihood_dstrength_total2 = -self.weight / (denom * denom)
    else:
      d2loglikelihood_dstrength_total2 = -self.weight

    for (pidx1,coeff1) in self.pidxcombo:
      for (pidx2,coeff2) in self.pidxcombo:
        accum[pidx1,pidx2] += coeff1 * coeff2 * d2loglikelihood_dstrength_total2

  def accum_d2loglikelihood_dstrength2_scalepow(self, strengths: np.ndarray, accum: np.ndarray, scale: float, power: float):
    strength_total = self.offset + sum(strengths[pidx] * coeff for (pidx,coeff) in self.pidxcombo)
    if self.kind == Likelihood.SIGMOID_KIND:
      denom = math.exp(-0.5 * strength_total) + math.exp(0.5 * strength_total)
      d2loglikelihood_dstrength_total2 = -self.weight / (denom * denom)
    else:
      d2loglikelihood_dstrength_total2 = -self.weight

    for (pidx1,coeff1) in self.pidxcombo:
      for (pidx2,coeff2) in self.pidxcombo:
        x = coeff1 * coeff2 * d2loglikelihood_dstrength_total2
        accum[pidx1,pidx2] += scale * (x ** power)

def likelihood_of_games(
    p1: Player,
    p2: Player,
    num_games: float,
    p1_won_proportion: float,
    include_first_player_advantage: bool,
) -> List[Likelihood]:
  """Return a list of Likelihood objects representing the result of set of games between p1 and p2

  These Likelihoods can accumulated with any other Likelihoods, and then all passed to compute_elos
  to compute maximum likelihood Elos for all the players.

  NOTE: For performance reasons, you should try to minimize the number of these you create. If p1 and p2 played
  a large number of games, don't call this function once per game. Instead, call it once for all the games
  together (or twice, if you are using include_first_player_advantage=True, separately reporting the stats
  that occurred when each side was the first player).

  NOTE: If specifying include_first_player_advantage=True, make sure to add a make_single_player_prior on
  P1_ADVANTAGE_NAME.

  Arguments:
  p1: Name of the first player
  p2: Name of the second player
  num_games: The number of games played
  p1_won_proportion: The proportion of games that p1 won among those games played, counting draws as 0.5.
  include_first_player_advantage: If true, will also make the computation take into account that the first player
    might have an advantage (or a disadvantage!) and it will try to estimate the amount of that advantage.

  Returns:
  List of likelihood objects summarizing the information.
  """

  ret = []
  assert p1_won_proportion >= 0.0 and p1_won_proportion <= 1.0
  assert num_games >= 0.0
  assert p1 != p2

  if num_games > 0.0:
    if not include_first_player_advantage:
      if p1_won_proportion > 0.0:
        ret.append(Likelihood(
          playercombo={p1: 1.0, p2: -1.0},
          offset=0.0,
          weight=p1_won_proportion*num_games,
          gamecount=p1_won_proportion*num_games,
          kind=Likelihood.SIGMOID_KIND
        ))
      if p1_won_proportion < 1.0:
        ret.append(Likelihood(
          playercombo={p2: 1.0, p1: -1.0},
          offset=0.0,
          weight=(1.0-p1_won_proportion)*num_games,
          gamecount=(1.0-p1_won_proportion)*num_games,
          kind=Likelihood.SIGMOID_KIND
        ))
    else:
      if p1_won_proportion > 0.0:
        ret.append(Likelihood(
          playercombo={p1: 1.0, p2: -1.0, P1_ADVANTAGE_NAME: 1.0},
          offset=0.0,
          weight=p1_won_proportion*num_games,
          gamecount=p1_won_proportion*num_games,
          kind=Likelihood.SIGMOID_KIND
        ))
      if p1_won_proportion < 1.0:
        ret.append(Likelihood(
          playercombo={p2: 1.0, p1: -1.0, P1_ADVANTAGE_NAME: -1.0},
          offset=0.0,
          weight=(1.0-p1_won_proportion)*num_games,
          gamecount=(1.0-p1_won_proportion)*num_games,
          kind=Likelihood.SIGMOID_KIND
        ))

  return ret

def make_single_player_prior(
    p1: Player,
    num_games: float,
    elo: float,
) -> List[Likelihood]:
  """Return a list of Likelihood objects representing a Bayesian prior that p1 is the specified Elo.

  The strength of the prior that p1 is the specified Elo is as if p1 were observed to have played
  num_games many games against a known player of that Elo and won half and lost half.

  Returns:
  List of likelihood objects summarizing the information.
  """
  ret = []
  assert num_games >= 0.0
  assert np.isfinite(elo)
  if num_games > 0.0:
    ret.append(Likelihood(
      playercombo={p1: 1.0},
      offset=(-elo / ELO_PER_STRENGTH),
      weight=0.5*num_games,
      gamecount=0.5*num_games,
      kind=Likelihood.SIGMOID_KIND
    ))
    ret.append(Likelihood(
      playercombo={p1: -1.0},
      offset=(elo / ELO_PER_STRENGTH),
      weight=0.5*num_games,
      gamecount=0.5*num_games,
      kind=Likelihood.SIGMOID_KIND
    ))
  return ret

def make_sequential_prior(
    players: List[Player],
    num_games: float,
) -> List[Likelihood]:
  """Return a list of Likelihood objects representing a Bayesian prior that each player in the sequence is similar in strength to the previous.

  This can be used, for example, if there were a sequence of changes between different versions, such that each version on average
  is expected to be more similar to its neighbors.

  The strength of the prior between each sequential pair of players is as if they were observed to have played
  num_games many games against each other and won half and lost half.

  Returns:
  List of likelihood objects summarizing the information.
  """
  ret = []
  assert num_games >= 0.0
  assert len(set(players)) == len(players), "players must not contain any duplicates"

  if len(players) < 1:
    return ret

  for i in range(len(players)-1):
    ret.extend(likelihood_of_games(
      p1=players[i],
      p2=players[i+1],
      num_games=num_games,
      p1_won_proportion=0.5,
      include_first_player_advantage=False,
    ))
  return ret

def make_center_elos_prior(
    players: Sequence[Player],
    elo: float,
) -> List[Likelihood]:
  """Return a list of Likelihood objects representing a Bayesian prior that the mean of all player Elos is the specified Elo.

  This prior will have no effect on the relative Elos of the players, unless it fights with another that sets players to
  specific Elos, such as make_single_player_prior. It can simply be used to center all the Elos of the players.

  Returns:
  List of likelihood objects summarizing the information.
  """
  ret = []
  assert np.isfinite(elo)
  assert len(set(players)) == len(players), "players must not contain any duplicates"
  playercombo = { player: 1.0 for player in players }
  ret.append(Likelihood(
    playercombo=playercombo,
    offset=-len(players) * elo / ELO_PER_STRENGTH,
    weight=0.001,
    gamecount=0.0,
    kind=Likelihood.GAUSSIAN_KIND
  ))
  return ret

def compute_elos(
    data: List[Likelihood],
    tolerance: float = 0.001,
    max_iters: int = 1000,
    verbose: bool = False,
) -> EloInfo:
  """Compute maximum-likelihood Elo ratings given the data and any priors.

  NOTE: It is recommend that you specify some sort of prior, even if relatively weak, for numerical stability.
  When you call this function, it is up to you to make sure that the data and priors you have provided result in all
  all Elos of all players being "anchored". For example, this function may crash or fail or return bad values if:

  * There is a player with no data at all, and no prior for that player.
    (since that would mean an Elo for that player cannot be defined).

  * There is a player who has only won and never lost, and there is no prior that restricts that player's rating.
    (since then nothing could stop player's Elo from going to infinity).

  * All players *have* won and lost against other players, but there is nothing that anchors the Elo of the
    population as a whole, such no player having a make_single_player_prior AND there being no make_center_elos_prior.
    (since even if all players Elos are known relative to one another, the Elos to report are undefined - there is nothing
     to say where the Elos should be centered or what value they should start from).

  Examples of things that are normally sufficient to anchor everyone:
  If all players have won and lost against other players, adding a make_center_elos_prior is sufficient.
  If all players have won and lost against other players, AND every player has transitively beat every other player and
  transitively lost to every other player, then adding a make_single_player_prior to one player is sufficient.
  Regardless of whether the players have won or lost, adding a make_single_player_prior to every player is sufficient.
  Regardless of whether the players have won or lost, adding both a make_sequential_prior and a make_center_elos_prior is sufficient.

  NOTE: Even aside from ensuring numeric stability and anchoring as above, it is probably good practice to add some mild prior
  beyond that anyways. If your players are sequential (e.g. a series of different nets), a weak make_sequential_prior could be good.
  If you want to just put all players on equal footing unbiasedly, adding a weak make_single_player_prior to each player that its Elo
  is 0 (or 1000, or whatever) is also good.

  And if you have actual prior beliefs about the players Elos, feel free to add those.

  Arguments:
  data: A single list of all the likelihoods from all your games and priors.
  tolerance: Stop soon after the Elos stop changing by more than this.
  max_iters: Bail out if the optimization takes more than this many iterations.
  verbose: Print out the iteration as it proceeds.

  Returns:
  Elos. Yay!
  """

  players = []
  for d in data:
    players.extend(d.playercombo.keys())
  players = list(set(players))
  players.sort()
  player_to_idx = { player: i for (i,player) in enumerate(players) }

  data = [copy.copy(d) for d in data]
  for d in data:
    d.add_idxs(player_to_idx)

  num_players = len(players)

  def compute_loglikelihood(strengths: np.ndarray) -> float:
    total = 0.0
    for d in data:
      total += d.get_loglikelihood(strengths)
    return total

  # Gauss newton
  def find_ascent_vector(strengths: np.ndarray) -> np.ndarray:
    dloglikelihood_dstrength = np.zeros(num_players,dtype=np.float64)
    d2loglikelihood_dstrength2 = np.zeros((num_players,num_players),dtype=np.float64)

    for d in data:
      d.accum_dloglikelihood_dstrength(strengths, dloglikelihood_dstrength)
      d.accum_d2loglikelihood_dstrength2(strengths, d2loglikelihood_dstrength2)

    ascent = -np.linalg.solve(d2loglikelihood_dstrength2,dloglikelihood_dstrength)
    return ascent

  def line_search_ascend(strengths: np.ndarray, cur_loglikelihood: float) -> Tuple[np.ndarray,float]:
    ascent = find_ascent_vector(strengths)
    # Try up to this many times to find an improvement
    for i in range(30):
      new_strengths = strengths + ascent
      new_loglikelihood = compute_loglikelihood(new_strengths)
      if new_loglikelihood > cur_loglikelihood:
        return (new_strengths, new_loglikelihood)
      # Shrink ascent step and try again
      ascent *= 0.6
    return (strengths,cur_loglikelihood)

  strengths = np.zeros(num_players,dtype=np.float64)
  loglikelihood = compute_loglikelihood(strengths)
  iters_since_big_change = 0
  last_elo_change = None
  for i in range(max_iters):
    if verbose:
      print(f"Beginning iteration {i}, cur log likelihood {loglikelihood}, last elo change {last_elo_change}")

    (new_strengths, new_loglikelihood) = line_search_ascend(strengths, loglikelihood)
    elodiff = (new_strengths - strengths) * ELO_PER_STRENGTH
    last_elo_change = 0 if len(elodiff) <= 0 else np.max(np.abs(elodiff))

    strengths = new_strengths
    loglikelihood = new_loglikelihood

    iters_since_big_change += 1
    if np.any(elodiff > tolerance):
      iters_since_big_change = 0

    if iters_since_big_change > 3:
      break

  d2loglikelihood_dstrength2 = np.zeros((num_players,num_players),dtype=np.float64)
  for d in data:
    d.accum_d2loglikelihood_dstrength2(strengths, d2loglikelihood_dstrength2)
  strength_precision = -d2loglikelihood_dstrength2
  elo_precision = strength_precision / (ELO_PER_STRENGTH * ELO_PER_STRENGTH)
  elo_covariance = np.linalg.inv(elo_precision)

  sqrt_ess_numerator = np.zeros((num_players,num_players),dtype=np.float64)
  ess_denominator = np.zeros((num_players,num_players),dtype=np.float64)
  for d in data:
    if d.gamecount > 0.0:
      d.accum_d2loglikelihood_dstrength2_scalepow(strengths, sqrt_ess_numerator, scale = 1.0, power=1.0)
      d.accum_d2loglikelihood_dstrength2_scalepow(strengths, ess_denominator, scale = 1.0 / d.gamecount, power=2.0)

  info = EloInfo(
    players = players,
    elo = { player: ELO_PER_STRENGTH * strengths[player_to_idx[player]] for player in players },
    elo_stderr = { player: math.sqrt(1.0 / elo_precision[player_to_idx[player],player_to_idx[player]]) for player in players },
    elo_covariance = { (p1,p2): elo_covariance[player_to_idx[p1],player_to_idx[p2]] for p1 in players for p2 in players },
    effective_game_count = {
      player: (np.square(sqrt_ess_numerator[player_to_idx[player],player_to_idx[player]]) /
               ess_denominator[player_to_idx[player],player_to_idx[player]])
      for player in players
    },
  )
  return info

# Testing code
if __name__ == "__main__":
  data = []
  data.extend(likelihood_of_games("Alice","Bob", 18, 12/18, False))
  data.extend(likelihood_of_games("Bob","Carol", 18, 12/18, False))
  data.extend(likelihood_of_games("Carol","Dan", 36, 12/18, False))
  data.extend(likelihood_of_games("Dan","Eve", 48, 40/48, False))
  data.extend(make_center_elos_prior(["Alice","Bob","Carol","Dan","Eve"],0))
  info = compute_elos(data,verbose=True)

  for player in info.players:
    for player2 in info.players:
      print(info.get_approx_likelihood_of_superiority(player,player2),end=" ")
    print()


from operator import truediv
import sqlite3 as sl
import json
import time
import datetime
import os
from db_dictionary import dict_factory
from sys import platform

global car_space_id
car_space_id = 9

# no owner, need to add in frontend
def convert_path_to_data(path):
	text_file = open(path, "r")
	data = text_file.read()
	text_file.close()
	return data

def get_picture_path(id):
	cwd = os.getcwd()
	if platform == "linux" or platform == "linux2" or platform == 'darwin':
		directory = f"{cwd}/picture/{id}"
	else:
		directory = f"{cwd}\\picture\\{id}"
	return directory

def create_car_space(input):
	con = sl.connect('my-test.db')

	id = input["listing_id"]
	# delete if already exists
	sql_query = f'''DELETE FROM car_spaces
		WHERE id == {id}'''
	with con:
		con.execute(sql_query)

	name = input["name"]
	owner = input["user_id"]
	address = input["address"]

	cwd = os.getcwd()
	photo_path = get_picture_path(id)
	text_file = open(photo_path, "w+")
	n = text_file.write(input["photo_link"])
	text_file.close()
	photo_link = photo_path

	space_type = input["space_type"]

	amenity_24_7 = input["amenity_24_7"]
	amenity_sheltered = input["amenity_sheltered"]
	amenity_security_gates = input["amenity_security_gates"]

	description = input["description"]
	instruction = input["instruction"]
	monday = input["monday"]
	tuesday = input["tuesday"]
	wednesday = input["wednesday"]
	thursday = input["thursday"]
	friday = input["friday"]
	saturday = input["saturday"]
	sunday = input["sunday"]
	starttime = input["starttime"]
	endtime = input["endtime"]
	price_hourly = input["price_hourly"]
	price_daily = input["price_daily"]
	postcode = input["postcode"]
	coordinates = input["coordinates"]
	creation_date = int(time.time())

	sql_query = '''INSERT INTO car_spaces (id, name, owner, address, coordinates, description,
 	instruction, price_hourly, price_daily, postcode, photo_link, space_type, amenity_24_7, amenity_sheltered,
  	amenity_security_gates, monday, tuesday, wednesday, thursday, friday, saturday, sunday, starttime, endtime,
   	creation_date, hours_used)
   	values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'''
	data = [(id, name, owner, address, coordinates, description,
	instruction, price_hourly, price_daily, postcode, photo_link, space_type,
	amenity_24_7, amenity_sheltered, amenity_security_gates, monday, tuesday, wednesday, thursday, friday,
 	saturday, sunday, starttime, endtime, creation_date, 0)]
	with con:
		con.executemany(sql_query, data)

	con.commit()
	con.close()
	return

def delete_car_space(id, user_id):
	con = sl.connect('my-test.db')
	cur = con.cursor()

	sql_query = f'''SELECT * FROM car_spaces
		WHERE id = {id};'''

	cur.execute(sql_query)
	data = cur.fetchall()
	if len(data) == 0:
		return 404

	sql_query = "DELETE FROM car_spaces WHERE id = ?;"
	cur.execute(sql_query, (id,))
	con.commit()

	# check if deleted
	sql_query = f'''SELECT * FROM car_spaces
		WHERE id = {id};'''

	cur.execute(sql_query)
	data = cur.fetchall()
	if len(data) != 0:
		return 500

	con.close()

	return 200

def get_listing_by_id(id):
	#try:
	con = sl.connect('my-test.db')
	con.row_factory = dict_factory
	cur = con.cursor()
	sql_query = f"SELECT * FROM car_spaces WHERE id = {id};"
	cur.execute(sql_query)
	record = cur.fetchone()

	if not record:
		return 404, None

	sql_query = f'SELECT * FROM booking WHERE car_space = {id}'
	cur.execute(sql_query)
	records = cur.fetchall()
	bookings_num = 0
	bookings_in_the_past = 0
	now = datetime.datetime.now()
	for record2 in records:
		bookings_num = bookings_num + 1
		booking_end_time = record2['end_time']
		datetime_booking_end_time = datetime.datetime.strptime(booking_end_time, '%Y-%m-%d %H:%M:%S')
		if datetime_booking_end_time < now:
			bookings_in_the_past = bookings_in_the_past + 1

	record['total_bookings'] = bookings_num
	try:
		percent = bookings_in_the_past / bookings_num
	except ZeroDivisionError:
		percent = 0
	record['percentage_bookings'] = round(percent, 2) * 100

	car_space_id = record["id"]
	photo_path = get_picture_path(car_space_id)
	record["photo_link"] = convert_path_to_data(photo_path)

	return 200, record

def get_listings_all():
	#try:
	con = sl.connect('my-test.db')
	con.row_factory = dict_factory
	cur = con.cursor()
	sql_query = "SELECT * FROM car_spaces;"
	cur.execute(sql_query)
	records = cur.fetchall()

	for record in records:
		car_space_id = record["id"]
		photo_path = get_picture_path(car_space_id)
		record["photo_link"] = convert_path_to_data(photo_path)
	return records

def get_listing_by_userid(userid):
    	#try:
	con = sl.connect('my-test.db')
	con.row_factory = dict_factory
	cur = con.cursor()
	sql_query = f"SELECT * FROM car_spaces WHERE owner = {userid};"
	cur.execute(sql_query)
	records = cur.fetchall()

	if not records:
		return 404, None

	final_list = []
	for record in records:

		id = record['id']
		sql_query = f'SELECT * FROM booking WHERE car_space = {id}'
		cur.execute(sql_query)
		records = cur.fetchall()
		bookings_num = 0
		bookings_in_the_past = 0
		now = datetime.datetime.now()
		for record2 in records:
			bookings_num = bookings_num + 1
			booking_end_time = record2['end_time']
			datetime_booking_end_time = datetime.datetime.strptime(booking_end_time, '%Y-%m-%d %H:%M:%S')
			if datetime_booking_end_time < now:
				bookings_in_the_past = bookings_in_the_past + 1

		record['total_bookings'] = bookings_num
		percent = 0
		try:
			percent = bookings_in_the_past / bookings_num
		except ZeroDivisionError:
			percent = 0
		record['percentage_bookings'] = round(percent, 2)

		car_space_id = record["id"]
		photo_path = get_picture_path(car_space_id)
		record["photo_link"] = convert_path_to_data(photo_path)

		final_list.append(record)

	return 200, final_list

# checks if car space belongs to a user
def is_owner(listing_id, user_id):
	con = sl.connect('my-test.db')
	con.row_factory = dict_factory
	cur = con.cursor()

	sql_query = f"SELECT * FROM car_spaces WHERE id = {listing_id};"

	cur.execute(sql_query)
	record = cur.fetchone()

	if record["owner"] == user_id:
		return True

	return False

# check if a listing exists
def exists(listing_id):
	con = sl.connect('my-test.db')
	con.row_factory = dict_factory
	cur = con.cursor()

	sql_query = f"SELECT * FROM car_spaces WHERE id = {listing_id};"

	cur.execute(sql_query)
	record = cur.fetchone()

	if record:
		return True

	return False

def is_valid_time(listing_id, booking_start_time, booking_end_time):

	start_time = datetime.datetime.fromisoformat(booking_start_time)
	start_hour = start_time.hour

	end_time = datetime.datetime.fromisoformat(booking_end_time)
	end_hour = end_time.hour

	con = sl.connect('my-test.db')
	con.row_factory = dict_factory
	cur = con.cursor()

	sql_query = f"SELECT * FROM car_spaces WHERE id = {listing_id};"

	cur.execute(sql_query)
	record = cur.fetchone()

	if not record:
		return False

	if start_hour >= record["starttime"] and end_hour <= record["endtime"]:
		return True

	return False


# https://leetcode.com/problems/task-scheduler/
# leetcode
# 그리디
# 테스크 스케줄러

# 해결 못하고 책 풀이 분석함

#%%
tasks = ["A","A","A","B","B","B"]
n = 2
# tasks = ["A","A","A","B","B","B"]
# n = 0
# tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"]
# n = 2

#%%
from collections import Counter
count = Counter(tasks)
count
# %%
import heapq
heap = []
# %%
for _item, _count in count.items():
    heapq.heappush(heap, ((-1) * _count, _item))
heap
#%%
answer = 0
window = []
_answer = []

if n == 0:
    answer = len(tasks)
answer
#%%
while heap:
    print(f'heap : ', heap)
    answer += 1

    item = None
    # 초반 window 크기가 n보다 작을 때
    if len(window) < n:
        # window 공백 수가 heap의 문자 수보다 많다면
        if window.count("") >= len(heap):
            # 공백 추가
            window.append("")
            _answer.append("") # 삭제
            print('case 1-1')
            print(f'_answer : {_answer}')

        else:
            _temp = []
            while True:
                item = heapq.heappop(heap)
                # window에 문자가 이미 존재하는 경우에만 _temp 저장
                if item[1] in window:
                    _temp.append((item[0], item[1]))

                # window에 없는 문자를 찾아 삽입하기
                if item[1] not in window:
                    window.append(item[1])

                    _answer.append(item[1])
                    print(f'_answer : {_answer}')

                    # _temp -> heap 복원
                    for _item in _temp:
                        heapq.heappush(heap, _item)
                    break
            print('case 1-2')

        print(f'case 1, window : {window}')

    # window 수 == n 인 경우
    else:
        item = heapq.heappop(heap)
        if item[1] in window:
            window.remove(item[1])
            window.append("")
            print(f'case 2, window : {window}')
            _answer.append("")
            print(f'_answer : {_answer}')
        else:
            window.pop(0)
            window.append(item[1])
            print(f'case 3, window : {window}')
            _answer.append(item[1])
            print(f'_answer : {_answer}')

    if item and item[0] < -1:
        heapq.heappush(heap, (item[0] + 1, item[1]))

    print(f'answer : {answer}')
    print(f'heap {heap}')
    print('___________________')
answer
# %%

# 책 풀이 분석
import collections
class Solution:
    def leastInterval(self, tasks: List[str], n: int) -> int:
        counter = collections.Counter(tasks)
        result = 0

        while True:
            sub_count = 0

            # 개수 많은 순서로 추출
            # (n + 1)개 만큼 추출하여 불필요한 idle 수를 제거
            for task, _ in counter.most_common(n + 1):
                sub_count += 1
                result += 1

                counter.subtract(task)
                # 0 이하인 아이템을 목록에서 완전히 제거
                counter += collections.Counter()

            if not counter:
                break

            # 필요한 idle 개수를 더함
            result += n - sub_count + 1

        return result


from conans import ConanFile, tools

class SFTPAdapterInterfaceConan(ConanFile):
	name = "SFTPAdapterInterface"
	description = "Interface of library-agnostic API for C++ to work with SFTP"
	author = "CSW <csw@werfen.com>"
	topics = ("conan", "sftp", "adapter", "wrapper")
	license = "MIT"
	generators = "visual_studio"
	no_copy_source = True
	# No settings/options are necessary, this is header only

	def package(self):
		self.copy("*.h", dst="include/SFTPAdapterInterface", src=".")

	def package_id(self):
		self.info.header_only()


"""Multiple TQDM progress bar manager singleton class"""
import random
from contextlib import contextmanager
from tqdm import tqdm

class Mtqdm():
    """Manage multiple nested tqdm progress bars with optional auto-coloring"""
    def __new__(cls, use_color : bool=True, palette : str="default"):
        if not hasattr(cls, 'instance'):
            cls.instance = super(Mtqdm, cls).__new__(cls)
            cls.instance.init(use_color, palette)
        return cls.instance

    MAX_BARS = 9
    MAX_COLORS = MAX_BARS

    colors = {
        "red" : "#AF0000",
        "orange" : "#AF5F00",
        "yellow" : "#AFAF00",
        "green" : "#00AF00",
        "blue" : "#0000AF",
        "purple" : "#5F00AF",
        "cyan" : "#00AFAF",
        "magenta" : "#AF00AF",
        "white" : "#AFAFAF"}

    alt_colors = {
        "red" : "#9F0000",
        "orange" : "#9F5700",
        "yellow" : "#9FAF00",
        "green" : "#009F00",
        "blue" : "#0000AF",
        "purple" : "#57009F",
        "cyan" : "#009F9F",
        "magenta" : "#9F009F",
        "white" : "#9F9F9F"}

    subdued_colors = {
        "red" : "#ffb3b3",
        "orange" : "#ffddb3",
        "yellow" : "#ffffb3",
        "green" : "#b3ffb3",
        "blue" : "#b3b3ff",
        "purple" : "#ddb3ff",
        "cyan" : "#b3ffff",
        "magenta" : "#ffb3ff",
        "white" : "#d9d9d9"}

    alt_subdued_colors = {
        "red" : "#ffcccc",
        "orange" : "#ffe8cc",
        "yellow" : "#ffffcc",
        "green" : "#ccffcc",
        "blue" : "#ccccff",
        "purple" : "#e8ccff",
        "cyan" : "#ccffff",
        "magenta" : "#ffccff",
        "white" : "#e6e6e6"}

    default_palette = [
        colors["green"],
        colors["yellow"],
        colors["orange"],
        colors["red"],
        colors["blue"],
        colors["purple"],
        colors["magenta"],
        colors["cyan"],
        colors["white"]]

    default_palette_alt = [
        alt_colors["green"],
        alt_colors["yellow"],
        alt_colors["orange"],
        alt_colors["red"],
        alt_colors["blue"],
        alt_colors["purple"],
        alt_colors["magenta"],
        alt_colors["cyan"],
        alt_colors["white"]]

    rainbow_palette = [
        colors["red"],
        colors["orange"],
        colors["yellow"],
        colors["green"],
        colors["blue"],
        colors["purple"],
        colors["magenta"],
        colors["cyan"],
        colors["white"]]

    rainbow_palette_alt = [
        alt_colors["red"],
        alt_colors["orange"],
        alt_colors["yellow"],
        alt_colors["green"],
        alt_colors["blue"],
        alt_colors["purple"],
        alt_colors["magenta"],
        alt_colors["cyan"],
        alt_colors["white"]]

    bemine_palette = [
        subdued_colors["green"],
        subdued_colors["yellow"],
        subdued_colors["orange"],
        subdued_colors["red"],
        subdued_colors["blue"],
        subdued_colors["purple"],
        subdued_colors["magenta"],
        subdued_colors["cyan"],
        subdued_colors["white"]]

    bemine_palette_alt = [
        alt_subdued_colors["green"],
        alt_subdued_colors["yellow"],
        alt_subdued_colors["orange"],
        alt_subdued_colors["red"],
        alt_subdued_colors["blue"],
        alt_subdued_colors["purple"],
        alt_subdued_colors["magenta"],
        alt_subdued_colors["cyan"],
        alt_subdued_colors["white"]]

    palettes = {
        "default" : default_palette,
        "rainbow" : rainbow_palette,
        "bemine" : bemine_palette,
        "random" : default_palette.copy(),
        "randbemine" : bemine_palette.copy()
    }

    alt_palettes = {
        "default" : default_palette_alt,
        "rainbow" : rainbow_palette_alt,
        "bemine" : bemine_palette_alt,
        "random" : default_palette.copy(),
        "randbemine" : bemine_palette_alt.copy()
    }

    def init(self, use_color : bool=True, palette : str="default"):
        """Initialize the singleton class"""
        self.use_color = use_color
        self.current_palette = palette

        # tracks currently open bars
        self.entered_bars = [None for n in range(Mtqdm.MAX_BARS)]
        self.entered_count = 0

        # tracks totals for current open bars
        self.bar_totals = [0 for n in range(Mtqdm.MAX_BARS)]

        # seta up for first open bar
        self.position = -1
        self.color = -1
        self.leave = -1

        # alternates are dimmer, use brighter ones at first (less surprising)
        self.alternation = True

        # tracks whether messages were sent, to clear them on bar closing
        self.bar_message = [-1 for n in range(Mtqdm.MAX_BARS)]

        # tracks auto-totalling for bars
        self.bar_auto_total = [False for n in range(Mtqdm.MAX_BARS)]
        self.bar_updates = [0 for n in range(Mtqdm.MAX_BARS)]

    def reset(self):
        for index in range(Mtqdm.MAX_BARS - 1, -1, -1):
            if self.entered_bars[index]:
                self.leave_bar(self.entered_bars[index])
        self.init()

    @contextmanager
    def open_bar(self, total=100, desc="Calming...", leave=False, auto_total=False):
        try:
            bar = self.enter_bar(total=total, desc=desc, leave=leave, auto_total=auto_total)
            yield bar
        finally:
            self.leave_bar(bar)

    def enter_bar(self, total=100, desc="Calming...", leave=False, auto_total=False):
        """Open a new bar"""
        if self.entered_count >= self.MAX_BARS:
            raise ValueError(f"The maximum number of bars {self.MAX_BARS} has been reached")
        position = self._enter_position()
        leave = self._enter_leave(leave)
        color = self._enter_color()

        if self.use_color:
            bar = tqdm(total=total, desc=desc, position=position, leave=leave, colour=color)
        else:
            bar = tqdm(total=total, desc=desc, position=position, leave=leave)

        self.entered_bars[position] = bar
        self.entered_count += 1
        self.bar_totals[position] = total
        self.bar_message[position] = -1
        self.bar_auto_total[position] = auto_total
        self.bar_updates[position] = 0
        return bar

    def leave_bar(self, bar):
        """Close a previously opened bar"""
        position = self._find_bar_position(bar)
        if position != None:
            self._leave_color()
            self._leave_leave()
            self._leave_position()

            # if a message was displayed, clear it
            bar_message_pos = self.bar_message[position]
            if bar_message_pos >= 0:
                bar.display("", bar_message_pos)
                self.bar_message[position] = -1

            # if auto totalling
            if self.bar_auto_total[position]:
                remainder = self.bar_totals[position] - self.bar_updates[position]
                self.update_bar(bar, steps=remainder)

            bar.close()
            self.entered_bars[position] = None
            self.entered_count -= 1
            self.bar_totals[position] = 0
            self.bar_message[position] = -1
            self.bar_auto_total[position] = False
            self.bar_updates[position] = 0

    def set_use_color(self, use_color):
        """True to use colorful bars, False to use default bars,
        set at the singleton level for newly opened bars"""
        self.use_color = use_color

    def set_palette(self, name : str):
        """Set the name of the current color palette to use"""
        self.current_palette = name

    def get_palette(self) -> str:
        """Get the name of the current color palette in use"""
        return self.current_palette

    def message(self, bar, message=""):
        position = self._find_bar_position(bar)
        self.bar_message[position] = position + 1
        if self.use_color:
            palette = self._get_palette(self.current_palette)
            color = palette[position]
            message = self._webcolor_text(message, color)
        bar.display(message, position + 1)

    def update_bar(self, bar, steps=1):
        """Update a bar's progress"""
        position = self._find_bar_position(bar)
        current_progress = self.bar_updates[position]
        new_progress = current_progress + steps
        progress_diff = new_progress - current_progress

        # don't let positive updates go above total
        total = self.bar_totals[position]
        if new_progress > total:
            steps = total - current_progress

        # don't let negative updates go below zero
        if new_progress < 0:
            steps = -current_progress

        new_progress = current_progress + steps
        self.bar_updates[position] += steps
        bar.update(n=steps)

        # negative and 100% updates don't refresh automatically
        if progress_diff <= 0 or new_progress == total:
        # if progress_diff < 0:
            bar.refresh()

    def get_bar(self, index):
        return self.entered_bars[index]

    def get_bar_max(self, index):
        """Get the total for a bar by index"""
        return self.bar_totals[index]

    def _get_palette(self, name : str):
        palette_set = self.alt_palettes if self.alternation else self.palettes
        return palette_set.get(name, palette_set.get("default"))

    def _get_position(self):
        return self.position

    def _enter_position(self):
        self.position += 1
        return self._get_position()

    def _leave_position(self):
        self.position -= 1

    def _get_color(self):
        palette = self._get_palette(self.current_palette)
        return palette[self.color]

    def _enter_color(self):
        if self.current_palette in ["random", "randbemine"]:
            self._manage_random_palette()
        else:
            self._manage_color_alternation()
        self.color += 1
        return self._get_color()

    def _leave_color(self):
        self.color -= 1

    def _toggle_color_alternation(self):
        self.alternation = not self.alternation

    def _manage_color_alternation(self):
        if self._get_position() == 0:
            self._toggle_color_alternation()

    def _manage_random_palette(self):
        if self._get_position() == 0:
            random.shuffle(self.palettes[self.current_palette])
            self.alt_palettes[self.current_palette] = self.palettes[self.current_palette]

    def _get_leave(self, override):
        return override or self.leave == 0

    def _enter_leave(self, override):
        self.leave += 1
        return self._get_leave(override)

    def _leave_leave(self):
        self.leave -= 1

    # bar display position is managed to be the same as the index into bar lists
    def _find_bar_position(self, bar):
        try:
            return self.entered_bars.index(bar)
        except ValueError:
            return None

    RGBSTART = "\x1b[38;2;"
    RGBEND = "m"
    RGBCLOSE = "\x1b[0m"

    def _webcolor_text(self, text : str, color : str) -> str:
        assert color.startswith("#") and len(color) == 7
        red = int(color[1:3], base=16)
        green = int(color[3:5], base=16)
        blue = int(color[5:7], base=16)
        color = f"{red};{green};{blue}"
        return f"{self.RGBSTART}{color}{self.RGBEND}{text.strip()}{self.RGBCLOSE}"


import os
import sys

def bool_env(varname: str, default: bool) -> bool:
  """Read an environment variable and interpret it as a boolean.

  True values are (case insensitive): 'y', 'yes', 't', 'true', 'on', and '1';
  false values are 'n', 'no', 'f', 'false', 'off', and '0'.

  Args:
    varname: the name of the variable
    default: the default boolean value
  Raises: ValueError if the environment variable is anything else.
  """
  val = os.getenv(varname, str(default))
  val = val.lower()
  if val in ('y', 'yes', 't', 'true', 'on', '1'):
    return True
  elif val in ('n', 'no', 'f', 'false', 'off', '0'):
    return False
  else:
    raise ValueError(
        'invalid truth value %r for environment %r' % (val, varname)
    )

class Config(object):

  def __init__(self):
    self.values = {}
    self.meta = {}
    self.FLAGS = NameSpace(self.read)
    self.use_absl = False

  def update(self, name, val):
    if self.use_absl:
      setattr(self.absl_flags.FLAGS, name, val)
    else:
      self.check_exists(name)
      if name not in self.values:
        raise Exception("Unrecognized config option: {}".format(name))
      self.values[name] = val

  def read(self, name):
    if self.use_absl:
      return getattr(self.absl_flags.FLAGS, name)
    else:
      self.check_exists(name)
      return self.values[name]

  def add_option(self, name, default, opt_type, meta_args, meta_kwargs):
    if name in self.values:
      raise Exception("Config option {} already defined".format(name))
    self.values[name] = default
    self.meta[name] = (opt_type, meta_args, meta_kwargs)

  def check_exists(self, name):
    if name not in self.values:
      raise Exception("Unrecognized config option: {}".format(name))

  def DEFINE_bool(self, name, default, *args, **kwargs):
    self.add_option(name, default, bool, args, kwargs)

  def DEFINE_integer(self, name, default, *args, **kwargs):
    self.add_option(name, default, int, args, kwargs)

  def DEFINE_string(self, name, default, *args, **kwargs):
    self.add_option(name, default, str, args, kwargs)

  def DEFINE_enum(self, name, default, *args, **kwargs):
    self.add_option(name, default, 'enum', args, kwargs)

  def config_with_absl(self):
    # Run this before calling `app.run(main)` etc
    import absl.flags as absl_FLAGS
    from absl import app, flags as absl_flags

    self.use_absl = True
    self.absl_flags = absl_flags
    absl_defs = { bool: absl_flags.DEFINE_bool,
                  int:  absl_flags.DEFINE_integer,
                  str:  absl_flags.DEFINE_string,
                  'enum': absl_flags.DEFINE_enum }

    for name, val in self.values.items():
      flag_type, meta_args, meta_kwargs = self.meta[name]
      absl_defs[flag_type](name, val, *meta_args, **meta_kwargs)

    app.call_after_init(lambda: self.complete_absl_config(absl_flags))

  def complete_absl_config(self, absl_flags):
    for name, _ in self.values.items():
      self.update(name, getattr(absl_flags.FLAGS, name))

  def parse_flags_with_absl(self):
    global already_configured_with_absl
    if not already_configured_with_absl:
      import absl.flags
      self.config_with_absl()
      absl.flags.FLAGS(sys.argv, known_only=True)
      self.complete_absl_config(absl.flags)
      already_configured_with_absl = True

class NameSpace(object):
  def __init__(self, getter):
    self._getter = getter

  def __getattr__(self, name):
    return self._getter(name)

config = Config()
flags = config
FLAGS = flags.FLAGS

already_configured_with_absl = False

flags.DEFINE_bool(
    'jax_enable_checks',
    bool_env('JAX_ENABLE_CHECKS', False),
    help='Turn on invariant checking (core.skip_checks = False)')

flags.DEFINE_bool('tf_numpy_additional_tests', True,
                  'Run tests added specifically for TF numpy')

{'absl_FLAGS': 'absl.flags', 'app': 'absl.app', 'absl_flags': 'absl.flags', 'absl.flags': 'absl.flags'}


import sys

assert sys.version_info > (3, 9)

from dataclasses import KW_ONLY, dataclass

import hypothesis.strategies as st
import pytest
from hypothesis import assume, given

from hydra_zen import builds, instantiate, just, make_config
from hydra_zen.structured_configs._type_guards import safe_getattr
from hydra_zen.typing import DataclassOptions as Dc
from tests.custom_strategies import valid_builds_args

sl = Dc(slots=True)

@dataclass
class Point:
    x: float
    _: KW_ONLY
    y: float
    z: float

def test_just_on_dataclass_w_kwonly_field():
    pt = Point(0, y=1.5, z=2.0)
    assert instantiate(just(pt)) == pt

def test_builds_on_dataclass_w_kwonly_field():
    pt = Point(0, y=1.5, z=2.0)
    Conf = builds(Point, populate_full_signature=True)
    assert instantiate(Conf(0, y=1.5, z=2.0)) == pt

@pytest.mark.parametrize(
    "obj,field,expected",
    [
        (builds(int, x=2, zen_dataclass=Dc(slots=True)), "x", 2),
        (builds(int, zen_dataclass=Dc(slots=True)), "_target_", "builtins.int"),
        (
            builds(int, zen_partial=True, zen_dataclass=Dc(slots=True)),
            "_partial_",
            True,
        ),
    ],
)
def test_safe_getattr_with_slots(obj, field, expected):
    assert safe_getattr(obj, field) == expected

def func(x):
    ...

@pytest.mark.filterwarnings("ignore:A structured config was supplied for")
@given(kw=valid_builds_args(), as_inst=..., x=st.sampled_from(["", 1, [1, 2]]))
def test_equiv_getattr(kw, as_inst: bool, x):
    obj = builds(func, x=x, **kw)

    if "zen_dataclass" in kw and kw["zen_dataclass"] is not None:
        dc_opts = kw["zen_dataclass"]
        if dc_opts.get("slots", False) and not dc_opts.get("init", True):
            # I think @dataclass(slots=True, init=False) has buggy
            # behavior!!
            assume(False)

        kw["zen_dataclass"]["slots"] = False
        kw["zen_dataclass"]["weakref_slot"] = False

    no_slot_obj = builds(func, x=x, **kw)

    if as_inst:
        obj = obj()
        no_slot_obj = no_slot_obj()

    try:
        getattr(no_slot_obj, "x")
    except AttributeError:
        with pytest.raises(AttributeError):
            safe_getattr(obj, "x")
        return

    actual = safe_getattr(obj, "x")

    assert x == actual

def test_safe_getattr_no_default():
    conf = make_config("x", zen_dataclass={"slots": True})

    with pytest.raises(AttributeError):
        safe_getattr(conf, "x")

    assert safe_getattr(conf, "x", 1) == 1


import numpy as np
import torch
from functools import reduce
from collections import Counter
import scipy as sp
from sklearn.preprocessing import normalize

class Mesh:
    def __init__(self, path, build_mat=False):
        self.path = path
        self.vs, self.faces = self.fill_from_file(path)
        self.compute_face_normals()
        self.compute_face_center()
        self.device = 'cpu'
        self.build_gemm() #self.edges, self.ve
        self.compute_vert_normals()
        self.build_v2v()
        self.build_vf()
        if build_mat:
            self.build_uni_lap()
            self.build_mesh_lap()

    def fill_from_file(self, path):
        vs, faces = [], []
        f = open(path)
        for line in f:
            line = line.strip()
            splitted_line = line.split()
            if not splitted_line:
                continue
            elif splitted_line[0] == 'v':
                vs.append([float(v) for v in splitted_line[1:4]])
            elif splitted_line[0] == 'f':
                face_vertex_ids = [int(c.split('/')[0]) for c in splitted_line[1:]]
                assert len(face_vertex_ids) == 3
                face_vertex_ids = [(ind - 1) if (ind >= 0) else (len(vs) + ind) for ind in face_vertex_ids]
                faces.append(face_vertex_ids)
        f.close()
        vs = np.asarray(vs)
        faces = np.asarray(faces, dtype=int)

        assert np.logical_and(faces >= 0, faces < len(vs)).all()
        return vs, faces

    def build_gemm(self):
        self.ve = [[] for _ in self.vs]
        self.vei = [[] for _ in self.vs]
        edge_nb = []
        sides = []
        edge2key = dict()
        edges = []
        edges_count = 0
        nb_count = []
        for face_id, face in enumerate(self.faces):
            faces_edges = []
            for i in range(3):
                cur_edge = (face[i], face[(i + 1) % 3])
                faces_edges.append(cur_edge)
            for idx, edge in enumerate(faces_edges):
                edge = tuple(sorted(list(edge)))
                faces_edges[idx] = edge
                if edge not in edge2key:
                    edge2key[edge] = edges_count
                    edges.append(list(edge))
                    edge_nb.append([-1, -1, -1, -1])
                    sides.append([-1, -1, -1, -1])
                    self.ve[edge[0]].append(edges_count)
                    self.ve[edge[1]].append(edges_count)
                    self.vei[edge[0]].append(0)
                    self.vei[edge[1]].append(1)
                    nb_count.append(0)
                    edges_count += 1
            for idx, edge in enumerate(faces_edges):
                edge_key = edge2key[edge]
                edge_nb[edge_key][nb_count[edge_key]] = edge2key[faces_edges[(idx + 1) % 3]]
                edge_nb[edge_key][nb_count[edge_key] + 1] = edge2key[faces_edges[(idx + 2) % 3]]
                nb_count[edge_key] += 2
            for idx, edge in enumerate(faces_edges):
                edge_key = edge2key[edge]
                sides[edge_key][nb_count[edge_key] - 2] = nb_count[edge2key[faces_edges[(idx + 1) % 3]]] - 1
                sides[edge_key][nb_count[edge_key] - 1] = nb_count[edge2key[faces_edges[(idx + 2) % 3]]] - 2
        self.edges = np.array(edges, dtype=np.int32)
        self.gemm_edges = np.array(edge_nb, dtype=np.int64)
        self.sides = np.array(sides, dtype=np.int64)
        self.edges_count = edges_count

    def compute_face_normals(self):
        face_normals = np.cross(self.vs[self.faces[:, 1]] - self.vs[self.faces[:, 0]], self.vs[self.faces[:, 2]] - self.vs[self.faces[:, 0]])
        norm = np.linalg.norm(face_normals, axis=1, keepdims=True) + 1e-24
        face_areas = 0.5 * np.sqrt((face_normals**2).sum(axis=1))
        face_normals /= norm
        self.fn, self.fa = face_normals, face_areas

    def compute_vert_normals(self):
        vert_normals = np.zeros((3, len(self.vs)))
        face_normals = self.fn
        faces = self.faces

        nv = len(self.vs)
        nf = len(faces)
        mat_rows = faces.reshape(-1)
        mat_cols = np.array([[i] * 3 for i in range(nf)]).reshape(-1)
        mat_vals = np.ones(len(mat_rows))
        f2v_mat = sp.sparse.csr_matrix((mat_vals, (mat_rows, mat_cols)), shape=(nv, nf))
        vert_normals = sp.sparse.csr_matrix.dot(f2v_mat, face_normals)
        vert_normals = normalize(vert_normals, norm='l2', axis=1)
        self.vn = vert_normals

    def compute_face_center(self):
        faces = self.faces
        vs = self.vs
        self.fc = np.sum(vs[faces], 1) / 3.0

    def build_uni_lap(self):
        """compute uniform laplacian matrix"""
        vs = torch.tensor(self.vs.T, dtype=torch.float)
        edges = self.edges
        ve = self.ve

        sub_mesh_vv = [edges[v_e, :].reshape(-1) for v_e in ve]
        sub_mesh_vv = [set(vv.tolist()).difference(set([i])) for i, vv in enumerate(sub_mesh_vv)]

        num_verts = vs.size(1)
        mat_rows = [np.array([i] * len(vv), dtype=np.int64) for i, vv in enumerate(sub_mesh_vv)]
        mat_rows = np.concatenate(mat_rows)
        mat_cols = [np.array(list(vv), dtype=np.int64) for vv in sub_mesh_vv]
        mat_cols = np.concatenate(mat_cols)

        mat_rows = torch.from_numpy(mat_rows).long()
        mat_cols = torch.from_numpy(mat_cols).long()
        mat_vals = torch.ones_like(mat_rows).float() * -1.0
        neig_mat = torch.sparse.FloatTensor(torch.stack([mat_rows, mat_cols], dim=0),
                                            mat_vals,
                                            size=torch.Size([num_verts, num_verts]))
        vs = vs.T

        sum_count = torch.sparse.mm(neig_mat, torch.ones((num_verts, 1)).type_as(vs))
        mat_rows_ident = np.array([i for i in range(num_verts)])
        mat_cols_ident = np.array([i for i in range(num_verts)])
        mat_ident = np.array([-s for s in sum_count[:, 0]])
        mat_rows_ident = torch.from_numpy(mat_rows_ident).long()
        mat_cols_ident = torch.from_numpy(mat_cols_ident).long()
        mat_ident = torch.from_numpy(mat_ident).long()
        mat_rows = torch.cat([mat_rows, mat_rows_ident])
        mat_cols = torch.cat([mat_cols, mat_cols_ident])
        mat_vals = torch.cat([mat_vals, mat_ident])

        self.lapmat = torch.sparse.FloatTensor(torch.stack([mat_rows, mat_cols], dim=0),
                                            mat_vals,
                                            size=torch.Size([num_verts, num_verts]))

    def build_vf(self):
        vf = [set() for _ in range(len(self.vs))]
        for i, f in enumerate(self.faces):
            vf[f[0]].add(i)
            vf[f[1]].add(i)
            vf[f[2]].add(i)
        self.vf = vf

        """ build vertex-to-face sparse matrix """
        v2f_inds = [[] for _ in range(2)]
        v2f_vals = []
        v2f_areas = [[] for _ in range(len(self.vs))]
        for i in range(len(vf)):
            v2f_inds[1] += list(vf[i])
            v2f_inds[0] += [i] * len(vf[i])
            v2f_vals += (self.fc[list(vf[i])] - self.vs[i].reshape(1, -1)).tolist()
            v2f_areas[i] = np.sum(self.fa[list(vf[i])])
        self.v2f_list = [v2f_inds, v2f_vals, v2f_areas]

        v2f_inds = torch.tensor(v2f_inds).long()
        v2f_vals = torch.ones(v2f_inds.shape[1]).float()
        self.v2f_mat = torch.sparse.FloatTensor(v2f_inds, v2f_vals, size=torch.Size([len(self.vs), len(self.faces)]))

        """ build face-to-face (1ring) matrix """
        f2f = [[] for _ in range(len(self.faces))]
        self.f_edges = [[] for _ in range(2)]
        for i, f in enumerate(self.faces):
            all_neig = list(vf[f[0]]) + list(vf[f[1]]) + list(vf[f[2]])
            one_neig = np.array(list(Counter(all_neig).values())) == 2
            f2f_i = np.array(list(Counter(all_neig).keys()))[one_neig].tolist()
            self.f_edges[0] += len(f2f_i) * [i]
            self.f_edges[1] += f2f_i
            f2f[i] = f2f_i + (3 - len(f2f_i)) * [-1]

        self.f2f = np.array(f2f)
        self.f_edges = np.array(self.f_edges)

    def build_v2v(self):
        """ compute adjacent matrix """
        edges = self.edges
        v2v_inds = edges.T
        v2v_inds = torch.from_numpy(np.concatenate([v2v_inds, v2v_inds[[1, 0]]], axis=1)).long()
        v2v_vals = torch.ones(v2v_inds.shape[1]).float()
        self.v2v_mat = torch.sparse.FloatTensor(v2v_inds, v2v_vals, size=torch.Size([len(self.vs), len(self.vs)]))
        # self.v_dims = torch.sum(self.v2v_mat.to_dense(), axis=1)
        self.v_dims = torch.sparse.sum(self.v2v_mat, dim=1).to_dense()

    def build_mesh_lap(self):
        """compute mesh laplacian matrix"""
        vs = self.vs
        vf = self.vf
        fa = self.fa
        edges = self.edges
        faces = self.faces
        e_dict = {}

        for e in edges:
            e0 = min(e)
            e1 = max(e)
            e_dict[(e0, e1)] = []

        for f in faces:
            s = vs[f[1]] - vs[f[0]]
            t = vs[f[2]] - vs[f[1]]
            u = vs[f[0]] - vs[f[2]]
            cos_0 = np.inner(s, -u) / (np.linalg.norm(s) * np.linalg.norm(u))
            cos_1 = np.inner(t, -s) / (np.linalg.norm(t) * np.linalg.norm(s))
            cos_2 = np.inner(u, -t) / (np.linalg.norm(u) * np.linalg.norm(t))
            cot_0 = cos_0 / (np.sqrt(1 - cos_0 ** 2) + 1e-12)
            cot_1 = cos_1 / (np.sqrt(1 - cos_1 ** 2) + 1e-12)
            cot_2 = cos_2 / (np.sqrt(1 - cos_2 ** 2) + 1e-12)
            key_0 = (min(f[1], f[2]), max(f[1], f[2]))
            key_1 = (min(f[2], f[0]), max(f[2], f[0]))
            key_2 = (min(f[0], f[1]), max(f[0], f[1]))
            e_dict[key_0].append(cot_0)
            e_dict[key_1].append(cot_1)
            e_dict[key_2].append(cot_2)

        for e in e_dict:
            e_dict[e] = -0.5 * (e_dict[e][0] + e_dict[e][1])

        C_ind = [[], []]
        C_val = []
        ident = [0] * len(vs)
        for e in e_dict:
            C_ind[0].append(e[0])
            C_ind[1].append(e[1])
            C_ind[0].append(e[1])
            C_ind[1].append(e[0])
            C_val.append(e_dict[e])
            C_val.append(e_dict[e])
            ident[e[0]] += -1.0 * e_dict[e]
            ident[e[1]] += -1.0 * e_dict[e]
        for i in range(len(vs)):
            C_ind[0].append(i)
            C_ind[1].append(i)
        C_val = C_val + ident
        C_ind = torch.LongTensor(C_ind)
        C_val = torch.FloatTensor(C_val)
        # cotangent matrix
        C = torch.sparse.FloatTensor(C_ind, C_val, torch.Size([len(vs), len(vs)]))
        self.cot_mat = C

        M_ind = torch.stack([torch.arange(len(vs)), torch.arange(len(vs))], dim=0).long()
        M_val = []
        for v in range(len(vs)):
            faces = list(vf[v])
            va = 3.0 / (sum(fa[faces]) + 1e-12)
            M_val.append(va)
        M_val = torch.FloatTensor(M_val)
        # diagonal mass inverse matrix
        Minv = torch.sparse.FloatTensor(M_ind, M_val, torch.Size([len(vs), len(vs)]))
        C = torch.sparse.mm(Minv, C.to_dense()).to_sparse()
        self.mesh_lap = C

    def save(self, filename):
        assert len(self.vs) > 0
        vertices = np.array(self.vs, dtype=np.float32).flatten()
        indices = np.array(self.faces, dtype=np.uint32).flatten()

        with open(filename, 'w') as fp:
            # Write positions
            for i in range(0, vertices.size, 3):
                x = vertices[i + 0]
                y = vertices[i + 1]
                z = vertices[i + 2]
                fp.write('v {0:.8f} {1:.8f} {2:.8f}\n'.format(x, y, z))

            # Write indices
            for i in range(0, len(indices), 3):
                i0 = indices[i + 0] + 1
                i1 = indices[i + 1] + 1
                i2 = indices[i + 2] + 1
                fp.write('f {0} {1} {2}\n'.format(i0, i1, i2))

    def save_as_ply(self, filename, fn):
        assert len(self.vs) > 0
        vertices = np.array(self.vs, dtype=np.float32).flatten()
        indices = np.array(self.faces, dtype=np.uint32).flatten()
        fnormals = np.array(fn, dtype=np.float32).flatten()

        with open(filename, 'w') as fp:
            # Write Header
            fp.write("ply\nformat ascii 1.0\nelement vertex {}\n".format(len(self.vs)))
            fp.write("property float x\nproperty float y\nproperty float z\n")
            fp.write("element face {}\n".format(len(self.faces)))
            fp.write("property list uchar int vertex_indices\n")
            fp.write("property uchar red\nproperty uchar green\nproperty uchar blue\nproperty uchar alpha\n")
            fp.write("end_header\n")
            for i in range(0, vertices.size, 3):
                x = vertices[i + 0]
                y = vertices[i + 1]
                z = vertices[i + 2]
                fp.write("{0:.6f} {1:.6f} {2:.6f}\n".format(x, y, z))

            for i in range(0, len(indices), 3):
                i0 = indices[i + 0]
                i1 = indices[i + 1]
                i2 = indices[i + 2]
                c0 = fnormals[i + 0]
                c1 = fnormals[i + 1]
                c2 = fnormals[i + 2]
                c0 = np.clip(int(255 * c0), 0, 255)
                c1 = np.clip(int(255 * c1), 0, 255)
                c2 = np.clip(int(255 * c2), 0, 255)
                c3 = 255
                fp.write("3 {0} {1} {2} {3} {4} {5} {6}\n".format(i0, i1, i2, c0, c1, c2, c3))


from datetime import date, datetime, timedelta
try:
    from .song_history import load_df
    from .song_history import  get_durations
except:
    from song_history import load_df
    from song_history import  get_durations
import pandas as pd
import plotly.offline as opy
pd.options.plotting.backend = "plotly"

def to_html(figure):
    context = {}
    div = opy.plot(figure, auto_open=False, output_type='div')
    context['plot'] = div
    return context

def artist_duration(n=37):
    df, mdf = load_df()
    df = select_month(df, 7)

    durations = get_durations(list(set(df.id)))
    df = df.merge(durations, on='id', how='left')

    df_artist_track = df.groupby(['artist', 'id'])
    duration = df_artist_track['duration'].sum()
    count = df_artist_track.count()['played_at']

    total_time_artist = (count*duration).groupby('artist').sum()
    total_time_artist.sort_values(ascending=True, inplace=True)

    top_artists_ms = total_time_artist.tail(n)

    top_artists = top_artists_ms//(1000*60)  # minutes:1000*60   hours:1000*60*60

    top_artists = pd.DataFrame(top_artists).reset_index()
    top_artists.columns=['artist', 'time']
    top_artists.rename(columns={"time":"minutes"}, inplace=True)
    top_artists.index = [f'Rank: {n-i}' for i in range(n)]

    figure = top_artists.plot.barh(
        x = 'minutes',
        y = 'artist',
        color='minutes',
        hover_name = top_artists.index,
        template='plotly_dark'
    )
    figure.update(layout_showlegend=False)

    return to_html(figure)

def song_plays(n=37):
    print('Downloading to df()...')
    df, mdf = load_df()
    df = select_month(df, 7)

    name_artist = df.groupby(['name', 'artist', 'id'])  # TODO: maybe ids in front?
    _counts = name_artist.size().reset_index(name='play_count')
    sorted_counts = _counts.sort_values('play_count', ascending=True)
    top_songs = sorted_counts.tail(n)

    # ############# this should all be inside a duration f #############
    durations = get_durations(top_songs.id.unique())
    df = top_songs.merge(durations, how='left', on='id')

    df["minutes"] = (df.play_count * df.duration)/(1000*60)  # mintues:1000*60   hours:1000*60*60
    df.sort_values("minutes", ascending=True, inplace=True)

    df.index = ['Rank: '+str(n-i) for i in range(n)]

    df.rename(columns={"play_count":"count"}, inplace=True)

    #############HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH###################

    figure = df.plot.barh(
        y = 'name',
        x = 'minutes',
        labels = {
            'artist':'Artist',
            'name': 'Track',
        },
        custom_data = ['artist', 'name'],
        color='count',
        hover_name = df.index,
        hover_data=['artist', 'name', 'count'],
        template='plotly_dark'
    )
    figure.update_layout(
        showlegend=False
    )
    return to_html(figure)

def select_month(df, months=6):
    n_months_ago = (date.today() - timedelta(days=months*30)).strftime("%Y-%m-%dT00:00:00.000Z")
    return df[df.played_at > n_months_ago]

#### other tools #####

def show_named_plotly_colours():
    """
    function to display to user the colours to match plotly's named
    css colours.

    Reference:
        #https://community.plotly.com/t/plotly-colours-list/11730/3

    Returns:
        plotly dataframe with cell colour to match named colour name

    """
    s='''
        aliceblue, antiquewhite, aqua, aquamarine, azure,
        beige, bisque, black, blanchedalmond, blue,
        blueviolet, brown, burlywood, cadetblue,
        chartreuse, chocolate, coral, cornflowerblue,
        cornsilk, crimson, cyan, darkblue, darkcyan,
        darkgoldenrod, darkgray, darkgrey, darkgreen,
        darkkhaki, darkmagenta, darkolivegreen, darkorange,
        darkorchid, darkred, darksalmon, darkseagreen,
        darkslateblue, darkslategray, darkslategrey,
        darkturquoise, darkviolet, deeppink, deepskyblue,
        dimgray, dimgrey, dodgerblue, firebrick,
        floralwhite, forestgreen, fuchsia, gainsboro,
        ghostwhite, gold, goldenrod, gray, grey, green,
        greenyellow, honeydew, hotpink, indianred, indigo,
        ivory, khaki, lavender, lavenderblush, lawngreen,
        lemonchiffon, lightblue, lightcoral, lightcyan,
        lightgoldenrodyellow, lightgray, lightgrey,
        lightgreen, lightpink, lightsalmon, lightseagreen,
        lightskyblue, lightslategray, lightslategrey,
        lightsteelblue, lightyellow, lime, limegreen,
        linen, magenta, maroon, mediumaquamarine,
        mediumblue, mediumorchid, mediumpurple,
        mediumseagreen, mediumslateblue, mediumspringgreen,
        mediumturquoise, mediumvioletred, midnightblue,
        mintcream, mistyrose, moccasin, navajowhite, navy,
        oldlace, olive, olivedrab, orange, orangered,
        orchid, palegoldenrod, palegreen, paleturquoise,
        palevioletred, papayawhip, peachpuff, peru, pink,
        plum, powderblue, purple, red, rosybrown,
        royalblue, saddlebrown, salmon, sandybrown,
        seagreen, seashell, sienna, silver, skyblue,
        slateblue, slategray, slategrey, snow, springgreen,
        steelblue, tan, teal, thistle, tomato, turquoise,
        violet, wheat, white, whitesmoke, yellow,
        yellowgreen
        '''
    li=s.split(',')
    li=[l.replace('\n','') for l in li]
    li=[l.replace(' ','') for l in li]

    import pandas as pd
    import plotly.graph_objects as go

    df=pd.DataFrame.from_dict({'colour': li})
    fig = go.Figure(data=[go.Table(
      header=dict(
        values=["Plotly Named CSS colours"],
        line_color='black', fill_color='white',
        align='center', font=dict(color='black', size=14)
      ),
      cells=dict(
        values=[df.colour],
        line_color=[df.colour], fill_color=[df.colour],
        align='center', font=dict(color='black', size=11)
      ))
    ])

    fig.show()

######################

'''
Checklist of things to do:
    X hours per artist
    X songs plays with color
    - genre plots?
'''

if __name__=='__main__':
    # print('Downloading to df()...')
    # df, mdf = load_df()

    # artist_duration()
    song_plays()

    print('end')


import matplotlib.pyplot as plt
import pyNN.spiNNaker as sim
from spynnaker.pyNN.utilities import neo_convertor

# number of neurons in each population
n_neurons = 100
n_populations = 10
weights = 0.5
delays = 17.0
simtime = 1000

sim.setup(timestep=1.0, min_delay=1.0)

spikeArray = {'spike_times': [[0]]}
stimulus = sim.Population(1, sim.SpikeSourceArray, spikeArray,
                          label='stimulus')

chain_pops = [
    sim.Population(n_neurons, sim.IF_curr_exp, {}, label='chain_{}'.format(i))
    for i in range(n_populations)
]
for pop in chain_pops:
    pop.record("spikes")

connector = sim.FixedNumberPreConnector(10)
for i in range(n_populations):
    sim.Projection(chain_pops[i], chain_pops[(i + 1) % n_populations],
                   connector,
                   synapse_type=sim.StaticSynapse(weight=weights,
                                                  delay=delays))

sim.Projection(stimulus, chain_pops[0], sim.AllToAllConnector(),
               synapse_type=sim.StaticSynapse(weight=5.0))

sim.run(simtime)
# None PyNN method which is faster
# spikes = [pop.spinnaker_get_data("spikes") for pop in chain_pops]

# Pynn method and support method
neos = [pop.get_data("spikes") for pop in chain_pops]
spikes = map(neo_convertor.convert_spikes, neos)

sim.end()

try:
    plt.figure()
    plt.xlabel('Time (ms)')
    plt.ylabel('Neuron')
    plt.title('Spikes Sent By Chain')
    offset = 0
    for pop_spikes in spikes:
        plt.plot(
            [i[1] for i in pop_spikes],
            [i[0] + offset for i in pop_spikes], "."
        )
        offset += n_neurons
    plt.show()
    # pylab.savefig("results.png")
except Exception as ex:
    print(spikes)
    raise ex


from typing import Any, Dict, List, Optional, Tuple
from torch import Tensor
import torch
import torch.nn.functional as F
from fairseq.models.fairseq_encoder import EncoderOut
from fairseq.models.transformer import (
    TransformerModel,
    TransformerEncoder,
    TransformerDecoder,
)
from fairseq.models import (
    register_model,
    register_model_architecture,
)

from knnbox.common_utils import (
    global_vars,
    select_keys_with_pad_mask,
    archs,
    disable_model_grad,
    enable_module_grad,
)
from knnbox.datastore import Datastore, PckDatastore
from knnbox.retriever import Retriever
from knnbox.combiner import Combiner, AdaptiveCombiner
from .adaptive_knn_mt import AdaptiveKNNMT

@register_model("pck_knn_mt")
class PckKNNMT(AdaptiveKNNMT):
    r"""
    The  pck knn-mt model.
    """
    @staticmethod
    def add_args(parser):
        r"""
        add pck knn-mt related args here
        """
        AdaptiveKNNMT.add_args(parser)
        parser.add_argument("--knn-reduct-dim", type=int, metavar="N", default=64,
                            help="reducted dimension of datastore")

    @classmethod
    def build_decoder(cls, args, tgt_dict, embed_tokens):
        r"""
        we override this function, replace the TransformerDecoder with PckKNNMTDecoder
        """
        return PckKNNMTDecoder(
            args,
            tgt_dict,
            embed_tokens,
            no_encoder_attn=getattr(args, "no_cross_attention", False),
        )

class PckKNNMTDecoder(TransformerDecoder):
    r"""
    The pck knn-mt Decoder, equipped with knn datastore, retriever and combiner.
    """

    def __init__(self, args, dictionary, embed_tokens, no_encoder_attn=False):
        r"""
        we override this function to create knn-related module.
        In other words, create datastore, retriever and combiner.
        """
        super().__init__(args, dictionary, embed_tokens, no_encoder_attn)

        if args.knn_mode == "build_datastore":
            if "datastore" not in global_vars():
                # regist the datastore as a global variable if not exist,
                # because we need access the same datastore in another
                # python file (when traverse the dataset and `add value`)
                global_vars()["datastore"] = PckDatastore(
                        path=args.knn_datastore_path,
                        dictionary_len=len(self.dictionary),
                    )
            self.datastore = global_vars()["datastore"]

        else:
            self.datastore = PckDatastore.load(args.knn_datastore_path, load_list=["vals"], load_network=True)
            self.datastore.load_faiss_index("keys")
            self.retriever = Retriever(datastore=self.datastore, k=args.knn_max_k)
            if args.knn_mode == "train_metak":
                self.combiner = AdaptiveCombiner(max_k=args.knn_max_k, probability_dim=len(dictionary),
                            k_trainable=(args.knn_k_type=="trainable"),
                            lambda_trainable=(args.knn_lambda_type=="trainable"), lamda_=args.knn_lambda,
                            temperature_trainable=(args.knn_temperature_type=="trainable"), temperature=args.knn_temperature
            )
            elif args.knn_mode == "inference":
                self.combiner = AdaptiveCombiner.load(args.knn_combiner_path)

    def forward(
        self,
        prev_output_tokens,
        encoder_out: Optional[EncoderOut] = None,
        incremental_state: Optional[Dict[str, Dict[str, Optional[Tensor]]]] = None,
        features_only: bool = False,
        full_context_alignment: bool = False,
        alignment_layer: Optional[int] = None,
        alignment_heads: Optional[int] = None,
        src_lengths: Optional[Any] = None,
        return_all_hiddens: bool = False,
    ):
        r"""
        we overwrite this function to do something else besides forward the TransformerDecoder.

        when the action mode is `building datastore`, we save keys to datastore.
        when the action mode is `inference`, we retrieve the datastore with hidden state.
        """
        x, extra = self.extract_features(
            prev_output_tokens,
            encoder_out=encoder_out,
            incremental_state=incremental_state,
            full_context_alignment=full_context_alignment,
            alignment_layer=alignment_layer,
            alignment_heads=alignment_heads,
        )

        if self.args.knn_mode == "build_datastore":
            def get_4_gram(target):
                r"""
                Args:
                    target: [B, T]
                Return: [B, T, 4]
                """
                batch_size = target.size(0)
                target = target[:, :, None]
                target_pad_1 = torch.cat((torch.zeros((batch_size, 1, 1), device=x.device, dtype=torch.long), target[:, :-1]), 1)
                target_pad_2 = torch.cat((torch.zeros((batch_size, 2, 1), device=x.device, dtype=torch.long), target[:,:-2]), 1)
                target_pad_3 = torch.cat((torch.zeros((batch_size, 3, 1), device=x.device, dtype=torch.long), target[:,:-3]), 1)
                return torch.cat((target, target_pad_1, target_pad_2, target_pad_3), -1)

            def get_tgt_probs(probs, target):
                r"""
                Args:
                    probs: [B, T, dictionary]
                    target: [B, T]
                Return: [B, T]
                """
                B, T, C = probs.size(0), probs.size(1), probs.size(2)
                one_hot = torch.arange(0, C).to(target.device)[None, None].repeat(B, T, 1) == target[:, :, None]
                return (probs * one_hot.float()).sum(-1)

            def get_4_gram_probs(target_prob):
                r"""
                Args:
                    target_prob: [B, T]
                Return: [B, T, 4]
                """
                target_prob = target_prob[:, :, None]
                target_pad_1 = torch.cat((target_prob[:, :1].repeat(1, 1, 1), target_prob[:, :-1]), 1)
                target_pad_2 = torch.cat((target_prob[:, :1].repeat(1, 2, 1), target_prob[:, :-2]), 1)
                target_pad_3 = torch.cat((target_prob[:, :1].repeat(1, 3, 1), target_prob[:, :-3]), 1)
                return torch.cat((target_prob, target_pad_1,  target_pad_2, target_pad_3), -1)

            def get_entropy(probs):
                r"""probs: [B, T, dictionary]"""
                return - (probs * torch.log(probs+1e-7)).sum(-1)

            # calulate probs
            output_logit = self.output_layer(x)
            output_probs = F.softmax(output_logit, dim=-1)
            # get useful info
            target = self.datastore.get_target()
            ids_4_gram = get_4_gram(target) # [B, T, 4]
            target_prob = get_tgt_probs(output_probs, target) # [B, T]
            probs_4_gram = get_4_gram_probs(target_prob) # [B, T, 4]
            entropy = get_entropy(output_probs) # [B, T]
            # process pad
            pad_mask = self.datastore.get_pad_mask()
            keys = select_keys_with_pad_mask(x, pad_mask)
            ids_4_gram = select_keys_with_pad_mask(ids_4_gram, pad_mask)
            probs_4_gram = select_keys_with_pad_mask(probs_4_gram, pad_mask)
            entropy = entropy.masked_select(pad_mask)
            # save infomation to datastore
            self.datastore["keys"].add(keys.half())
            self.datastore["ids_4_gram"].add(ids_4_gram)
            self.datastore["probs_4_gram"].add(probs_4_gram)
            self.datastore["entropy"].add(entropy)

        elif self.args.knn_mode == "train_metak" or self.args.knn_mode == "inference":
            ## query with x (x needn't to be half precision),
            ## save retrieved `vals` and `distances`
            self.retriever.retrieve(self.datastore.vector_reduct(x), return_list=["vals", "distances"])

        if not features_only:
            x = self.output_layer(x)
        return x, extra

    def get_normalized_probs(
        self,
        net_output: Tuple[Tensor, Optional[Dict[str, List[Optional[Tensor]]]]],
        log_probs: bool,
        sample: Optional[Dict[str, Tensor]] = None,
    ):
        r"""
        we overwrite this function to change the probability calculation process.
        step 1.
            calculate the knn probability based on retrieve resultes
        step 2.
            combine the knn probability with NMT's probability
        """
        if self.args.knn_mode == "inference" or self.args.knn_mode == "train_metak":
            knn_prob = self.combiner.get_knn_prob(**self.retriever.results, device=net_output[0].device)
            combined_prob, _ = self.combiner.get_combined_prob(knn_prob, net_output[0], log_probs=log_probs)
            return combined_prob
        else:
            return super().get_normalized_probs(net_output, log_probs, sample)

r""" Define some pck knn-mt's arch.
     arch name format is: knn_mt_type@base_model_arch
"""
@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer")
def base_architecture(args):
    archs.base_architecture(args)

@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer_iwslt_de_en")
def transformer_iwslt_de_en(args):
    archs.transformer_iwslt_de_en(args)

@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer_wmt_en_de")
def transformer_wmt_en_de(args):
    archs.base_architecture(args)

# parameters used in the "Attention Is All You Need" paper (Vaswani et al., 2017)
@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer_vaswani_wmt_en_de_big")
def transformer_vaswani_wmt_en_de_big(args):
    archs.transformer_vaswani_wmt_en_de_big(args)

@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer_vaswani_wmt_en_fr_big")
def transformer_vaswani_wmt_en_fr_big(args):
    archs.transformer_vaswani_wmt_en_fr_big(args)

@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer_wmt_en_de_big")
def transformer_wmt_en_de_big(args):
    archs.transformer_vaswani_wmt_en_de_big(args)

# default parameters used in tensor2tensor implementation
@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer_wmt_en_de_big_t2t")
def transformer_wmt_en_de_big_t2t(args):
    archs.transformer_wmt_en_de_big_t2t(args)

@register_model_architecture("pck_knn_mt", "pck_knn_mt@transformer_wmt19_de_en")
def transformer_wmt19_de_en(args):
    archs.transformer_wmt19_de_en(args)


from flask import Blueprint, abort, jsonify, request
import urllib
from util import filter_actors_helper

def construct_actor_blueprint(actor_data):
	""" Create blueprint to handle API routing for actors

	:param actor_data: JSON data for actors
	:return: blueprint to handle routing for actor requests
	"""
	# initialize the blueprint
	actor_blueprint = Blueprint('actor_blueprint', __name__)

	@actor_blueprint.route('/api/actors/<string:actor_name>', methods=['GET'])
	def get_actor(actor_name):
		""" handle GET request for specific actor

		:param actor_name: actor to look-up
		:return: JSON data for requested actor
		"""
		# attempt to lookup actor
		actor = actor_data.get(actor_name)
		if actor is None:
			# custom error handler defined in app.py
			abort(400)
		return jsonify(actor), 200

	@actor_blueprint.route('/api/actors/', methods=['GET'])
	def get_actors():
		""" handle GET request for all actors satisfying query

		:return: JSON for actors satisfying query
		"""
		# parse the querystring
		query = request.query_string
		query = urllib.unquote(query)
		# apply the filter defined in query string
		movies = filter_actors_helper(query, actor_data)
		if movies is None:
			# custom error handler defined in app.py
			abort(400)
		return jsonify(movies), 200

	@actor_blueprint.route('/api/actors/<string:actor_name>', methods=['PUT'])
	def put_actor(actor_name):
		""" handle PUT request for a specific actor

		:param actor_name: actor to update info for
		:return: JSON with updated info. for actor
		"""
		# lookup the actor
		actor = actor_data.get(actor_name)
		update = request.json
		# if actor doesn't exist or invalid data, throw 400
		if update is None or actor is None:
			abort(400)
		# check all keys are valid before updating anything
		for key in update.keys():
			if key not in actor.keys():
				abort(400)
		# perform the update
		for key in update.keys():
			actor[key] = update[key]
		return jsonify(actor), 200

	@actor_blueprint.route('/api/actors/', methods=['POST'])
	def post_actor():
		""" handle POST request for new actor

		:return: JSON data for newly created actor
		"""
		# grab JSON data to use for new actor
		new_actor = request.json
		# must have valid data
		if new_actor is None:
			abort(400)
		# name key must exist and be unique
		actor_name = new_actor.get("name")
		if actor_name is None or actor_name in actor_data.keys():
			abort(400)
		actor_data[actor_name] = {}
		# add the data to stored JSON list
		for key in new_actor.keys():
			actor_data[actor_name][key] = new_actor[key]
		return jsonify(actor_data[actor_name]), 201

	@actor_blueprint.route('/api/actors/<string:actor_name>', methods=['DELETE'])
	def delete_actor(actor_name):
		""" handle DELETE request for specific actor

		:param actor_name: name of actor to delete
		:return: JSON indicating if deletion failed or succeeded
		"""
		# attempt to pop actor from JSON list
		actor = actor_data.pop(actor_name, None)
		# if pop failed, actor did not exist
		if actor is None:
			abort(400)
		return jsonify({'status': "Deletion of " + actor_name + " was successful"}), 200

	# return blueprint to main app
	return actor_blueprint


#!/usr/bin/env python
## Time-stamp: "2015-12-03 17:03:20 marine"

import numpy as np
from scipy.integrate import quad
import matplotlib.pyplot as plt

IRON = {'name' : 'Fe', 'f1' : 5.26e-9, 'f2' : 1.24, 'f3' : -3.21}

def rho(f,f1,f2,f3) :
    """ equation 9 Gomi 2013 """
    return f1*(f2 - f)**f3

def resistivity(f, T, El, theta0=417., gamma=1.52, Ir=IRON) :
    """ Ideal resistivity from the Bloch GRuneisen formula as in Gomi 2013 (equation (1))

    f: V/V0
    T : absolute temperature
    El: element (dictionnary with z valence charge and x atomic fraction)
    theta0
    gamma

    """

    def integrand_eq1(z):
        """ integrande for function resistivity """
        return z**5/(np.exp(z)-1)/(1-np.exp(-z))

    theta = theta0*np.exp(-gamma *np.log(f))  #Debye temperature
    tfac = (T/theta)**5 *quad(integrand_eq1, 0, theta/T)[0]

    imp = El['z']**2*El['x']

    idSi = rho(f, El['f1'], El['f2'], El['f3'])
    idFe = rho(f, Ir['f1'], Ir['f2'], Ir['f3']) * tfac

    return idFe + idSi*imp  #ideal resistivity

def k(id, T, f, L=2.44e-8):
    """ real resistivity, adding ideal resistivity (id) and saturation resistivity (sat)
    saturation resistivity at 1bar is 1.68e-6 \Ohm m
    """
    sat = 1.68e-6 * f**(1./3.)
    return L*T*(1./id+ 1./sat)

if __name__ == '__main__':

    import eos

    ## Light elements :
    # name, z: valence charge, x: atomic fraction
    # f1,f2,f3 are fitting parameters
    IRON = {'name' : 'Fe', 'f1' : 5.26e-9, 'f2' : 1.24, 'f3' : -3.21}
    SILICIUM = {'name' : 'Si', 'z' : 1, 'x' : 22.5, 'f1' : 3.77e-8, 'f2' : 1.48, 'f3' : -3.10}
    CARBON = {'name' : 'C', 'z' : 1, 'x' : 30}
    OXYGEN = {'name' : 'O', 'z' : 0.5, 'x' : 23.2}
    SULFUR = {'name' : 'S', 'z' : 0.5, 'x' : 19.4}

    MURNAGHAN = {'T0': 1812., 'rho0': 7010., 'K': 130.*1e9, 'KPrim':4.,
                        'alphaPrim': 130.*np.log(2.)/(360.-135.) ,
                        'alpha': 1.e-5/np.exp(-0.400485 *135./130.),   #1.e-5*np.exp(130.*np.log(2.)/(360.-135.)*135./130),#   1.e-5/np.exp( np.log(2)/(360.-135.)*135.),
                        'Cp': 800., 'cond': 140.}
    P0 = 135e9
    T0 = 3750.

    N = 100 #number of points in the profile
    P = np.linspace(P0,330.e9,N)
    T = []

    ## Adiabatic profile
    for p in P:
        #print Tadiabatic(p, T0, P0, MURNAGHAN)
        T = np.append(T, eos.Tadiabatic(p, T0, P0, MURNAGHAN))

    fig1 = plt.figure()
    ax1 = fig1.add_subplot(111)
    ax1.plot(P/1.e9,T)
    ax1.set_xlabel("Pressure (GPa)")
    ax1.set_xlabel("Temperature (K)")
    ax1.set_title("Adiabatic profile")

    ## Associated k profile
    conductivity = []
    for i in range(0,N):
        f = eos.fval(P[i],T[i], MURNAGHAN)
        res = resistivity(f, T[i], El=SILICIUM, theta0=417, gamma=1.52, Ir=IRON)
        conductivity = np.append(conductivity, k(res, T[i], f, L=2.44e-8))

    fig2 = plt.figure()
    ax2 = fig2.add_subplot(111)
    ax2.plot(P/1.e9,conductivity)
    ax2.set_xlabel("Pressure (GPa)")
    ax2.set_xlabel("Conductivity")
    ax2.set_title("Conductivity profile along the adiabatic profile")

    plt.show()


# Se importa openCV con cv2 para la inteligencia artificial de reconocimiento de caras
import cv2
# El haarcascade tiene precargado un reconocmiento de caras
face_cascade=cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
ds_factor=0.6

class VideoCamera(object):
	def __init__(self):
		# De esta forma se captura la webcam conectada por defecto
		self.video = cv2.VideoCapture(0)

	def __del__(self):
		# Con esto se suelta a la camara
		self.video.release()

	def get_frame(self):
		# En esta función se extrae cada imagen del video y se procesa para encontrar un rostro
		ret, frame = self.video.read()
		frame=cv2.resize(frame,None,fx=ds_factor,fy=ds_factor,interpolation=cv2.INTER_AREA)
		gray=cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
		face_rects=face_cascade.detectMultiScale(gray,1.3,5)

		# Se dibuja un rectángulo encima de cada rostro encontrado
		for (x,y,w,h) in face_rects:
			cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)
			break

		# Se encripta el frame bruto de OpenCV a JPG
		ret, jpeg = cv2.imencode('.jpg', frame)
		return jpeg.tobytes()


from typing import Optional

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from pydantic import BaseModel

from core.executor import exec_run_python, get_container

app = FastAPI()

origins = [
    "*",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class CodeRunForm(BaseModel):
    language: str
    version: Optional[str] = "0.01"
    code: str
    timestamp: int

@app.get("/")
def index_api():
    version = "0.0.1"
    return {"Name": "CoderRunner", "Version": version}

def runcode_action(form: CodeRunForm):
    LANGUAGES = {
        "python": get_container('codebox-python3'),
        "python3": get_container('codebox-python3')
    }
    if form.language  not in LANGUAGES:
        return {"result": "Unsupported language", "code": 400}

    user = "codemax"

    container = LANGUAGES[form.language]

    result = exec_run_python(container, form.code, user)

    return {"result": result, "code": 0}

@app.post("/api/coderun/")
def codecode_api(form: CodeRunForm):
    return runcode_action(form)

@app.post("/api/runcode/")
def runcode_api(form: CodeRunForm):
    return runcode_action(form)