/*
 * Copyright 2024 the original author or authors.
 *
 * 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
 *
 *      http://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.
 */
package org.gradle.launcher.cli;

import com.google.common.annotations.VisibleForTesting;
import org.gradle.api.Action;
import org.gradle.api.internal.file.FileCollectionFactory;
import org.gradle.api.launcher.cli.WelcomeMessageConfiguration;
import org.gradle.api.launcher.cli.WelcomeMessageDisplayMode;
import org.gradle.api.logging.configuration.LoggingConfiguration;
import org.gradle.cli.CommandLineArgumentException;
import org.gradle.cli.CommandLineParser;
import org.gradle.cli.OptionCategory;
import org.gradle.cli.ParsedCommandLine;
import org.gradle.configuration.DefaultBuildClientMetaData;
import org.gradle.configuration.GradleLauncherMetaData;
import org.gradle.initialization.BuildClientMetaData;
import org.gradle.initialization.layout.BuildLayoutFactory;
import org.gradle.internal.Actions;
import org.gradle.internal.buildevents.BuildExceptionReporter;
import org.gradle.internal.installation.CurrentGradleInstallation;
import org.gradle.internal.logging.DefaultLoggingConfiguration;
import org.gradle.internal.logging.LoggingConfigurationBuildOptions;
import org.gradle.internal.logging.LoggingManagerFactory;
import org.gradle.internal.logging.LoggingManagerInternal;
import org.gradle.internal.logging.services.LoggingServiceRegistry;
import org.gradle.internal.logging.text.StyledTextOutputFactory;
import org.gradle.internal.nativeintegration.services.NativeServices;
import org.gradle.internal.problems.failure.DefaultFailureFactory;
import org.gradle.internal.service.ServiceRegistry;
import org.gradle.internal.service.ServiceRegistryBuilder;
import org.gradle.internal.service.scopes.BasicGlobalScopeServices;
import org.gradle.internal.service.scopes.Scope;
import org.gradle.launcher.bootstrap.CommandLineActionFactory;
import org.gradle.launcher.bootstrap.ExecutionListener;
import org.gradle.launcher.cli.converter.BuildLayoutConverter;
import org.gradle.launcher.cli.converter.BuildOptionBackedConverter;
import org.gradle.launcher.cli.converter.InitialPropertiesConverter;
import org.gradle.launcher.cli.converter.LayoutToPropertiesConverter;
import org.gradle.launcher.cli.converter.WelcomeMessageBuildOptions;
import org.gradle.launcher.cli.internal.HelpRenderer;
import org.gradle.launcher.cli.internal.VersionInfoRenderer;
import org.gradle.launcher.configuration.AllProperties;
import org.gradle.launcher.configuration.BuildLayoutResult;
import org.gradle.launcher.configuration.InitialProperties;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * <p>Responsible for converting a set of command-line arguments into a {@link Runnable} action.</p>
 */
public class DefaultCommandLineActionFactory implements CommandLineActionFactory {
    private static final String HELP = "h";
    private static final String VERSION = "v";
    private static final String VERSION_CONTINUE = "V";

    /**
     * <p>Converts the given command-line arguments to an {@link Action} which performs the action requested by the
     * command-line args.
     *
     * @param args The command-line arguments.
     * @return The action to execute.
     */
    @Override
    public CommandLineExecution convert(List<String> args) {
        ServiceRegistry loggingServices = createLoggingServices();

        LoggingConfiguration loggingConfiguration = new DefaultLoggingConfiguration();

        return new WithLogging(loggingServices,
            args,
            loggingConfiguration,
            new ParseAndBuildAction(loggingServices, args),
            new BuildExceptionReporter(
                loggingServices.get(StyledTextOutputFactory.class),
                loggingConfiguration,
                clientMetaData(),
                DefaultFailureFactory.withDefaultClassifier()
            ));
    }

    private static BuildClientMetaData clientMetaData() {
        return new DefaultBuildClientMetaData(new GradleLauncherMetaData());
    }

