# 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.

"""Module which verifies attributes in an Emboss IR.

The main entry point is check_attributes_in_ir(), which checks attributes in an
IR.
"""

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


# Error messages used by multiple attribute type checkers.
_BAD_TYPE_MESSAGE = "Attribute '{name}' must have {type} value."
_MUST_BE_CONSTANT_MESSAGE = "Attribute '{name}' must have a constant value."


def _attribute_name_for_errors(attr):
  if ir_data_utils.reader(attr).back_end.text:
    return f"({attr.back_end.text}) {attr.name.text}"
  else:
    return attr.name.text


# Attribute type checkers
def _is_constant_boolean(attr, module_source_file):
  """Checks if the given attr is a constant boolean."""
  if not attr.value.expression.type.boolean.HasField("value"):
    return [[error.error(module_source_file,
                         attr.value.source_location,
                         _BAD_TYPE_MESSAGE.format(
                             name=_attribute_name_for_errors(attr),
                             type="a constant boolean"))]]
  return []


def _is_boolean(attr, module_source_file):
  """Checks if the given attr is a boolean."""
  if attr.value.expression.type.WhichOneof("type") != "boolean":
    return [[error.error(module_source_file,
                         attr.value.source_location,
                         _BAD_TYPE_MESSAGE.format(
                             name=_attribute_name_for_errors(attr),
                             type="a boolean"))]]
  return []


def _is_constant_integer(attr, module_source_file):
  """Checks if the given attr is an integer constant expression."""
  if (not attr.value.HasField("expression") or
      attr.value.expression.type.WhichOneof("type") != "integer"):
    return [[error.error(module_source_file,
                         attr.value.source_location,
                         _BAD_TYPE_MESSAGE.format(
                             name=_attribute_name_for_errors(attr),
                             type="an integer"))]]
  if not ir_util.is_constant(attr.value.expression):
    return [[error.error(module_source_file,
                         attr.value.source_location,
                         _MUST_BE_CONSTANT_MESSAGE.format(
                             name=_attribute_name_for_errors(attr)))]]
  return []


def _is_string(attr, module_source_file):
  """Checks if the given attr is a string."""
  if not attr.value.HasField("string_constant"):
    return [[error.error(module_source_file,
                         attr.value.source_location,
                         _BAD_TYPE_MESSAGE.format(
                             name=_attribute_name_for_errors(attr),
                             type="a string"))]]
  return []


# Provide more readable names for these functions when used in attribute type
# specifiers.
BOOLEAN_CONSTANT = _is_constant_boolean
BOOLEAN = _is_boolean
INTEGER_CONSTANT = _is_constant_integer
STRING = _is_string


def string_from_list(valid_values):
  """Checks if the given attr has one of the valid_values."""
  def _string_from_list(attr, module_source_file):
    if ir_data_utils.reader(attr).value.string_constant.text not in valid_values:
      return [[error.error(module_source_file,
                           attr.value.source_location,
                           "Attribute '{name}' must be '{options}'.".format(
                               name=_attribute_name_for_errors(attr),
                               options="' or '".join(sorted(valid_values))))]]
    return []
  return _string_from_list


