package com.atlassian.stash.internal.plugin.web.fragments;

import com.atlassian.plugin.Plugin;
import com.atlassian.plugin.PluginParseException;
import com.atlassian.plugin.hostcontainer.HostContainer;
import com.atlassian.plugin.module.ModuleFactory;
import com.atlassian.plugin.util.validation.ValidationPattern;
import com.atlassian.plugin.webresource.WebResourceIntegration;
import com.atlassian.stash.internal.plugin.PreprocessedWebResourceModuleDescriptor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.dom4j.Element;

import java.util.Collections;
import java.util.List;
import java.util.Set;

import static com.atlassian.plugin.util.validation.ValidationPattern.test;

public class WebFragmentLocationModuleDescriptor extends PreprocessedWebResourceModuleDescriptor {

    public static final String XML_ELEMENT_NAME = "web-fragment-location".intern();
    private final ClientWebInterfaceManager clientWebInterfaceManager;
    private final ClientPageDataHandler clientPageDataHandler;
    private final WebResourceIntegration webResourceIntegration;

    private static enum FragmentType {
        ITEM,
        SECTION,
        PANEL,
        FRAGMENT
    }

    private FragmentType fragmentType;

    private String location;

    public WebFragmentLocationModuleDescriptor(ModuleFactory moduleFactory, HostContainer hostContainer,
                                               ClientWebInterfaceManager clientWebInterfaceManager, ClientPageDataHandler clientPageDataHandler,
                                               WebResourceIntegration webResourceIntegration) {
        super(moduleFactory, hostContainer);
        this.clientWebInterfaceManager = clientWebInterfaceManager;
        this.clientPageDataHandler = clientPageDataHandler;
        this.webResourceIntegration = webResourceIntegration;
    }

    @Override
    protected void provideValidationRules(ValidationPattern pattern) {
        super.provideValidationRules(pattern);
        pattern.rule(test("@type").withError("type attribute is required for web-fragment-location. Must be one of: 'item', 'panel', or 'section'."));
        // Can't get this working. Who decided XPath was a good idea?
        //pattern.rule(test("*[lower-case(@type)='item' or lower-case(@type)='section' or lower-case(@type)='panel']").withError("type attribute must be 'item', 'panel', or 'section'"));
        pattern.rule(test("@location").withError("location attribute is required for web-fragment-location"));
    }

    @Override
    public void init(Plugin plugin, Element element) throws PluginParseException {
        super.init(plugin, element);

        // if someone can get XPath working, this exception handling can be moved into provideValidationRules
        try {
           fragmentType = FragmentType.valueOf(element.attributeValue("type").toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new PluginParseException("type attribute must be 'item', 'panel', 'section', or 'fragment'");
        }

        location = element.attributeValue("location");
    }

    @Override
    protected Element generateElement(Element originalElement) {
        Element generated = super.generateElement(originalElement);
        AbstractClientWebFragmentModuleDescriptor.addClientWebFragmentDependency(generated);
        return generated;
    }

    /**
     * Find all the client-web-panels for a set of locations.
     * @param locations the locations in which to look for web panels.
     * @return set of complete module keys for client web panels dependencies.
     */
    private Set<String> getWebPanelDependencies(Iterable<String> locations) {
        Set<String> dependencies = Sets.newHashSet();
        for (String location : locations) {
            for (ClientWebPanelModuleDescriptor panel : clientWebInterfaceManager.getPanels(location)) {
                dependencies.add(panel.getCompleteKey());
            }
        }
        return dependencies;
    }

    /**
     * Find all the client-web-items for a set of sections.
     * @param sections the sections in which to look for web items.
     * @return set of complete module keys for client web item dependencies.
     */
    private Set<String> getWebItemDependencies(Iterable<String> sections) {
        Set<String> dependencies = Sets.newHashSet();
        for (String location : sections) {
            for (ClientWebItemModuleDescriptor item : clientWebInterfaceManager.getItems(location)) {
                dependencies.add(item.getCompleteKey());
            }
        }
        return dependencies;
    }

    private void requirePageData(Iterable<String> locations) {
        for (String location : locations) {
            String key = "client_web_fragments_" + location;
            if (!webResourceIntegration.getRequestCache().containsKey(key)) {
                clientPageDataHandler.requirePageData(location, Collections.<String, Object>emptyMap());
                webResourceIntegration.getRequestCache().put(key, true);
            }
        }
    }

    /**
     * In order to support web sections without addition configuration, we also grab and web fragments whose location
     * matches a web section dependency's key (or location and key). This enables the following usage:
     * <code>
     * &lt;client-resource&gt;
     *     &lt;web-section-location&gt;foo&lt;/web-section-location&gt;
     * &lt;/client-resource&gt;
     * {foreach $section in getWebSections('foo')}
     *     {foreach $item in getWebItems($section.key)} // these items are also listed as dependencies.
     *         // output the items...
     *     {/foreach}
     * {/foreach}
     * </code>
     * We also support getWebItems($section.location + '/' + $section.key)
     *
     * @param locations locations to look for web sections
     * @param derivedLocations this Set will be populated with any sub-sections we've added recursively
     * @return set of complete module keys for client web section dependencies.
     */
    @VisibleForTesting
    Set<String> getWebSectionDependencies(Iterable<String> locations, Set<String> derivedLocations) {
        Set<String> dependencies = Sets.newHashSet();

        Set<String> sectionLocations = Sets.newHashSet(locations);
        Set<String> prevSectionLocations = Sets.newHashSet(locations);
        Set<String> newSectionLocations;
        while (prevSectionLocations.size() > 0) {
            newSectionLocations = Sets.newHashSet();
            for (String location : prevSectionLocations) {
                for (ClientWebSectionModuleDescriptor section : clientWebInterfaceManager.getSections(location)) {
                    dependencies.add(section.getCompleteKey());

                    String key = section.getKey();
                    if (sectionLocations.add(key)) {
                        newSectionLocations.add(key);
                    }

                    String locationAndKey = section.getLocation() + '/' + key;
                    if (sectionLocations.add(locationAndKey)) {
                        newSectionLocations.add(locationAndKey);
                    }
                }
            }
            prevSectionLocations = newSectionLocations;
            if (derivedLocations != null) {
                derivedLocations.addAll(newSectionLocations);
            }
        }

        return dependencies;
    }


    @Override
    public List<String> getDependencies() {
        Set<String> locations = Sets.newHashSet(location);
        List<String> dependencies = Lists.newArrayList(super.getDependencies());

        if (fragmentType == FragmentType.ITEM || fragmentType == FragmentType.FRAGMENT) {
            dependencies.addAll(getWebItemDependencies(locations));
        }

        if (fragmentType == FragmentType.PANEL) {
            dependencies.addAll(getWebPanelDependencies(locations));
        }

        if (fragmentType == FragmentType.SECTION || fragmentType == FragmentType.FRAGMENT) {
            Set<String> derivedLocations = Sets.newHashSet();
            dependencies.addAll(getWebSectionDependencies(locations, derivedLocations));

            // Grab derived item and panel locations.
            dependencies.addAll(getWebItemDependencies(derivedLocations));
            dependencies.addAll(getWebPanelDependencies(derivedLocations));

            requirePageData(derivedLocations);
        }

        requirePageData(locations);

        return dependencies;
    }

}