    /**
     * This method is left visible so that tests can override it to inject {@link CommandLineActionCreator}s which
     * don't actually attempt to run the build per normally.
     *
     * @param loggingServices logging services to use when instantiating any {@link CommandLineActionCreator}s
     * @param basicServices basic services to use when instantiating any {@link CommandLineActionCreator}s
     * @param actionCreators collection of {@link CommandLineActionCreator}s to which to add a new {@link BuildActionsFactory}
     */
    @VisibleForTesting
    protected void createBuildActionFactoryActionCreator(ServiceRegistry loggingServices, ServiceRegistry basicServices, List<CommandLineActionCreator> actionCreators) {
        actionCreators.add(new BuildActionsFactory(loggingServices, basicServices, CurrentGradleInstallation.locate()));
    }

    /**
     * This method is left visible so that tests can override it to inject mocked {@link ServiceRegistry}s.
     *
     * @return the created {@link ServiceRegistry}
     */
    @VisibleForTesting
    protected ServiceRegistry createLoggingServices() {
        return LoggingServiceRegistry.newCommandLineProcessLogging();
    }

    private static class BuiltInActionCreator implements CommandLineActionCreator {
        @Override
        public void configureCommandLineParser(CommandLineParser parser) {
            parser.option(HELP, "?", "help").hasDescription("Shows this help message.").hasCategory(OptionCategory.HELP);
            parser.option(VERSION, "version").hasDescription("Prints version information and exits.").hasCategory(OptionCategory.HELP);
            parser.option(VERSION_CONTINUE, "show-version").hasDescription("Prints version information and continues.").hasCategory(OptionCategory.HELP);
        }

        @Override
        @Nullable
        public Action<? super ExecutionListener> createAction(CommandLineParser parser, ParsedCommandLine commandLine, Parameters parameters) {
            if (commandLine.hasOption(HELP)) {
                return new ShowUsageAction(parser, commandLine);
            }
            if (commandLine.hasOption(VERSION)) {
                return new ShowVersionAction(parameters);
            }
            return null;
        }
    }

    /**
     * This {@link CommandLineActionCreator} is responsible for handling any command line options that produce {@link ContinuingAction}s.
     */
    private static class ContinuingActionCreator extends NonParserConfiguringCommandLineActionCreator {
        @Override
        @Nullable
        public ContinuingAction<? super ExecutionListener> createAction(CommandLineParser parser, ParsedCommandLine commandLine, Parameters parameters) {
            if (commandLine.hasOption(DefaultCommandLineActionFactory.VERSION_CONTINUE)) {
                return (ContinuingAction<ExecutionListener>) executionListener -> new ShowVersionAction(parameters).execute(executionListener);
            }
            return null;
        }
    }

    private static class CommandLineParseFailureAction implements Action<ExecutionListener> {
        private final CommandLineParser parser;
        private final Exception exception;

        private final List<String> args;

        public CommandLineParseFailureAction(CommandLineParser parser, Exception exception, List<String> args) {
            this.parser = parser;
            this.exception = exception;
            this.args = args;
        }

        @Override
        public void execute(ExecutionListener executionListener) {
            System.err.println();
            System.err.println(exception.getMessage());
            String output = HelpRenderer.render(parser, getSuggestedTaskSelector(), false);
            System.err.print(output);
            executionListener.onFailure(exception);
        }

        @Nullable
        private String getSuggestedTaskSelector() {
            for (String arg : args) {
                if (!arg.startsWith("-")) {
                    return arg;
                }
            }
            return null;
        }
    }

    private static class ShowUsageAction implements Action<ExecutionListener> {
        private final CommandLineParser parser;
        private final ParsedCommandLine commandLine;

        public ShowUsageAction(CommandLineParser parser, ParsedCommandLine commandLine) {
            this.parser = parser;
            this.commandLine = commandLine;
        }

        @Override
        public void execute(ExecutionListener executionListener) {
            String output = HelpRenderer.render(parser, getSuggestedTaskSelector(), true);
            System.out.print(output);
        }