def check_attributes_in_ir(ir,
                           *,
                           back_end=None,
                           types=None,
                           module_attributes=None,
                           struct_attributes=None,
                           bits_attributes=None,
                           enum_attributes=None,
                           enum_value_attributes=None,
                           external_attributes=None,
                           structure_virtual_field_attributes=None,
                           structure_physical_field_attributes=None):
  """Performs basic checks on all attributes in the given ir.

  This function calls _check_attributes on each attribute list in ir.

  Arguments:
    ir: An ir_data.EmbossIr to check.
    back_end: A string specifying the attribute qualifier to check (such as
        `cpp` for `[(cpp) namespace = "foo"]`), or None to check unqualified
        attributes.

        Attributes with a different qualifier will not be checked.
    types: A map from attribute names to validators, such as:
        {
            "maximum_bits": attribute_util.INTEGER_CONSTANT,
            "requires": attribute_util.BOOLEAN,
        }
    module_attributes: A set of (attribute_name, is_default) tuples specifying
        the attributes that are allowed at module scope.
    struct_attributes: A set of (attribute_name, is_default) tuples specifying
        the attributes that are allowed at `struct` scope.
    bits_attributes: A set of (attribute_name, is_default) tuples specifying
        the attributes that are allowed at `bits` scope.
    enum_attributes: A set of (attribute_name, is_default) tuples specifying
        the attributes that are allowed at `enum` scope.
    enum_value_attributes: A set of (attribute_name, is_default) tuples
        specifying the attributes that are allowed at the scope of enum values.
    external_attributes: A set of (attribute_name, is_default) tuples
        specifying the attributes that are allowed at `external` scope.
    structure_virtual_field_attributes: A set of (attribute_name, is_default)
        tuples specifying the attributes that are allowed at the scope of
        virtual fields (`let` fields) in structures (both `struct` and `bits`).
    structure_physical_field_attributes: A set of (attribute_name, is_default)
        tuples specifying the attributes that are allowed at the scope of
        physical fields in structures (both `struct` and `bits`).

  Returns:
    A list of lists of error.error, or an empty list if there were no errors.
  """

  def check_module(module, errors):
    errors.extend(_check_attributes(
        module.attribute, types, back_end, module_attributes,
        "module '{}'".format(
            module.source_file_name), module.source_file_name))

  def check_type_definition(type_definition, source_file_name, errors):
    if type_definition.HasField("structure"):
      if type_definition.addressable_unit == ir_data.AddressableUnit.BYTE:
        errors.extend(_check_attributes(
            type_definition.attribute, types, back_end, struct_attributes,
            "struct '{}'".format(
                type_definition.name.name.text), source_file_name))
      elif type_definition.addressable_unit == ir_data.AddressableUnit.BIT:
        errors.extend(_check_attributes(
            type_definition.attribute, types, back_end, bits_attributes,
            "bits '{}'".format(
                type_definition.name.name.text), source_file_name))
      else:
        assert False, "Unexpected addressable_unit '{}'".format(
            type_definition.addressable_unit)
    elif type_definition.HasField("enumeration"):
      errors.extend(_check_attributes(
          type_definition.attribute, types, back_end, enum_attributes,
          "enum '{}'".format(
              type_definition.name.name.text), source_file_name))
    elif type_definition.HasField("external"):
      errors.extend(_check_attributes(
          type_definition.attribute, types, back_end, external_attributes,
          "external '{}'".format(
              type_definition.name.name.text), source_file_name))

  def check_struct_field(field, source_file_name, errors):
    if ir_util.field_is_virtual(field):
      field_attributes = structure_virtual_field_attributes
      field_adjective = "virtual "
    else:
      field_attributes = structure_physical_field_attributes
      field_adjective = ""
    errors.extend(_check_attributes(
        field.attribute, types, back_end, field_attributes,
        "{}struct field '{}'".format(field_adjective, field.name.name.text),
        source_file_name))

  def check_enum_value(value, source_file_name, errors):
    errors.extend(_check_attributes(
        value.attribute, types, back_end, enum_value_attributes,
        "enum value '{}'".format(value.name.name.text), source_file_name))

  errors = []
  # TODO(bolms): Add a check that only known $default'ed attributes are
  # used.
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.Module], check_module,
      parameters={"errors": errors})
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.TypeDefinition], check_type_definition,
      parameters={"errors": errors})
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.Field], check_struct_field,
      parameters={"errors": errors})
  traverse_ir.fast_traverse_ir_top_down(
      ir, [ir_data.EnumValue], check_enum_value,
      parameters={"errors": errors})
  return errors


def _check_attributes(attribute_list, types, back_end, attribute_specs,
                      context_name, module_source_file):
  """Performs basic checks on the given list of attributes.

  Checks the given attribute_list for duplicates, unknown attributes, attributes
  with incorrect type, and attributes whose values are not constant.

  Arguments:
    attribute_list: An iterable of ir_data.Attribute.
    back_end: The qualifier for attributes to check, or None.
    attribute_specs: A dict of attribute names to _Attribute structures
      specifying the allowed attributes.
    context_name: A name for the context of these attributes, such as "struct
      'Foo'" or "module 'm.emb'".  Used in error messages.
    module_source_file: The value of module.source_file_name from the module
      containing 'attribute_list'.  Used in error messages.

  Returns:
    A list of lists of error.Errors.  An empty list indicates no errors were
    found.
  """
  if attribute_specs is None:
    attribute_specs = []
  errors = []
  already_seen_attributes = {}
  for attr in attribute_list:
    field_checker = ir_data_utils.reader(attr)
    if field_checker.back_end.text:
      if attr.back_end.text != back_end:
        continue
    else:
      if back_end is not None:
        continue
    attribute_name = _attribute_name_for_errors(attr)
    attr_key = (field_checker.name.text, field_checker.is_default)
    if attr_key in already_seen_attributes:
      original_attr = already_seen_attributes[attr_key]
      errors.append([
          error.error(module_source_file,
                      attr.source_location,
                      "Duplicate attribute '{}'.".format(attribute_name)),
          error.note(module_source_file,
                     original_attr.source_location,
                     "Original attribute")])
      continue
    already_seen_attributes[attr_key] = attr

    if attr_key not in attribute_specs:
      if attr.is_default:
        error_message = "Attribute '{}' may not be defaulted on {}.".format(
            attribute_name, context_name)
      else:
        error_message = "Unknown attribute '{}' on {}.".format(attribute_name,
                                                               context_name)
      errors.append([error.error(module_source_file,
                                 attr.name.source_location,
                                 error_message)])
    else:
      errors.extend(types[attr.name.text](attr, module_source_file))
  return errors


def gather_default_attributes(obj, defaults):
  """Gathers default attributes for an IR object

  This is designed to be able to be used as-is as an incidental action in an IR
  traversal to accumulate defaults for child nodes.

  Arguments:
    defaults: A dict of `{ "defaults": { attr.name.text: attr } }`

  Returns:
    A dict of `{ "defaults": { attr.name.text: attr } }` with any defaults
    provided by `obj` added/overridden.
  """
  defaults = defaults.copy()
  for attr in obj.attribute:
    if attr.is_default:
      defaulted_attr = ir_data_utils.copy(attr)
      defaulted_attr.is_default = False
      defaults[attr.name.text] = defaulted_attr
  return {"defaults": defaults}
