# Copyright 2019 Google LLC
#
# 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
#
#     https://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.

"""Symbol resolver for Emboss IR.

The resolve_symbols function should be used to generate canonical resolutions
for all symbol references in an Emboss IR.
"""

import collections

from compiler.util import error
from compiler.util import ir_data
from compiler.util import ir_data_utils
from compiler.util import ir_util
from compiler.util import traverse_ir

# TODO(bolms): Symbol resolution raises an exception at the first error, but
# this is one place where it can make sense to report multiple errors.

FileLocation = collections.namedtuple("FileLocation", ["file", "location"])


def ambiguous_name_error(file_name, location, name, candidate_locations):
  """A name cannot be resolved because there are two or more candidates."""
  result = [error.error(file_name, location, "Ambiguous name '{}'".format(name))
           ]
  for location in sorted(candidate_locations):
    result.append(error.note(location.file, location.location,
                             "Possible resolution"))
  return result


def duplicate_name_error(file_name, location, name, original_location):
  """A name is defined two or more times."""
  return [error.error(file_name, location, "Duplicate name '{}'".format(name)),
          error.note(original_location.file, original_location.location,
                     "Original definition")]


def missing_name_error(file_name, location, name):
  return [error.error(file_name, location, "No candidate for '{}'".format(name))
         ]


def array_subfield_error(file_name, location, name):
  return [error.error(file_name, location,
                      "Cannot access member of array '{}'".format(name))]


def noncomposite_subfield_error(file_name, location, name):
  return [error.error(file_name, location,
                      "Cannot access member of noncomposite field '{}'".format(
                          name))]


def _nested_name(canonical_name, name):
  """Creates a new CanonicalName with name appended to the object_path."""
  return ir_data.CanonicalName(
      module_file=canonical_name.module_file,
      object_path=list(canonical_name.object_path) + [name])


class _Scope(dict):
  """A _Scope holds data for a symbol.

  A _Scope is a dict with some additional attributes.  Lexically nested names
  are kept in the dict, and bookkeeping is kept in the additional attributes.

  For example, each module should have a child _Scope for each type contained in
  the module.  `struct` and `bits` types should have nested _Scopes for each
  field; `enum` types should have nested scopes for each enumerated name.

  Attributes:
    canonical_name: The absolute name of this symbol; e.g. ("file.emb",
      "TypeName", "SubTypeName", "field_name")
    source_location: The ir_data.SourceLocation where this symbol is defined.
    visibility: LOCAL, PRIVATE, or SEARCHABLE; see below.
    alias: If set, this name is merely a pointer to another name.
  """
  __slots__ = ("canonical_name", "source_location", "visibility", "alias")

  # A LOCAL name is visible outside of its enclosing scope, but should not be
  # found when searching for a name.  That is, this name should be matched in
  # the tail of a qualified reference (the 'bar' in 'foo.bar'), but not when
  # searching for names (the 'foo' in 'foo.bar' should not match outside of
  # 'foo's scope).  This applies to public field names.
  LOCAL = object()

  # A PRIVATE name is similar to LOCAL except that it is never visible outside
  # its enclosing scope.  This applies to abbreviations of field names: if 'a'
  # is an abbreviation for field 'apple', then 'foo.a' is not a valid reference;
  # instead it should be 'foo.apple'.
  PRIVATE = object()

  # A SEARCHABLE name is visible as long as it is in a scope in the search list.
  # This applies to type names ('Foo'), which may be found from many scopes.
  SEARCHABLE = object()

  def __init__(self, canonical_name, source_location, visibility, alias=None):
    super(_Scope, self).__init__()
    self.canonical_name = canonical_name
    self.source_location = source_location
    self.visibility = visibility
    self.alias = alias


def _add_name_to_scope(name_ir, scope, canonical_name, visibility, errors):
  """Adds the given name_ir to the given scope."""
  name = name_ir.text
  new_scope = _Scope(canonical_name, name_ir.source_location, visibility)
  if name in scope:
    errors.append(duplicate_name_error(
        scope.canonical_name.module_file, name_ir.source_location, name,
        FileLocation(scope[name].canonical_name.module_file,
                     scope[name].source_location)))
  else:
    scope[name] = new_scope
  return new_scope


