package org.jboss.windup.ui;

import java.io.File;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;

import javax.inject.Inject;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.jboss.forge.addon.resource.DirectoryResource;
import org.jboss.forge.addon.resource.FileResource;
import org.jboss.forge.addon.resource.Resource;
import org.jboss.forge.addon.resource.ResourceFactory;
import org.jboss.forge.addon.resource.util.ResourcePathResolver;
import org.jboss.forge.addon.ui.UIProvider;
import org.jboss.forge.addon.ui.command.UICommand;
import org.jboss.forge.addon.ui.context.UIBuilder;
import org.jboss.forge.addon.ui.context.UIContext;
import org.jboss.forge.addon.ui.context.UIExecutionContext;
import org.jboss.forge.addon.ui.context.UIValidationContext;
import org.jboss.forge.addon.ui.input.InputComponent;
import org.jboss.forge.addon.ui.input.InputComponentFactory;
import org.jboss.forge.addon.ui.input.UIInput;
import org.jboss.forge.addon.ui.input.UIInputMany;
import org.jboss.forge.addon.ui.input.UISelectMany;
import org.jboss.forge.addon.ui.input.UISelectOne;
import org.jboss.forge.addon.ui.metadata.UICommandMetadata;
import org.jboss.forge.addon.ui.output.UIOutput;
import org.jboss.forge.addon.ui.progress.UIProgressMonitor;
import org.jboss.forge.addon.ui.result.Result;
import org.jboss.forge.addon.ui.result.Results;
import org.jboss.forge.addon.ui.util.Categories;
import org.jboss.forge.addon.ui.util.Metadata;
import org.jboss.forge.addon.ui.validate.UIValidator;
import org.jboss.windup.config.ConfigurationOption;
import org.jboss.windup.config.ValidationResult;
import org.jboss.windup.exec.WindupProcessor;
import org.jboss.windup.exec.WindupProgressMonitor;
import org.jboss.windup.exec.configuration.WindupConfiguration;
import org.jboss.windup.exec.configuration.options.InputPathOption;
import org.jboss.windup.exec.configuration.options.OutputPathOption;
import org.jboss.windup.exec.configuration.options.OverwriteOption;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.GraphContextFactory;

/**
 * Provides a basic Forge UI implementation for running Windup from within a {@link UIProvider}.
 *
 * @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a>
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 * @author <a href="http://ondra.zizka.cz/">Ondrej Zizka, ozizka at seznam.cz</a>
 */
public class WindupCommand implements UICommand
{
    private Map<ConfigurationOption, InputComponent<?, ?>> inputOptions = new LinkedHashMap<>();

    @Inject
    private InputComponentFactory componentFactory;

    @Inject
    private GraphContextFactory graphContextFactory;

    @Inject
    private WindupProcessor processor;

    @Inject
    private ResourceFactory resourceFactory;

    /*
     * Using a Set because messages seem to be added multiple times by validation. Need to look into this.
     */
    private Set<ValidationResult> promptMessages = new HashSet<>();

    @Override
    public UICommandMetadata getMetadata(UIContext ctx)
    {
        return Metadata.forCommand(getClass()).name("Windup Migrate App").description("Run Windup Migration Analyzer")
                    .category(Categories.create("Platform", "Migration"));
    }

    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void initializeUI(UIBuilder builder) throws Exception
    {
        initializeConfigurationOptionComponents(builder);

        final UIInputMany inputPaths = (UIInputMany) getInputForOption(InputPathOption.class);
        final UIInput outputPath = (UIInput) getInputForOption(OutputPathOption.class);
        outputPath.setDefaultValue(new Callable<DirectoryResource>()
        {
            @Override
            public DirectoryResource call() throws Exception
            {
                Iterable<File> inputPathsIterable = inputPaths.getValue();

                if (inputPathsIterable.iterator().hasNext())
                {
                    // set the default based on the first one

                    File value = inputPathsIterable.iterator().next();
                    File childDirectory = new File(value.getCanonicalFile().getParentFile(), value.getName() + ".report");

                    return resourceFactory.create(DirectoryResource.class, childDirectory);
                }
                return null;
            }

        });

        outputPath.addValidator(new UIValidator()
        {
            @Override
            public void validate(UIValidationContext context)
            {
                File outputFile = (File) getValueForInput(getOption(OutputPathOption.NAME), outputPath);
                final UIInputMany inputPathsUI = (UIInputMany) getInputForOption(InputPathOption.class);
                Iterable<File> inputPathsIterable = inputPathsUI.getValue();

                /**
                 * It would be really nice to be able to use native Resource types here... but we can't "realllly"
                 * do that because the Windup configuration API doesn't understand Forge data types, so instead we
                 * use string comparison and write a test case.
                 */
                //File inputFile = (File) inputPath.getUnderlyingResourceObject();
                for (File inputFile : inputPathsIterable){
                    if (inputFile.equals(outputFile))
                        context.addValidationError(outputPath, "Output file cannot be the same as the input file.");

                    File inputParent = inputFile.getParentFile();
                    while (inputParent != null)
                    {
                        if (inputParent.equals(outputFile))
                            context.addValidationError(outputPath, "Output path must not be a parent of input path.");
                        inputParent = inputParent.getParentFile();
                    }

                    File outputParent = outputFile.getParentFile();
                    while (outputParent != null)
                    {
                        if (outputParent.equals(inputFile))
                            context.addValidationError(inputPathsUI, "Input path must not be a parent of output path.");
                        outputParent = outputParent.getParentFile();
                    }
                }
            }
        });
    }

