# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2017-2026, by Samuel Williams.

require "set"

module Build
	module Dependency
		# An error raised when dependencies cannot be resolved.
		class UnresolvedDependencyError < StandardError
			# Initialize the error with the unresolved chain.
			# @parameter chain [Chain] The chain with unresolved dependencies.
			def initialize(chain)
				super "Unresolved dependency chain: #{chain.unresolved.inspect}!"
				
				@chain = chain
			end
			
			attr :chain
		end
		
		TOP = Depends.new("<top>").freeze
		
		# Base class for resolving dependencies.
		class Resolver
			# Initialize an empty resolver.
			def initialize
				@resolved = {}
				@ordered = []
				@provisions = []
				@unresolved = []
				@conflicts = {}
			end
			
			attr :resolved
			attr :ordered
			attr :provisions
			attr :unresolved
			attr :conflicts
			
			# Freeze the resolver and all its internal state.
			def freeze
				return self if frozen?
				
				@resolved.freeze
				@ordered.freeze
				@provisions.freeze
				@unresolved.freeze
				@conflicts.freeze
				
				super
			end
			
			protected
			
			def expand_nested(dependencies, provider)
				dependencies.each do |dependency|
					expand(Depends[dependency], provider)
				end
			end
			
			def expand_provision(provision, dependency)
				provider = provision.provider
				
				# If the provision was an Alias, make sure to resolve the alias first:
				if provision.alias?
					# puts "** Resolving alias #{provision} (#{provision.dependencies.inspect})"
					expand_nested(provision.dependencies, provider)
				end
				
				# puts "** Checking for #{provider.inspect} in #{resolved.inspect}"
				unless @resolved.include?(provider)
					# We are now satisfying the provider by expanding all its own dependencies:
					@resolved[provider] = provision
					
					# Make sure we satisfy the provider's dependencies first:
					expand_nested(provider.dependencies, provider)
					
					# puts "** Appending #{dependency} -> ordered"
					
					# Add the provider to the ordered list.
					@ordered << Resolution.new(provision, dependency)
				end
				
				# This goes here because we want to ensure 1/ that if 
				unless provision == nil or provision.alias?
					# puts "** Appending #{dependency} -> provisions"
					
					# Add the provision to the set of required provisions.
					@provisions << provision
				end
				
				# For both @ordered and @provisions, we ensure that for [...xs..., x, ...], x is satisfied by ...xs....
			end
			
			def expand(dependency, parent)
				# puts "** Expanding #{dependency.inspect} from #{parent.inspect} (private: #{dependency.private?})"
				
				if @resolved.include?(dependency)
					# puts "** Already resolved dependency!"
					
					return nil
				end
				
				# The find_provider method is abstract in this base class.
				provisions = expand_dependency(dependency, parent)
				
				if provisions.empty?
					# puts "** Couldn't resolve #{dependency}"
					@unresolved << [dependency, parent]
				else
					# We will now satisfy this dependency by satisfying any dependent dependencies, but we no longer need to revisit this one.
					# puts "** Resolved #{dependency}"
					@resolved[dependency] = provisions
					
					provisions.each do |provision|
						expand_provision(provision, dependency)
					end
				end
			end
		end
	end
end