        @Nullable
        private String getSuggestedTaskSelector() {
            if (!commandLine.getExtraArguments().isEmpty()) {
                return commandLine.getExtraArguments().get(0);
            } else {
                return null;
            }
        }
    }

    public static class ShowVersionAction implements Action<ExecutionListener> {
        private final Parameters parameters;

        public ShowVersionAction(Parameters parameters) {
            this.parameters = parameters;
        }

        @Override
        public void execute(ExecutionListener executionListener) {
            String versionInfo = VersionInfoRenderer.renderWithLauncherJvm(parameters.getDaemonParameters().getRequestedJvmCriteria().toString());
            System.out.print(versionInfo);
        }
    }

    /**
     * This {@link Action} will create new {@link Action}s that will be immediately executed.
     *
     * This class accomplishes this be maintaining a list of {@link CommandLineActionCreator}s which can each attempt to
     * create an {@link Action} from the given CLI args, and handles the logic for deciding whether or not to continue processing
     * based on whether the result is a {@link ContinuingAction} or not.  It allows for injecting alternate Creators which
     * won't actually attempt to run a build via the containing class' {@link #createBuildActionFactoryActionCreator(ServiceRegistry, ServiceRegistry, List)}
     * method - this is why this class is not {@code static}.
     */
    private class ParseAndBuildAction implements Action<ExecutionListener> {
        private final ServiceRegistry loggingServices;
        private final List<String> args;
        private final List<CommandLineActionCreator> actionCreators;
        private final CommandLineParser parser = new CommandLineParser();

        private ParseAndBuildAction(ServiceRegistry loggingServices, List<String> args) {
            this.loggingServices = loggingServices;
            this.args = args;

            actionCreators = new ArrayList<>();
            actionCreators.add(new BuiltInActionCreator());
            actionCreators.add(new ContinuingActionCreator());
        }

        @Override
        public void execute(ExecutionListener executionListener) {
            ServiceRegistry basicServices = createBasicGlobalServices(loggingServices);
            BuildEnvironmentConfigurationConverter buildEnvironmentConfigurationConverter = new BuildEnvironmentConfigurationConverter(
                basicServices.get(BuildLayoutFactory.class),
                basicServices.get(FileCollectionFactory.class));
            buildEnvironmentConfigurationConverter.configure(parser);

            // This must be added only during execute, because the actual constructor is called by various tests and this will not succeed if called then
            createBuildActionFactoryActionCreator(loggingServices, basicServices, actionCreators);
            configureCreators();

            Action<? super ExecutionListener> action;
            try {
                ParsedCommandLine commandLine = parser.parse(args);
                Parameters parameters = buildEnvironmentConfigurationConverter.convertParameters(commandLine, null);
                action = createAction(parser, commandLine, parameters);
            } catch (CommandLineArgumentException e) {
                action = new CommandLineParseFailureAction(parser, e, args);
            }

            action.execute(executionListener);
        }

        private void configureCreators() {
            actionCreators.forEach(creator -> creator.configureCommandLineParser(parser));
        }

        public Action<? super ExecutionListener> createAction(CommandLineParser parser, ParsedCommandLine commandLine, Parameters parameters) {
            List<Action<? super ExecutionListener>> actions = new ArrayList<>(2);
            for (CommandLineActionCreator actionCreator : actionCreators) {
                Action<? super ExecutionListener> action = actionCreator.createAction(parser, commandLine, parameters);
                if (action != null) {
                    actions.add(action);
                    if (!(action instanceof ContinuingAction)) {
                        break;
                    }
                }
            }

            if (!actions.isEmpty()) {
                return Actions.composite(actions);
            }

            throw new UnsupportedOperationException("No action factory for specified command-line arguments.");
        }
    }

    @VisibleForTesting
    public ServiceRegistry createBasicGlobalServices(ServiceRegistry loggingServices) {
        return ServiceRegistryBuilder.builder()
            .scopeStrictly(Scope.Global.class)
            .displayName("Basic global services")
            .parent(loggingServices)
            .parent(NativeServices.getInstance())
            .provider(new BasicGlobalScopeServices())
            .build();
    }