    @Override
    public void validate(UIValidationContext context)
    {
        for (Entry<ConfigurationOption, InputComponent<?, ?>> entry : this.inputOptions.entrySet())
        {
            final InputComponent<?, ?> inputComponent = entry.getValue();

            ConfigurationOption option = entry.getKey();
            Object inputValue = getValueForInput(option, inputComponent);
            if (inputValue == null && !option.isRequired())
                return;

            ValidationResult result = option.validate(inputValue);

            switch (result.getLevel())
            {
                case ERROR:
                    context.addValidationError(inputComponent, result.getMessage());
                    break;
                case WARNING:
                    context.addValidationWarning(inputComponent, result.getMessage());
                    break;
                case PROMPT_TO_CONTINUE:
                    this.promptMessages.add(result);
                    break;
            }
        }
    }

    @Override
    public Result execute(UIExecutionContext context) throws Exception
    {
        if (!this.promptMessages.isEmpty())
        {
            for (ValidationResult message : promptMessages)
            {
                UIOutput output = context.getUIContext().getProvider().getOutput();
                output.warn(output.out(), message.getMessage());
            }

            if (context.getPrompt().promptBoolean("Would you like to continue?", true) == false)
            {
                return Results.fail("Aborted by the user.");
            }
        }

        WindupConfiguration windupConfiguration = new WindupConfiguration();
        for (Entry<ConfigurationOption, InputComponent<?, ?>> entry : this.inputOptions.entrySet())
        {
            ConfigurationOption option = entry.getKey();
            String name = option.getName();
            Object value = getValueForInput(option, entry.getValue());
            windupConfiguration.setOptionValue(name, value);
        }

        windupConfiguration.useDefaultDirectories();

        Boolean overwrite = (Boolean) windupConfiguration.getOptionMap().get(OverwriteOption.NAME);
        if (overwrite == null)
        {
            overwrite = false;
        }

        if (!overwrite && pathNotEmpty(windupConfiguration.getOutputDirectory().toFile()))
        {
            String promptMsg = "Overwrite all contents of \"" + windupConfiguration.getOutputDirectory().toString()
                        + "\" (anything already in the directory will be deleted)?";
            if (!context.getPrompt().promptBoolean(promptMsg, false))
            {
                String outputPath = windupConfiguration.getOutputDirectory().toString();
                return Results.fail("Files exist in " + outputPath + ", but --overwrite not specified. Aborting!");
            }
        }

        /*
         * Put this in the context for debugging, and unit tests (or anything else that needs it).
         */
        context.getUIContext().getAttributeMap().put(WindupConfiguration.class, windupConfiguration);

        FileUtils.deleteQuietly(windupConfiguration.getOutputDirectory().toFile());
        Path graphPath = windupConfiguration.getOutputDirectory().resolve("graph");
        try (GraphContext graphContext = graphContextFactory.create(graphPath))
        {
            context.getUIContext().getAttributeMap().put(GraphContext.class, graphContext);
            UIProgressMonitor uiProgressMonitor = context.getProgressMonitor();
            WindupProgressMonitor progressMonitor = new WindupProgressMonitorAdapter(uiProgressMonitor);
            windupConfiguration
                        .setProgressMonitor(progressMonitor)
                        .setGraphContext(graphContext);
            processor.execute(windupConfiguration);

            uiProgressMonitor.done();

            Path indexHtmlPath = windupConfiguration.getOutputDirectory().resolve("index.html").normalize().toAbsolutePath();
            return Results.success("Windup report created: " + indexHtmlPath + System.getProperty("line.separator")
                        + "              Access it at this URL: " + indexHtmlPath.toUri());
        }
    }

    @Override
    public boolean isEnabled(UIContext context)
    {
        return true;
    }