def _add_name_to_scope_and_normalize(name_ir, scope, visibility, errors):
  """Adds the given name_ir to scope and sets its canonical_name."""
  name = name_ir.name.text
  canonical_name = _nested_name(scope.canonical_name, name)
  ir_data_utils.builder(name_ir).canonical_name.CopyFrom(canonical_name)
  return _add_name_to_scope(name_ir.name, scope, canonical_name, visibility,
                            errors)


def _add_struct_field_to_scope(field, scope, errors):
  """Adds the name of the given field to the scope."""
  new_scope = _add_name_to_scope_and_normalize(field.name, scope, _Scope.LOCAL,
                                               errors)
  if field.HasField("abbreviation"):
    _add_name_to_scope(field.abbreviation, scope, new_scope.canonical_name,
                       _Scope.PRIVATE, errors)

  value_builtin_name = ir_data.Word(
      text="this",
      source_location=ir_data.Location(is_synthetic=True),
  )
  # In "inside field" scope, the name `this` maps back to the field itself.
  # This is important for attributes like `[requires]`.
  _add_name_to_scope(value_builtin_name, new_scope,
                     field.name.canonical_name, _Scope.PRIVATE, errors)


def _add_parameter_name_to_scope(parameter, scope, errors):
  """Adds the name of the given parameter to the scope."""
  _add_name_to_scope_and_normalize(parameter.name, scope, _Scope.LOCAL, errors)


def _add_enum_value_to_scope(value, scope, errors):
  """Adds the name of the enum value to scope."""
  _add_name_to_scope_and_normalize(value.name, scope, _Scope.LOCAL, errors)


def _add_type_name_to_scope(type_definition, scope, errors):
  """Adds the name of type_definition to the given scope."""
  new_scope = _add_name_to_scope_and_normalize(type_definition.name, scope,
                                               _Scope.SEARCHABLE, errors)
  return {"scope": new_scope}


def _set_scope_for_type_definition(type_definition, scope):
  """Sets the current scope for an ir_data.AddressableUnit."""
  return {"scope": scope[type_definition.name.name.text]}


def _add_module_to_scope(module, scope):
  """Adds the name of the module to the given scope."""
  module_symbol_table = _Scope(
      ir_data.CanonicalName(module_file=module.source_file_name,
                           object_path=[]),
      None,
      _Scope.SEARCHABLE)
  scope[module.source_file_name] = module_symbol_table
  return {"scope": scope[module.source_file_name]}


def _set_scope_for_module(module, scope):
  """Adds the name of the module to the given scope."""
  return {"scope": scope[module.source_file_name]}


def _add_import_to_scope(foreign_import, table, module, errors):
  if not foreign_import.local_name.text:
    # This is the prelude import; ignore it.
    return
  _add_alias_to_scope(foreign_import.local_name, table, module.canonical_name,
                      [foreign_import.file_name.text], _Scope.SEARCHABLE,
                      errors)