    /**
     * Abstract type for any {@link CommandLineActionCreator} that does not make use of the {@link #configureCommandLineParser(CommandLineParser)}
     * method.
     */
    private static abstract class NonParserConfiguringCommandLineActionCreator implements CommandLineActionCreator {
        @Override
        public void configureCommandLineParser(CommandLineParser parser) {
            // no-op
        }
    }

    private static class WithLogging implements CommandLineExecution {
        private final ServiceRegistry loggingServices;
        private final List<String> args;
        private final LoggingConfiguration loggingConfiguration;
        private final Action<ExecutionListener> action;
        private final Action<Throwable> reporter;

        WithLogging(ServiceRegistry loggingServices, List<String> args, LoggingConfiguration loggingConfiguration, Action<ExecutionListener> action, Action<Throwable> reporter) {
            this.loggingServices = loggingServices;
            this.args = args;
            this.loggingConfiguration = loggingConfiguration;
            this.action = action;
            this.reporter = reporter;
        }

        @Override
        public void execute(ExecutionListener executionListener) {
            BuildOptionBackedConverter<WelcomeMessageConfiguration> welcomeMessageConverter = new BuildOptionBackedConverter<>(new WelcomeMessageBuildOptions());
            BuildOptionBackedConverter<LoggingConfiguration> loggingBuildOptions = new BuildOptionBackedConverter<>(new LoggingConfigurationBuildOptions());
            InitialPropertiesConverter propertiesConverter = new InitialPropertiesConverter();
            BuildLayoutConverter buildLayoutConverter = new BuildLayoutConverter();
            LayoutToPropertiesConverter layoutToPropertiesConverter = new LayoutToPropertiesConverter(new BuildLayoutFactory());
            Map<String, String> environmentVariables = System.getenv();

            BuildLayoutResult buildLayout = buildLayoutConverter.defaultValues();

            CommandLineParser parser = new CommandLineParser();
            propertiesConverter.configure(parser);
            buildLayoutConverter.configure(parser);
            loggingBuildOptions.configure(parser);

            parser.allowUnknownOptions();
            parser.allowMixedSubcommandsAndOptions();

            WelcomeMessageConfiguration welcomeMessageConfiguration = new WelcomeMessageConfiguration(WelcomeMessageDisplayMode.ONCE);

            try {
                ParsedCommandLine parsedCommandLine = parser.parse(args);
                InitialProperties initialProperties = propertiesConverter.convert(parsedCommandLine);

                // Calculate build layout, for loading properties and other logging configuration
                buildLayout = buildLayoutConverter.convert(initialProperties, parsedCommandLine, null);

                // Read *.properties files
                AllProperties properties = layoutToPropertiesConverter.convert(initialProperties, buildLayout);

                // Calculate the logging configuration
                loggingBuildOptions.convert(parsedCommandLine, properties.getProperties(), environmentVariables, loggingConfiguration);

                // Get configuration for showing the welcome message
                welcomeMessageConverter.convert(parsedCommandLine, properties.getProperties(), environmentVariables, welcomeMessageConfiguration);
            } catch (CommandLineArgumentException e) {
                // Ignore, deal with this problem later
            }

            LoggingManagerInternal loggingManager = loggingServices.get(LoggingManagerFactory.class).createLoggingManager();
            loggingManager.setLevelInternal(loggingConfiguration.getLogLevel());
            loggingManager.start();
            try {
                Action<ExecutionListener> exceptionReportingAction =
                    new ExceptionReportingAction(reporter, loggingManager,
                        new NativeServicesInitializingAction(buildLayout, loggingConfiguration, loggingManager,
                            new WelcomeMessageAction(buildLayout, welcomeMessageConfiguration,
                                new DebugLoggerWarningAction(loggingConfiguration, action))));
                exceptionReportingAction.execute(executionListener);
            } finally {
                loggingManager.stop();
            }
        }
    }
}
