use	v6.d;

use	Glob::Grammar;
use	Glob::ToRegexActions;

role	Implementation::Loader {
	has	Lock	%!library-locks;

	=begin pod

	=head1 method load-library

	method	load-library(
		Str :$type,		     # The type to load, as a string
		Str :$module-name,   # The module to load, as a string; defaults to $type
		Str :$does,          # When the type is loaded, check if it does this role
		Bool :$return-type = False, # Return the type object instead of an instance
		*%parameters            # Additional parameters to pass to the type's .new() method
	)

	Loads the library in question, and makes an object of the named type.
	Supports separating module name from type name, role verification, and returning type objects.
	=end pod
	method	load-library(
		Str :$type,
		Str :$module-name,
		Str :$does,
		Bool :$return-type = False,
		*%parameters
	) {
		# Determine which module to load
		my $module-to-load = $module-name // $type;
		
		# Ensure we have a type name (for backward compatibility)
		my $type-name = $type // $module-to-load;
		
		# Backward compatibility: if neither is provided, error
		unless $module-to-load.defined {
			die "Error: Either :module-name or :type must be provided";
		}
		
		%!library-locks{$module-to-load}:exists or %!library-locks{$module-to-load} = Lock.new();
		
		my $result = %!library-locks{$module-to-load}.protect: {
			# Load the module
			my \M = (require ::($module-to-load));
			
			# If type name differs from module name, resolve it
			my \Type = $type-name eq $module-to-load ?? M !! ::($type-name);
			
			# Verify role composition if specified
			if $does.defined {
				my \Role = do {
					my $role-symbol;
					try {
						$role-symbol = ::($does);
					}
					$role-symbol // (require ::($does));
				}
				unless Type.^does(Role) {
					die "Type {Type.^name} does not do role {$does}";
				}
			}
			
			# Return type object or instance
			if $return-type {
				return Type;
			} else {
				return Type.new(|%parameters);
			}
		}
		
		without $result { .throw }
		return $result;
	}

	=begin pod

	=head1 method available-modules

	method available-modules(@lib-paths = [])

	Scans the specified library paths and installed modules to discover all available Raku modules.

	Returns a sorted list of unique module names found in both the provided library paths and the
	installed module repositories.

	=head2 Parameters

	=item @lib-paths - An array of paths to search for .rakumod files. If empty, only installed modules are returned.

	=head2 Why use this?

	Use this method when you need to:
	=item Discover what modules are available in your system or custom library paths
	=item Build dynamic module selection interfaces
	=item Validate that a module exists before attempting to load it
	=item Generate lists of available implementations for plugin systems

	The method recursively searches directories for .rakumod files and also queries the Raku
	module repository chain for installed modules, giving you a comprehensive view of what's
	available.

	Note that, if you plan on filtering the modules you may be better off with find-module-pattern instead.

	=end pod
	method available-modules(@lib-paths = []) {
		my @lib-mods;
		for @lib-paths.flat>>.IO -> $root {
			my @stack = @($root);
			my @lib-these = gather while @stack {
				with @stack.pop {
					when :d { @stack.append: .dir }
					when .extension.lc eq 'rakumod' {
						take .relative($root).Str.subst('.rakumod', '').subst(/\//, '::', :g)
					}
				}
			}
			@lib-mods.push(|@lib-these);
		}

		my @installed = $*REPO.repo-chain
			.grep(*.^can('installed'))
			.map(*.installed)
			.flat.map(*.meta<name>)
			.grep(*.^name eq 'Str');

		my @all-mods = (@lib-mods, @installed).flat.unique.sort;
		return @all-mods;
	}

	=begin pod

	=head1 method find-module-pattern

	method find-module-pattern(:@paths = [], :@regexes = [], :@globs = [])

	Finds module names that match specified patterns using either regular expressions or glob patterns.

	This method first calls C<available-modules> to get all available modules, then filters them
	based on the provided patterns.

	=head2 Parameters

	=item :@paths - Library paths to search (passed to C<available-modules>)
	=item :@regexes - Array of regular expressions to match against module names
	=item :@globs - Array of glob patterns (e.g., "Foo::Bar::*") to match against module names

	=head2 Why use this?

	Use this method when you need to:
	=item Find all modules matching a specific naming pattern (e.g., all "Implementation::*" modules)
	=item Discover plugins or implementations that follow a naming convention
	=item Filter available modules before loading them
	=item Build dynamic module discovery based on patterns rather than exact names

	This is particularly useful for plugin systems where modules follow a naming convention, allowing
	you to discover and work with multiple implementations without hardcoding their names. The glob
	pattern support makes it easy to use shell-like wildcards (e.g., "MyApp::*::Handler") which are
	more intuitive than writing regular expressions.

	=head2 Example

	=begin code
	# Find all modules matching the glob pattern
	my @found = $loader.find-module-pattern(globs => ['Implementation::*::Backend']);
	
	# Find modules using regex
	my @found = $loader.find-module-pattern(regexes => [/^ Implementation \:\: \w+ \:\: Backend $/]);
	=end code

	=end pod
	method find-module-pattern(:@paths = [], :@regexes = [], :@globs = []) {
		# Get the regex to use with .available-modules()
		my @use-regexes;
		given True {
			when @regexes.Bool {
				@use-regexes.push: |@regexes;
				proceed;
			}
			when @globs.Bool {
				for @globs -> $glob {
					my $match = Glob::Grammar.parse($glob, actions => Glob::ToRegexActions.new());
					my Str $pattern = $match.made;
					@use-regexes.push: qq|rx/$pattern/|.EVAL;
				}
				proceed;
			}
			when ! @globs and !@regexes {
				die "Error: Please pass either globs or regexen";
				# This next line would load every possible module, which probably isn't the greatest idea
				#@use-regexes.push: /./;
			}
		}

		# Call .available-modules()
		my @all-mods = self.available-modules(@paths);
		my @module-names;
		for @use-regexes -> $regex {
			@module-names.push: |@all-mods.grep($regex);
		}
		return @module-names;
	}

	=begin pod

	=head1 method load-module-pattern

	method load-module-pattern(
		:@paths = [], :@regexes = [], :@globs = [], :@modules = [] is copy,
		Str :$does
	)

	Loads multiple modules matching specified patterns and returns a hash of successful and failed loads.

	This method combines pattern matching (via C<find-module-pattern>) with module loading (via
	C<load-library>), making it easy to bulk-load modules that match certain criteria.

	=head2 Parameters

	=item :@paths - Library paths to search for modules
	=item :@regexes - Regular expressions to match module names
	=item :@globs - Glob patterns to match module names
	=item :@modules - Optional pre-specified list of module hashes to load (skips pattern matching)
	=item :$does - Optional role name that loaded types must implement

	=head2 Returns

	Returns a list of two hashes: C<(%passes, %fails)>
	=item C<%passes> - Hash mapping module names to successfully loaded instances
	=item C<%fails> - Hash mapping module names to exception objects for failed loads

	=head2 Why use this?

	Use this method when you need to:
	=item Load multiple plugin modules that follow a naming pattern
	=item Implement a plugin system where you want to discover and load all available implementations
	=item Bulk-load modules with error handling (some may fail, others succeed)
	=item Load modules conditionally based on patterns while verifying they implement a specific role

	This is the high-level method that combines discovery and loading, making it ideal for plugin
	systems where you want to find all matching modules and load them in one operation. The method
	gracefully handles failures, allowing you to see which modules loaded successfully and which
	failed, rather than stopping on the first error.

	=head2 Example

	=begin code
	# Load all backend implementations
	my (%passes, %fails) = $loader.load-module-pattern(
		globs => ['MyApp::Backend::*'],
		does => 'MyApp::Backend'
	);
	
	# Use the successfully loaded backends
	for %passes.kv -> $name, $backend {
		say "Loaded backend: $name";
		$backend.process();
	}
	
	# Report failures
	if %fails {
		warn "Failed to load: " ~ %fails.keys.join(', ');
	}
	=end code

	=end pod
	method load-module-pattern(
		:@paths = [], :@regexes = [], :@globs = [], :@modules is copy = [],
		Str :$does
	) {
		if ! @modules.Bool {
			my @module-names = self.find-module-pattern(:@paths, :@regexes, :@globs);
			for @module-names -> $module-name {
				@modules.push: {
					module-name => $module-name,
					type => $module-name,
					does => $does,
				};
			}
		}

		my %passes;
		my %fails;
		for @modules -> $module {
			my $module-name = $module<module-name>;
			my $object = try self.load-library(|%$module);
			if $! {
				%fails{$module-name} = $!;
			} else {
				%passes{$module-name} = $object;
			}
		}

		return %passes, %fails;
	}
} # End Loader