package org.jboss.windup.rules.apps.java.scan.operation.packagemapping;

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.GraphRule;
import org.jboss.windup.config.PreRulesetEvaluation;
import org.jboss.windup.rules.apps.java.model.WindupJavaConfigurationModel;
import org.jboss.windup.rules.apps.java.service.WindupJavaConfigurationService;
import org.jboss.windup.util.PathUtil;
import org.ocpsoft.rewrite.config.Rule;
import org.ocpsoft.rewrite.context.EvaluationContext;

/**
 * Maps from a package to a organization name.
 */
public class PackageNameMapping extends GraphRule implements PackageNameMappingWithPackagePattern, PackageNameMappingWithOrganization, PreRulesetEvaluation
{
    private static final Logger LOG = Logger.getLogger(PackageNameMapping.class.getSimpleName());

    private String id = this.getClass().getName() + "_" + UUID.randomUUID().toString();

    private String organization;
    private String packagePattern;

    /**
     * Gets the organization for the given package (or Maven group id).
     */
    public static String getOrganizationForPackage(GraphRewrite event, String pkg)
    {
        final String pkgComparison = pkg + ".";
        String organization = null;
        for (Map.Entry<String, String> entry : getMappings(event).entrySet())
        {
            final String pkgPattern = entry.getKey() + ".";
            if (StringUtils.startsWith(pkgComparison, pkgPattern))
            {
                organization = entry.getValue();
                if (LOG.isLoggable(Level.FINE))
                {
                    LOG.fine(" -- Found organization: " + organization);
                }
                break;
            }
        }
        return organization;
    }

    /**
     * Sets the package pattern to match against.
     */
    public static PackageNameMappingWithPackagePattern fromPackage(String packagePattern)
    {
        PackageNameMapping packageNameMapping = new PackageNameMapping();
        packageNameMapping.setPackagePattern(packagePattern);
        return packageNameMapping;
    }

    @Override
    public Rule withId(String id)
    {
        this.id = id;
        return this;
    }

    /**
     * Sets the organization to map the package to.
     */
    @Override
    public PackageNameMappingWithOrganization toOrganization(String organization)
    {
        setOrganization(organization);
        return this;
    }

    /**
     * Indicates that all of the packages within an archive are "known" by the package mapper. Generally
     * this indicates that the archive does not contain customer code.
     */
    public static boolean isExclusivelyKnownArchive(GraphRewrite event, String filePath)
    {
        String extension = StringUtils.substringAfterLast(filePath, ".");

        if (!StringUtils.equalsIgnoreCase(extension, "jar"))
            return false;

        ZipFile archive;
        try
        {
            archive = new ZipFile(filePath);
        } catch (IOException e)
        {
            return false;
        }

        WindupJavaConfigurationService javaConfigurationService = new WindupJavaConfigurationService(event.getGraphContext());
        WindupJavaConfigurationModel javaConfigurationModel = WindupJavaConfigurationService.getJavaConfigurationModel(event.getGraphContext());

        // indicates that the user did specify some packages to scan explicitly (as opposed to scanning everything)
        boolean customerPackagesSpecified = javaConfigurationModel.getScanJavaPackages().iterator().hasNext();

        // this should only be true if:
        // 1) the package does not contain *any* customer packages.
        // 2) the package contains "known" vendor packages.
        boolean exclusivelyKnown = false;

        String organization = null;
        Enumeration<?> e = archive.entries();

        // go through all entries...
        ZipEntry entry;
        while (e.hasMoreElements())
        {
            entry = (ZipEntry) e.nextElement();
            String entryName = entry.getName();

            if (entry.isDirectory() || !StringUtils.endsWith(entryName, ".class"))
                continue;

            String classname = PathUtil.classFilePathToClassname(entryName);
            // if the package isn't current "known", try to match against known packages for this entry.
            if (!exclusivelyKnown)
            {
                organization = getOrganizationForPackage(event, classname);
                if (organization != null)
                {
                    exclusivelyKnown = true;
                } else
                {
                    // we couldn't find a package definitively, so ignore the archive
                    exclusivelyKnown = false;
                    break;
                }
            }

            // If the user specified package names and this is in those package names, then scan it anyway
            if (customerPackagesSpecified && javaConfigurationService.shouldScanPackage(classname))
            {
                return false;
            }
        }

        if (exclusivelyKnown)
            LOG.info("Known Package: " + archive.getName() + "; Organization: " + organization);

        // Return the evaluated exclusively known value.
        return exclusivelyKnown;
    }

    @Override
    public void preRulesetEvaluation(GraphRewrite event)
    {
        PackageNameMapping.getMappings(event).put(packagePattern, organization);
    }

    private static Map<String, String> getMappings(GraphRewrite event)
    {
        @SuppressWarnings("unchecked")
        Map<String, String> map = (Map<String, String>) event.getRewriteContext().get(PackageNameMapping.class);
        if (map == null)
        {
            map = new HashMap<>();
            event.getRewriteContext().put(PackageNameMapping.class, map);
        }
        return map;
    }

    @Override
    public boolean evaluate(GraphRewrite event, EvaluationContext context)
    {
        /*
         * This is empty because we only care about PreRulesetEvaluation
         */
        return true;
    }

    @Override
    public void perform(GraphRewrite event, EvaluationContext context)
    {
        /*
         * This is empty because we only care about PreRulesetEvaluation
         */
    }

    /**
     * Contains the organization name.
     */
    public String getOrganization()
    {
        return organization;
    }

    /**
     * Contains the organization name.
     */
    public void setOrganization(String organization)
    {
        this.organization = organization;
    }

    /**
     * Contains the package pattern.
     */
    public String getPackagePattern()
    {
        return packagePattern;
    }

    /**
     * Contains the package pattern.
     */
    public void setPackagePattern(String packagePattern)
    {
        this.packagePattern = packagePattern;
    }

    @Override
    public String getId()
    {
        return id;
    }

    @Override
    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        builder.append(getClass().getName());
        builder.append(".fromPackage(" + packagePattern + ")");
        builder.append(".toOrganization(" + organization + ")");
        return builder.toString();
    }
}