    /*
     * Utility methods
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void initializeConfigurationOptionComponents(UIBuilder builder)
    {
        for (final ConfigurationOption option : WindupConfiguration.getWindupConfigurationOptions())
        {
            InputComponent<?, ?> inputComponent = null;
            switch (option.getUIType())
            {
            case SINGLE:
            {
                UIInput<?> inputSingle = componentFactory.createInput(option.getName(), option.getType());
                inputSingle.setDefaultValue(new DefaultValueAdapter(option));
                inputComponent = inputSingle;
                break;
            }
            case MANY:
            {
                // forge can't handle "Path", so use File
                Class<?> optionType = option.getType() == Path.class ? File.class : option.getType();

                UIInputMany<?> inputMany = componentFactory.createInputMany(option.getName(), optionType);
                inputMany.setDefaultValue(new DefaultValueAdapter(option, Iterable.class));
                inputComponent = inputMany;
                break;
            }
            case SELECT_MANY:
            {
                UISelectMany<?> selectMany = componentFactory.createSelectMany(option.getName(), option.getType());
                selectMany.setValueChoices((Iterable) option.getAvailableValues());
                selectMany.setDefaultValue(new DefaultValueAdapter(option, Iterable.class));
                inputComponent = selectMany;
                break;
            }
            case SELECT_ONE:
            {
                UISelectOne<?> selectOne = componentFactory.createSelectOne(option.getName(), option.getType());
                selectOne.setValueChoices((Iterable) option.getAvailableValues());
                selectOne.setDefaultValue(new DefaultValueAdapter(option));
                inputComponent = selectOne;
                break;
            }
            case DIRECTORY:
            {
                UIInput<DirectoryResource> directoryInput = componentFactory.createInput(option.getName(),
                            DirectoryResource.class);
                directoryInput.setDefaultValue(new DefaultValueAdapter(option, DirectoryResource.class));
                inputComponent = directoryInput;
                break;
            }
            case FILE:
            {
                UIInput<?> fileInput = componentFactory.createInput(option.getName(), FileResource.class);
                fileInput.setDefaultValue(new DefaultValueAdapter(option, FileResource.class));
                inputComponent = fileInput;
                break;
            }
            case FILE_OR_DIRECTORY:
            {
                UIInput<?> fileOrDirInput = componentFactory.createInput(option.getName(), FileResource.class);
                fileOrDirInput.setDefaultValue(new DefaultValueAdapter(option, FileResource.class));
                inputComponent = fileOrDirInput;
                break;
            }
            }
            if (inputComponent == null)
            {
                throw new IllegalArgumentException("Could not build input component for: " + option);
            }
            inputComponent.setLabel(option.getLabel());
            inputComponent.setRequired(option.isRequired());
            inputComponent.setDescription(option.getDescription());
            builder.add(inputComponent);
            inputOptions.put(option, inputComponent);
        }
    }

    private ConfigurationOption getOption(String name)
    {
        for (Entry<ConfigurationOption, InputComponent<?, ?>> entry : this.inputOptions.entrySet())
        {
            if (StringUtils.equals(name, entry.getKey().getName()))
            {
                return entry.getKey();
            }
        }
        return null;
    }

    private InputComponent<?, ?> getInputForOption(Class<? extends ConfigurationOption> option)
    {
        for (Entry<ConfigurationOption, InputComponent<?, ?>> entry : this.inputOptions.entrySet())
        {
            if (option.isAssignableFrom(entry.getKey().getClass()))
            {
                return entry.getValue();
            }
        }
        return null;
    }

    private Object getValueForInput(ConfigurationOption option, InputComponent<?, ?> input)
    {
        Object value = input.getValue();
        if (value == null)
        {
            return value;
        }

        if (value instanceof Resource<?>)
        {
            Resource<?> resourceResolved = getResourceResolved((Resource<?>) value);
            return resourceResolved.getUnderlyingResourceObject();
        }

        if (option.getType() == Path.class)
        {
            // these have to be converted
            Set<Path> paths = new LinkedHashSet<>();
            if (value instanceof List)
            {
                for (Object path : (List) value)
                {
                    if (path instanceof File)
                        path = ((File) path).toPath();
                    paths.add((Path) path);
                }
            }
            value = paths;
        }

        return value;
    }

    private Resource<?> getResourceResolved(Resource<?> value)
    {
        Resource<?> resource = (Resource<?>) value;
        File file = (File) resource.getUnderlyingResourceObject();
        return new ResourcePathResolver(resourceFactory, resource, file.getPath()).resolve().get(0);
    }

    private boolean pathNotEmpty(File f)
    {
        if (f.exists() && !f.isDirectory())
        {
            return true;
        }
        if (f.isDirectory() && f.listFiles() != null && f.listFiles().length > 0)
        {
            return true;
        }
        return false;
    }

}
