# Copyright 2018 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines a registry for storing per-runtime information.

A registry is essentially a wrapper around a Python dict that stores a mapping
from (runtime, environment) to arbitrary data. Its main feature is that it
supports lookups by matching both the runtime and the environment.
"""


from __future__ import absolute_import
from __future__ import unicode_literals
from six.moves import map  # pylint:disable=redefined-builtin


class RegistryEntry(object):
  """An entry in the Registry.

  Attributes:
    runtime: str or re.RegexObject, the runtime to be staged
    envs: set(env.Environment), the environments to be staged
  """

  def __init__(self, runtime, envs):
    self.runtime = runtime
    self.envs = envs

  def _RuntimeMatches(self, runtime):
    try:
      return self.runtime.match(runtime)
    except AttributeError:
      return self.runtime == runtime

  def _EnvMatches(self, env):
    return env in self.envs

  def Matches(self, runtime, env):
    """Returns True iff the given runtime and environment match this entry.

    The runtime matches if it is an exact string match.

    The environment matches if it is an exact Enum match or if this entry has a
    "wildcard" (that is, None) for the environment.

    Args:
      runtime: str, the runtime to match
      env: env.Environment, the environment to match

    Returns:
      bool, whether the given runtime and environment match.
    """
    return self._RuntimeMatches(runtime) and self._EnvMatches(env)

  def __hash__(self):
    # Sets are unhashable; Environments are unorderable
    return hash((self.runtime, sum(sorted(map(hash, self.envs)))))

  def __eq__(self, other):
    return self.runtime == other.runtime and self.envs == other.envs

  def __ne__(self, other):
    return not self.__eq__(other)


class Registry(object):
  """A registry to store values for various runtimes and environments.

  The registry is a map from (runtime, app-engine-environment) to
  user-specified values. As an example, storing Booleans for different
  runtimes/environments would look like:

  REGISTRY = {
    RegistryEntry('php72', {env.STANDARD}): True,
    RegistryEntry('php55', {env.STANDARD}): False,
    RegistryEntry('nodejs8', {env.FLEX}): False,
  }

  Attributes:
    mappings: dict, where keys are RegistryEntry objects and values can be
      of any type
    override: object or None; if specified, this value will always be returned
      by Get()
    default: object or None; if specified, will be returned if Get() could not
      find a matching registry entry
  """

  def __init__(self, mappings=None, override=None, default=None):
    self.mappings = mappings or {}
    self.override = override
    self.default = default

  def Get(self, runtime, env):
    """Return the associated value for the given runtime/environment.

    Args:
      runtime: str, the runtime to get a stager for
      env: env, the environment to get a stager for

    Returns:
      object, the matching entry, or override if one was specified. If no
        match is found, will return default if specified or None otherwise.
    """
    if self.override:
      return self.override

    for entry, value in self.mappings.items():
      if entry.Matches(runtime, env):
        return value

    if self.default is not None:
      return self.default
    else:
      return None
