# frozen_string_literal: true

# The TypeDefinitionInstantiator instantiates a type alias or a type definition
#
module Puppet::Pops
module Loader
class TypeDefinitionInstantiator
  def self.create(loader, typed_name, source_ref, pp_code_string)
    # parse and validate
    parser = Parser::EvaluatingParser.new()
    model = parser.parse_string(pp_code_string, source_ref)
    # Only one type is allowed (and no other definitions)

    name = typed_name.name
    case model.definitions.size
    when 0
      raise ArgumentError, _("The code loaded from %{source_ref} does not define the type '%{name}' - it is empty.") % { source_ref: source_ref, name: name }
    when 1
      # ok
    else
      raise ArgumentError,
            _("The code loaded from %{source_ref} must contain only the type '%{name}' - it has additional definitions.") % { source_ref: source_ref, name: name }
    end
    type_definition = model.definitions[0]

    unless type_definition.is_a?(Model::TypeAlias) || type_definition.is_a?(Model::TypeDefinition)
      raise ArgumentError,
            _("The code loaded from %{source_ref} does not define the type '%{name}' - no type alias or type definition found.") % { source_ref: source_ref, name: name }
    end

    actual_name = type_definition.name
    unless name == actual_name.downcase
      raise ArgumentError,
            _("The code loaded from %{source_ref} produced type with the wrong name, expected '%{name}', actual '%{actual_name}'") % { source_ref: source_ref, name: name, actual_name: actual_name }
    end

    unless model.body == type_definition
      raise ArgumentError,
            _("The code loaded from %{source_ref} contains additional logic - can only contain the type '%{name}'") % { source_ref: source_ref, name: name }
    end

    # Adapt the type definition with loader - this is used from logic contained in its body to find the
    # loader to use when resolving contained aliases API. Such logic have a hard time finding the closure (where
    # the loader is known - hence this mechanism
    private_loader = loader.private_loader
    Adapters::LoaderAdapter.adapt(type_definition).loader_name = private_loader.loader_name
    create_runtime_type(type_definition)
  end

  def self.create_from_model(type_definition, loader)
    typed_name = TypedName.new(:type, type_definition.name)
    type = create_runtime_type(type_definition)
    loader.set_entry(
      typed_name,
      type,
      type_definition.locator.to_uri(type_definition)
    )
    type
  end

  # @api private
  def self.create_runtime_type(type_definition)
    # Using the RUNTIME_NAME_AUTHORITY as the name_authority is motivated by the fact that the type
    # alias name (managed by the runtime) becomes the name of the created type
    #
    create_type(type_definition.name, type_definition.type_expr, Pcore::RUNTIME_NAME_AUTHORITY)
  end

  # @api private
  def self.create_type(name, type_expr, name_authority)
    create_named_type(name, named_definition(type_expr), type_expr, name_authority)
  end

  # @api private
  def self.create_named_type(name, type_name, type_expr, name_authority)
    case type_name
    when 'Object'
      # No need for an alias. The Object type itself will receive the name instead
      unless type_expr.is_a?(Model::LiteralHash)
        type_expr = type_expr.keys.empty? ? nil : type_expr.keys[0] unless type_expr.is_a?(Hash)
      end
      Types::PObjectType.new(name, type_expr)
    when 'TypeSet'
      # No need for an alias. The Object type itself will receive the name instead
      type_expr = type_expr.keys.empty? ? nil : type_expr.keys[0] unless type_expr.is_a?(Hash)
      Types::PTypeSetType.new(name, type_expr, name_authority)
    else
      Types::PTypeAliasType.new(name, type_expr)
    end
  end

  # @api private
  def self.named_definition(te)
    return 'Object' if te.is_a?(Model::LiteralHash)

    te.is_a?(Model::AccessExpression) && (left = te.left_expr).is_a?(Model::QualifiedReference) ? left.cased_value : nil
  end

  def several_paths?
    false
  end
end
end
end