def _construct_symbol_tables(ir):
  """Constructs per-module symbol tables for each module in ir."""
  symbol_tables = {}
  errors = []
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.Module], _add_module_to_scope,
      parameters={"errors": errors, "scope": symbol_tables})
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.TypeDefinition], _add_type_name_to_scope,
      incidental_actions={ir_data.Module: _set_scope_for_module},
      parameters={"errors": errors, "scope": symbol_tables})
  if errors:
    # Ideally, we would find duplicate field names elsewhere in the module, even
    # if there are duplicate type names, but field/enum names in the colliding
    # types also end up colliding, leading to spurious errors.  E.g., if you
    # have two `struct Foo`s, then the field check will also discover a
    # collision for `$size_in_bytes`, since there are two `Foo.$size_in_bytes`.
    return symbol_tables, errors

  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.EnumValue], _add_enum_value_to_scope,
      incidental_actions={
          ir_data.Module: _set_scope_for_module,
          ir_data.TypeDefinition: _set_scope_for_type_definition,
      },
      parameters={"errors": errors, "scope": symbol_tables})
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.Field], _add_struct_field_to_scope,
      incidental_actions={
          ir_data.Module: _set_scope_for_module,
          ir_data.TypeDefinition: _set_scope_for_type_definition,
      },
      parameters={"errors": errors, "scope": symbol_tables})
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.RuntimeParameter], _add_parameter_name_to_scope,
      incidental_actions={
          ir_data.Module: _set_scope_for_module,
          ir_data.TypeDefinition: _set_scope_for_type_definition,
      },
      parameters={"errors": errors, "scope": symbol_tables})
  return symbol_tables, errors


def _add_alias_to_scope(name_ir, table, scope, alias, visibility, errors):
  """Adds the given name to the scope as an alias."""
  name = name_ir.text
  new_scope = _Scope(_nested_name(scope, name), name_ir.source_location,
                     visibility, alias)
  scoped_table = table[scope.module_file]
  for path_element in scope.object_path:
    scoped_table = scoped_table[path_element]
  if name in scoped_table:
    errors.append(duplicate_name_error(
        scoped_table.canonical_name.module_file, name_ir.source_location, name,
        FileLocation(scoped_table[name].canonical_name.module_file,
                     scoped_table[name].source_location)))
  else:
    scoped_table[name] = new_scope
  return new_scope


def _resolve_head_of_field_reference(field_reference, table, current_scope,
                                     visible_scopes, source_file_name, errors):
  return _resolve_reference(
      field_reference.path[0], table, current_scope,
      visible_scopes, source_file_name, errors)


def _resolve_reference(reference, table, current_scope, visible_scopes,
                       source_file_name, errors):
  """Sets the canonical name of the given reference."""
  if reference.HasField("canonical_name"):
    # This reference has already been resolved by the _resolve_field_reference
    # pass.
    return
  target = _find_target_of_reference(reference, table, current_scope,
                                     visible_scopes, source_file_name, errors)
  if target is not None:
    assert not target.alias
    ir_data_utils.builder(reference).canonical_name.CopyFrom(target.canonical_name)


