package com.atlassian.stash.internal.plugin;

import com.atlassian.bitbucket.ui.ContextualFormFragment;
import com.atlassian.bitbucket.ui.PluginFormFragments;
import com.atlassian.bitbucket.ui.PluginFormFragmentsFactory;
import com.atlassian.bitbucket.ui.ValidationErrors;
import com.atlassian.bitbucket.util.ModuleDescriptorUtils;
import com.atlassian.bitbucket.view.ViewStreamIOException;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.plugin.web.Condition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.util.MoreCollectors.toImmutableList;
import static java.util.Objects.requireNonNull;

@AvailableToPlugins(PluginFormFragmentsFactory.class)
@Component("pluginFormFragmentsFactory")
public class PluginFormFragmentsFactoryImpl implements PluginFormFragmentsFactory {

    private final PluginAccessor pluginAccessor;

    @Autowired
    public PluginFormFragmentsFactoryImpl(PluginAccessor pluginAccessor) {
        this.pluginAccessor = pluginAccessor;
    }

    @Override
    public boolean existsForKey(@Nonnull String formKey) {
        requireNonNull(formKey, "formKey");

        return getFormFragmentDescriptors()
                .stream()
                .map(FormFragmentModuleDescriptor::getLocation)
                .anyMatch(formKey::equals);
    }

    @Nonnull
    @Override
    public PluginFormFragments forKey(@Nonnull String formKey, @Nonnull Map<String, Object> context) {
        requireNonNull(formKey, "formKey");
        requireNonNull(context, "context");

        Stream<FormFragmentModuleDescriptor> descriptors = getFormFragmentDescriptors()
                .stream()
                .filter(descriptor -> {
                    if (formKey.equals(descriptor.getLocation())) {
                        Condition condition = descriptor.getCondition();
                        return condition == null || condition.shouldDisplay(Collections.unmodifiableMap(context));
                    }
                    return false;
                });

        return new PluginFormFragmentsImpl(ModuleDescriptorUtils.toSortedModules(descriptors).collect(toImmutableList()), context);
    }

    private List<FormFragmentModuleDescriptor> getFormFragmentDescriptors() {
        return pluginAccessor.getEnabledModuleDescriptorsByClass(FormFragmentModuleDescriptor.class);
    }

    private static class PluginFormFragmentsImpl implements PluginFormFragments {

        private final Map<String, Object> context;
        private final Iterable<ContextualFormFragment> formFragments;

        private PluginFormFragmentsImpl(Iterable<ContextualFormFragment> formFragments, Map<String, Object> context) {
            this.context = context;
            this.formFragments = formFragments;
        }

        @Override
        public void execute(@Nonnull Map<String, String[]> requestParams) {
            formFragments.forEach(fragment -> fragment.execute(requestParams, newContext()));
        }

        @Nonnull
        @Override
        public String getErrorHtml(@Nonnull Map<String, String[]> requestParams, @Nonnull Map<String, Collection<String>> fieldErrors) {
            StringBuilder fieldsHtml = new StringBuilder();
            for (ContextualFormFragment fragment : formFragments) {
                try {
                    fragment.doError(fieldsHtml, requestParams, fieldErrors, newContext());
                } catch (IOException e) {
                    throw new ViewStreamIOException("Failed to render FormFragment doError: " + fragment, e);
                }
            }
            return fieldsHtml.toString();
        }

        @Nonnull
        @Override
        public String getViewHtml() {
            StringBuilder fieldsHtml = new StringBuilder();
            for (ContextualFormFragment fragment : formFragments) {
                try {
                    fragment.doView(fieldsHtml, newContext());
                } catch (IOException e) {
                    throw new ViewStreamIOException("Failed to render FormFragment doView: " + fragment, e);
                }
            }
            return fieldsHtml.toString();
        }

        @Override
        public void validate(@Nonnull Map<String, String[]> requestParams, @Nonnull ValidationErrors errors) {
            formFragments.forEach(fragment -> fragment.validate(requestParams, errors, newContext()));
        }

        /**
         * The mutability of the context provided to {@link ContextualFormFragment fragments} was never documented,
         * but it has been mutable since its inception. As a result, several fragments are putting their own values
         * into the context and then passing it through to the {@code SoyTemplateRenderer} (for example).
         * <p>
         * Long term, fragments which need a specific rendering context should be creating their own map, rather than
         * modifying the context map, because that map is passed to multiple fragments. Until then, to prevent one
         * fragment from modifying the context in a way that another fragment can see, this method constructs a new
         * map, containing the original context items, to be passed into each fragment.
         * <p>
         * This method should be removed for Stash 4.0 and instead it should be enforced that fragments should not
         * modify the context map.
         *
         * @return a new mutable map containing the original context
         */
        private Map<String, Object> newContext() {
            return new HashMap<>(context);
        }
    }
}