package org.jboss.windup.config.metadata;

import org.apache.commons.lang3.StringUtils;
import org.jboss.forge.furnace.versions.MultipleVersionRange;
import org.jboss.forge.furnace.versions.VersionRange;
import org.jboss.forge.furnace.versions.Versions;
import org.jboss.windup.graph.model.TechnologyReferenceModel;

/**
 * Represents a technology with a name (id) and {@link VersionRange}.
 * 
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
public class TechnologyReference
{
    private String id;
    private VersionRange versionRange;

    public TechnologyReference(TechnologyReferenceModel model)
    {
        this(model.getTechnologyID(), model.getVersionRange());
    }

    /**
     * Construct a new {@link TechnologyReference} using the given {@link String} ID and {@link String} version range.
     */
    public TechnologyReference(String id, String versionRange)
    {
        this.id = id;
        if (versionRange != null)
            this.versionRange = Versions.parseVersionRange(versionRange);
    }

    /**
     * Construct a new {@link TechnologyReference} using the given {@link String} ID and {@link VersionRange}.
     */
    public TechnologyReference(String id, VersionRange versionRange)
    {
        this.id = id;
        this.versionRange = versionRange;
    }

    /**
     * Construct a new {@link TechnologyReference} using the given {@link String} ID.
     */
    public TechnologyReference(String id)
    {
        this(id, (VersionRange) null);
    }

    /**
     * Parses a {@link TechnologyReference} from a string that is formatted as either
     * "id" or "id:versionRange".
     */
    public static TechnologyReference parseFromIDAndVersion(String idAndVersion)
    {
        if (idAndVersion.contains(":"))
        {
            String tech = StringUtils.substringBefore(idAndVersion, ":");
            String versionRangeString = StringUtils.substringAfter(idAndVersion, ":");
            if (!versionRangeString.matches("^[(\\[].*[)\\]]"))
                versionRangeString = "[" + versionRangeString + "]";

            VersionRange versionRange = Versions.parseVersionRange(versionRangeString);
            return new TechnologyReference(tech, versionRange);
        }
        return new TechnologyReference(idAndVersion);
    }

    /**
     * Get the name/ID of this {@link TechnologyReference}.
     */
    public String getId()
    {
        return id;
    }

    /**
     * Get the {@link VersionRange} of this {@link TechnologyReference}.
     */
    public VersionRange getVersionRange()
    {
        return versionRange;
    }

    /**
     * Returns true if the other {@link TechnologyReference} has the same technology id and the two version ranges overlap.
     */
    public boolean matches(TechnologyReference other)
    {
        return StringUtils.equals(getId(), other.getId()) && versionRangesOverlap(other.getVersionRange());
    }

    /**
     * Takes the given {@link VersionRange} objects and returns true if there is any overlap between the two
     * ranges.
     *
     * If either is null, then it is treated as overlapping.
     */
    public boolean versionRangesOverlap(VersionRange otherRange)
    {
        if (this.getVersionRange() == null || otherRange == null)
            return true;

        /*
         * FIXME HACK - The code in MultipleVersionRange works pretty well for this calculation, so we are reusing that.
         *
         * Some of the other range intersection algorithms have design flaws that make them return results incorrectly.
         *
         * This hack needs an extensive unit test to insure that it retains the behavior that we expect.
         */
        MultipleVersionRange range1Multiple;
        if (getVersionRange() instanceof MultipleVersionRange)
            range1Multiple = (MultipleVersionRange)getVersionRange();
        else
            range1Multiple = new MultipleVersionRange(getVersionRange());

        try
        {
            VersionRange intersection = range1Multiple.getIntersection(otherRange);
            return intersection != null && !intersection.isEmpty();
        } catch (Throwable t)
        {
            // This generally only occurs if there was no intersection
            return false;
        }
    }

    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        result = prime * result + ((versionRange == null) ? 0 : versionRange.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        TechnologyReference other = (TechnologyReference) obj;
        if (id == null)
        {
            if (other.id != null)
                return false;
        }
        else if (!id.equals(other.id))
            return false;
        if (versionRange == null)
        {
            if (other.versionRange != null)
                return false;
        }
        else if (!versionRange.equals(other.versionRange))
            return false;
        return true;
    }

    /**
     * This provides a parsable version string based upon the current {@link VersionRange}. If the version
     * range is null, this will return null.
     */
    public String getVersionRangeAsString()
    {
        if (this.versionRange == null)
            return null;

        String rangeString = this.versionRange.toString();

        /**
         * FIXME
         *
         * Workaround a bug in Forge that causes VersionRange to sometimes return a string like [7,7] when
         * it should only return "[7]". This seems to be an issue with the way Versions.parseVersionRange()
         * works.
         *
         * See also: FORGE-2667
         */
        if (StringUtils.startsWith(rangeString, "[") &&
            StringUtils.endsWith(rangeString, "]") &&
            StringUtils.countMatches(rangeString, ",") == 1)
        {
            String firstVersion = StringUtils.substringBefore(rangeString, ",").substring(1);
            String secondString = StringUtils.stripEnd(StringUtils.substringAfter(rangeString, ","), "]");
            if (StringUtils.equals(firstVersion, secondString))
                return "[" + firstVersion + "]";
            else
                return rangeString;
        }
        return rangeString;
    }

    @Override
    public String toString()
    {
        String rangeString = getVersionRangeAsString();
        String range = rangeString== null ? "" : ":" + rangeString;
        return id + range;
    }
}