def _find_target_of_reference(reference, table, current_scope, visible_scopes,
                              source_file_name, errors):
  """Returns the resolved name of the given reference."""
  found_in_table = None
  name = reference.source_name[0].text
  for scope in visible_scopes:
    scoped_table = table[scope.module_file]
    for path_element in scope.object_path or []:
      scoped_table = scoped_table[path_element]
    if (name in scoped_table and
        (scope == current_scope or
         scoped_table[name].visibility == _Scope.SEARCHABLE)):
      # Prelude is "", so explicitly check for None.
      if found_in_table is not None:
        # TODO(bolms): Currently, this catches the case where a module tries to
        # use a name that is defined (at the same scope) in two different
        # modules.  It may make sense to raise duplicate_name_error whenever two
        # modules define the same name (whether it is used or not), and reserve
        # ambiguous_name_error for cases where a name is found in multiple
        # scopes.
        errors.append(ambiguous_name_error(
            source_file_name, reference.source_location, name, [FileLocation(
                found_in_table[name].canonical_name.module_file,
                found_in_table[name].source_location), FileLocation(
                    scoped_table[name].canonical_name.module_file, scoped_table[
                        name].source_location)]))
        continue
      found_in_table = scoped_table
      if reference.is_local_name:
        # This is a little hacky.  When "is_local_name" is True, the name refers
        # to a type that was defined inline.  In many cases, the type should be
        # found at the same scope as the field; e.g.:
        #
        #     struct Foo:
        #       0 [+1]  enum  bar:
        #         BAZ = 1
        #
        # In this case, `Foo.bar` has type `Foo.Bar`.  Unfortunately, things
        # break down a little bit when there is an inline type in an anonymous
        # `bits`:
        #
        #     struct Foo:
        #       0 [+1]  bits:
        #         0 [+7]  enum  bar:
        #           BAZ = 1
        #
        # Types inside of anonymous `bits` are hoisted into their parent type,
        # so instead of `Foo.EmbossReservedAnonymous1.Bar`, `bar`'s type is just
        # `Foo.Bar`.  Unfortunately, the field is still
        # `Foo.EmbossReservedAnonymous1.bar`, so `bar`'s type won't be found in
        # `bar`'s `current_scope`.
        #
        # (The name `bar` is exposed from `Foo` as an alias virtual field, so
        # perhaps the correct answer is to allow type aliases, so that `Bar` can
        # be found in both `Foo` and `Foo.EmbossReservedAnonymous1`.  That would
        # involve an entirely new feature, though.)
        #
        # The workaround here is to search scopes from the innermost outward,
        # and just stop as soon as a match is found.  This isn't ideal, because
        # it relies on other bits of the front end having correctly added the
        # inline type to the correct scope before symbol resolution, but it does
        # work.  Names with False `is_local_name` will still be checked for
        # ambiguity.
        break
  if found_in_table is None:
    errors.append(missing_name_error(
        source_file_name, reference.source_name[0].source_location, name))
  if not errors:
    for subname in reference.source_name:
      if subname.text not in found_in_table:
        errors.append(missing_name_error(source_file_name,
                                         subname.source_location, subname.text))
        return None
      found_in_table = found_in_table[subname.text]
      while found_in_table.alias:
        referenced_table = table
        for name in found_in_table.alias:
          referenced_table = referenced_table[name]
          # TODO(bolms): This section should really be a recursive lookup
          # function, which would be able to handle arbitrary aliases through
          # other aliases.
          #
          # This should be fine for now, since the only aliases here should be
          # imports, which can't refer to other imports.
          assert not referenced_table.alias, "Alias found to contain alias."
        found_in_table = referenced_table
    return found_in_table
  return None


def _resolve_field_reference(field_reference, source_file_name, errors, ir):
  """Resolves the References inside of a FieldReference."""
  if field_reference.path[-1].HasField("canonical_name"):
    # Already done.
    return
  previous_field = ir_util.find_object_or_none(field_reference.path[0], ir)
  previous_reference = field_reference.path[0]
  for ref in field_reference.path[1:]:
    while ir_util.field_is_virtual(previous_field):
      if (previous_field.read_transform.WhichOneof("expression") ==
          "field_reference"):
        # Pass a separate error list into the recursive _resolve_field_reference
        # call so that only one copy of the error for a particular reference
        # will actually surface: in particular, the one that results from a
        # direct call from traverse_ir_top_down into _resolve_field_reference.
        new_errors = []
        _resolve_field_reference(
            previous_field.read_transform.field_reference,
            previous_field.name.canonical_name.module_file, new_errors, ir)
        # If the recursive _resolve_field_reference was unable to resolve the
        # field, then bail.  Otherwise we get a cascade of errors, where an
        # error in `x` leads to errors in anything trying to reach a member of
        # `x`.
        if not previous_field.read_transform.field_reference.path[-1].HasField(
            "canonical_name"):
          return
        previous_field = ir_util.find_object(
            previous_field.read_transform.field_reference.path[-1], ir)
      else:
        errors.append(
            noncomposite_subfield_error(source_file_name,
                                        previous_reference.source_location,
                                        previous_reference.source_name[0].text))
        return
    if previous_field.type.WhichOneof("type") == "array_type":
      errors.append(
          array_subfield_error(source_file_name,
                               previous_reference.source_location,
                               previous_reference.source_name[0].text))
      return
    assert previous_field.type.WhichOneof("type") == "atomic_type"
    member_name = ir_data_utils.copy(
        previous_field.type.atomic_type.reference.canonical_name)
    ir_data_utils.builder(member_name).object_path.extend([ref.source_name[0].text])
    previous_field = ir_util.find_object_or_none(member_name, ir)
    if previous_field is None:
      errors.append(
          missing_name_error(source_file_name,
                             ref.source_name[0].source_location,
                             ref.source_name[0].text))
      return
    ir_data_utils.builder(ref).canonical_name.CopyFrom(member_name)
    previous_reference = ref


