package com.r35157.nenjim.service.journal.valuetypes;

import com.r35157.libs.valuetypes.basic.SemanticVersion;

import java.util.Objects;
import java.util.Optional;

/** A normalized, lower-inclusive and upper-exclusive set of stable releases. */
public final class VersionExpression {
    private final Boundary lowerInclusive;
    private final Boundary upperExclusive;

    public VersionExpression(Boundary lowerInclusive, Boundary upperExclusive) {
        this.lowerInclusive = Objects.requireNonNull(lowerInclusive, "lowerInclusive");
        this.upperExclusive = Objects.requireNonNull(upperExclusive, "upperExclusive");
        if (lowerInclusive.compareTo(upperExclusive) >= 0) {
            throw new IllegalArgumentException("Version expression must not be empty or reversed");
        }
    }

    public static VersionExpression exact(SemanticVersion version) {
        Boundary lower = Boundary.from(Objects.requireNonNull(version, "version"));
        return new VersionExpression(lower, lower.nextPatch());
    }

    public static VersionExpression minorSeries(int major, int minor) {
        if (major < 0 || minor < 0) {
            throw new IllegalArgumentException("Version elements cannot be negative");
        }
        Boundary lower = new Boundary(major, minor, 0);
        return new VersionExpression(lower, lower.nextMinor());
    }

    public Boundary lowerInclusive() {
        return lowerInclusive;
    }

    public Boundary upperExclusive() {
        return upperExclusive;
    }

    public boolean contains(SemanticVersion version) {
        Boundary candidate = Boundary.from(Objects.requireNonNull(version, "version"));
        return lowerInclusive.compareTo(candidate) <= 0 && candidate.compareTo(upperExclusive) < 0;
    }

    public boolean isExact() {
        return upperExclusive.equals(lowerInclusive.nextPatch())
                && lowerInclusive.major <= Integer.MAX_VALUE
                && lowerInclusive.minor <= Integer.MAX_VALUE
                && lowerInclusive.patch <= Integer.MAX_VALUE;
    }

    public Optional<SemanticVersion> exactVersion() {
        if (!isExact()) {
            return Optional.empty();
        }
        return Optional.of(new SemanticVersion(
                (int) lowerInclusive.major,
                (int) lowerInclusive.minor,
                (int) lowerInclusive.patch));
    }

    VersionExpression merge(VersionExpression other) {
        if (upperExclusive.compareTo(other.lowerInclusive) < 0
                || other.upperExclusive.compareTo(lowerInclusive) < 0) {
            throw new IllegalArgumentException("Cannot merge disjoint version expressions");
        }
        Boundary lower = lowerInclusive.compareTo(other.lowerInclusive) <= 0
                ? lowerInclusive : other.lowerInclusive;
        Boundary upper = upperExclusive.compareTo(other.upperExclusive) >= 0
                ? upperExclusive : other.upperExclusive;
        return new VersionExpression(lower, upper);
    }

    @Override
    public boolean equals(Object other) {
        return this == other || other instanceof VersionExpression expression
                && lowerInclusive.equals(expression.lowerInclusive)
                && upperExclusive.equals(expression.upperExclusive);
    }

    @Override
    public int hashCode() {
        return Objects.hash(lowerInclusive, upperExclusive);
    }

    @Override
    public String toString() {
        return "[" + lowerInclusive + "-->" + upperExclusive + ")";
    }

    /** A fully expanded stable-version boundary used by normalized expressions. */
    public record Boundary(long major, long minor, long patch) implements Comparable<Boundary> {
        public Boundary {
            if (major < 0 || minor < 0 || patch < 0) {
                throw new IllegalArgumentException("Version boundary elements cannot be negative");
            }
        }

        public static Boundary from(SemanticVersion version) {
            Objects.requireNonNull(version, "version");
            return new Boundary(version.major(), version.minor(), version.patch());
        }

        public Boundary nextPatch() {
            return new Boundary(major, minor, Math.addExact(patch, 1));
        }

        public Boundary nextMinor() {
            return new Boundary(major, Math.addExact(minor, 1), 0);
        }

        public Boundary nextMajor() {
            return new Boundary(Math.addExact(major, 1), 0, 0);
        }

        @Override
        public int compareTo(Boundary other) {
            int majorComparison = Long.compare(major, other.major);
            if (majorComparison != 0) return majorComparison;
            int minorComparison = Long.compare(minor, other.minor);
            return minorComparison != 0 ? minorComparison : Long.compare(patch, other.patch);
        }

        @Override
        public String toString() {
            return major + "." + minor + "." + patch;
        }
    }
}
