module Crystal
  class Type
    # Looks up a *path* (for example: `Foo::Bar::Baz`) relative to `self`.
    #
    # For example, given:
    #
    # ```
    # class Foo
    #   class Bar
    #     class Baz
    #     end
    #   end
    # end
    # ```
    #
    # If `self` is `Foo` and we invoke `lookup_path(["Bar", "Baz"])` on it,
    # we'll get `Foo::Bar::Baz` as the return value.
    #
    # The path is searched in the current type's ancestors, and optionally
    # in its namespace, according to *lookup_in_namespace*.
    #
    # Returns `nil` if the path can't be found.
    #
    # *include_private* controls whether private types are found inside
    # other types (when doing Foo::Bar, Bar won't be found if it's private).
    #
    # *location* can be passed and is the location where the lookup happens,
    # and is useful to find file-private types.
    #
    # The result can be an `ASTNode` in the case the path denotes a type variable
    # whose variable is an `ASTNode`. One such example is the `N` of `StaticArray(T, N)`
    # for some instantiated `StaticArray`.
    #
    # If the path is global (for example ::Foo::Bar), the search starts at
    # the top level.
    def lookup_path(path : Path, lookup_in_namespace = true, include_private = false, location = path.location) : Type | ASTNode | Nil
      location = nil if path.global?
      (path.global? ? program : self).lookup_path(path.names, lookup_in_namespace, include_private, location)
    end

    # ditto
    def lookup_path(names : Array(String), lookup_in_namespace = true, include_private = false, location = nil) : Type | ASTNode | Nil
      type = self
      names.each_with_index do |name, i|
        # The search must continue in the namespace only for the first path
        # item: for subsequent path items only the parents must be looked up
        type = type.lookup_path_item(name, lookup_in_namespace: lookup_in_namespace && i == 0, include_private: i == 0 || include_private, location: location)
        return unless type

        # Stop if this is the last name
        break if i == names.size - 1

        # An intermediate match could be an ASTNode, for example
        # when searching T::N::X, and T denotes a static array:
        # in this case we can't continue searching past `N`
        return unless type.is_a?(Type)
      end
      type
    end

    # Looks up a single path item relative to *self`.
    #
    # If *lookup_in_namespace* is `true`, if the type is not found
    # in `self` or `self`'s parents, the path item is searched in this
    # type's namespace. This parameter is useful because when writing
    # `Foo::Bar::Baz`, `Foo` should be searched in enclosing namespaces,
    # but `Bar` and `Baz` not.
    def lookup_path_item(name : String, lookup_in_namespace, include_private, location) : Type | ASTNode | Nil
      # First search in our types
      type = types?.try &.[name]?
      if type
        if type.private? && !include_private
          return nil
        end

        return type
      end

      # Then try out parents, but don't search in our parents namespace
      parents.try &.each do |parent|
        match = parent.lookup_path_item(name, lookup_in_namespace: false, include_private: include_private, location: location)
        return match if match
      end

      # Try our namespace, unless we are the top-level
      if lookup_in_namespace && self != program
        return namespace.lookup_path_item(name, lookup_in_namespace, include_private, location)
      end

      nil
    end
  end

  class Program
    def lookup_path_item(name : String, lookup_in_namespace, include_private, location)
      # Check if there's a private type in location
      if location && (original_filename = location.original_filename) &&
         (file_module = file_module?(original_filename)) &&
         (item = file_module.types[name]?)
        return item
      end

      super
    end
  end

  module GenericType
    def lookup_path_item(name : String, lookup_in_namespace, include_private, location)
      # If we are Foo(T) and somebody looks up the type T, we return `nil` because we don't
      # know what type T is, and we don't want to continue search in the namespace
      if type_vars.includes?(name)
        return nil
      end
      super
    end
  end

  class GenericInstanceType
    def lookup_path_item(name : String, lookup_in_namespace, include_private, location)
      # Check if *name* is a type variable
      if type_var = type_vars[name]?
        if type_var.is_a?(Var)
          type_var.type
        else
          type_var
        end
      else
        generic_type.lookup_path_item(name, lookup_in_namespace, include_private, location)
      end
    end
  end

  class UnionType
    def lookup_path_item(name : String, lookup_in_namespace, include_private, location)
      # Union type does not currently inherit GenericClassInstanceType,
      # so we check if *name* is the only type variable of Union(*T)
      if name == "T"
        return program.tuple_of(union_types)
      end
      program.lookup_path_item(name, lookup_in_namespace, include_private, location)
    end
  end

  class TypeDefType
    delegate lookup_path, to: typedef
  end

  class MetaclassType
    delegate lookup_path, to: instance_type
  end

  class GenericClassInstanceMetaclassType
    delegate lookup_path, to: instance_type
  end

  class GenericModuleInstanceMetaclassType
    delegate lookup_path, to: instance_type
  end

  class VirtualType
    delegate lookup_path, to: base_type
  end

  class VirtualMetaclassType
    delegate lookup_path, to: instance_type
  end
end