def _set_visible_scopes_for_type_definition(type_definition, visible_scopes):
  """Sets current_scope and visible_scopes for the given type_definition."""
  return {
      "current_scope": type_definition.name.canonical_name,

      # In order to ensure that the iteration through scopes in
      # _find_target_of_reference will go from innermost to outermost, it is
      # important that the current scope (type_definition.name.canonical_name)
      # precedes the previous visible_scopes here.
      "visible_scopes": (type_definition.name.canonical_name,) + visible_scopes,
  }


def _set_visible_scopes_for_module(module):
  """Sets visible_scopes for the given module."""
  self_scope = ir_data.CanonicalName(module_file=module.source_file_name)
  extra_visible_scopes = []
  for foreign_import in module.foreign_import:
    # Anonymous imports are searched for top-level names; named imports are not.
    # As of right now, only the prelude should be imported anonymously; other
    # modules must be imported with names.
    if not foreign_import.local_name.text:
      extra_visible_scopes.append(
          ir_data.CanonicalName(module_file=foreign_import.file_name.text))
  return {"visible_scopes": (self_scope,) + tuple(extra_visible_scopes)}


def _set_visible_scopes_for_attribute(attribute, field, visible_scopes):
  """Sets current_scope and visible_scopes for the attribute."""
  del attribute  # Unused
  if field is None:
    return
  return {
      "current_scope": field.name.canonical_name,
      "visible_scopes": (field.name.canonical_name,) + visible_scopes,
  }

def _module_source_from_table_action(m, table):
  return {"module": table[m.source_file_name]}

def _resolve_symbols_from_table(ir, table):
  """Resolves all references in the given IR, given the constructed table."""
  errors = []
  # Symbol resolution is broken into five passes.  First, this code resolves any
  # imports, and adds import aliases to modules.
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.Import], _add_import_to_scope,
      incidental_actions={
          ir_data.Module: _module_source_from_table_action,
      },
      parameters={"errors": errors, "table": table})
  if errors:
    return errors
  # Next, this resolves all absolute references (e.g., it resolves "UInt" in
  # "0:1  UInt  field" to [prelude]::UInt).
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.Reference], _resolve_reference,
      skip_descendants_of=(ir_data.FieldReference,),
      incidental_actions={
          ir_data.TypeDefinition: _set_visible_scopes_for_type_definition,
          ir_data.Module: _set_visible_scopes_for_module,
          ir_data.Attribute: _set_visible_scopes_for_attribute,
      },
      parameters={"table": table, "errors": errors, "field": None})
  # Lastly, head References to fields (e.g., the `a` of `a.b.c`) are resolved.
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.FieldReference], _resolve_head_of_field_reference,
      incidental_actions={
          ir_data.TypeDefinition: _set_visible_scopes_for_type_definition,
          ir_data.Module: _set_visible_scopes_for_module,
          ir_data.Attribute: _set_visible_scopes_for_attribute,
      },
      parameters={"table": table, "errors": errors, "field": None})
  return errors


def resolve_field_references(ir):
  """Resolves structure member accesses ("field.subfield") in ir."""
  errors = []
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.FieldReference], _resolve_field_reference,
      incidental_actions={
          ir_data.TypeDefinition: _set_visible_scopes_for_type_definition,
          ir_data.Module: _set_visible_scopes_for_module,
          ir_data.Attribute: _set_visible_scopes_for_attribute,
      },
      parameters={"errors": errors, "field": None})
  return errors


def resolve_symbols(ir):
  """Resolves the symbols in all modules in ir."""
  symbol_tables, errors = _construct_symbol_tables(ir)
  if errors:
    return errors
  return _resolve_symbols_from_table(ir, symbol_tables)
