/*
 * To the extent possible under law, the author has dedicated all copyright
 * and related and neighboring rights to this software to the public domain
 * worldwide. This software is distributed without any warranty.
 *
 * See <http://creativecommons.org/publicdomain/zero/1.0/>
 */

package com.unascribed;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Implements FlexVer, a SemVer-compatible intuitive comparator for free-form versioning strings as
 * seen in the wild. It's designed to sort versions like people do, rather than attempting to force
 * conformance to a rigid and limited standard. As such, it imposes no restrictions. Comparing two
 * versions with differing formats will likely produce nonsensical results (garbage in, garbage out),
 * but best effort is made to correct for basic structural changes, and versions of differing length
 * will be parsed in a logical fashion.
 */
public class FlexVerComparator {
	
	/*
	 * Break apart a string into "logical" version components, by splitting it where a string
	 * of characters changes from numeric to non-numeric.
	 */
	private static List<Comparable<?>> decompose(String str, boolean debug) {
		if (str.isEmpty()) return Collections.emptyList();
		boolean lastWasNumber = Character.isDigit(str.codePointAt(0));
		StringBuilder accum = new StringBuilder();
		List<Comparable<?>> out = new ArrayList<>();
		// remove appendices
		int plus = str.indexOf('+');
		if (plus != -1) str = str.substring(0, plus);
		for (int i = 0; i < str.length(); i++) {
			if (Character.isLowSurrogate(str.charAt(i))) continue;
			int cp = str.codePointAt(i);
			boolean number = Character.isDigit(cp);
			if (number != lastWasNumber) {
				complete(lastWasNumber, accum, out, debug);
				lastWasNumber = number;
			}
			accum.appendCodePoint(cp);
		}
		complete(lastWasNumber, accum, out, debug);
		return out;
	}

	private static void complete(boolean number, StringBuilder accum, List<Comparable<?>> out, boolean debug) {
		if (debug) {
			out.add("\u001B["+(number?"96":isSemverPrerelease(accum.toString())?"91":"95")+"m"+accum);
		} else {
			String s = accum.toString();
			if (number) {
				// just in case someone uses a pointlessly long version string...
				out.add(Long.parseLong(s));
			} else {
				out.add(s);
			}
		}
		accum.setLength(0);
	}
	
	// it's difficult to generically deal with comparables - these operations are safe
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static int compare(String a, String b) {
		// Arrays.compare gives wrong precedence to array length, causing 1.0.1 to sort as older than 1.0.0_01
		// so, implement it ourselves from scratch
		List<Comparable<?>> ad = decompose(a, false);
		List<Comparable<?>> bd = decompose(b, false);
		for (int i = 0; i < Math.max(ad.size(), bd.size()); i++) {
			Comparable ac = i >= ad.size() ? null : ad.get(i);
			Comparable bc = i >= bd.size() ? null : bd.get(i);
			if (typeof(ac) != typeof(bc)) {
				// Comparables assume the input object is the same type as the object, so we need to
				// ensure that's true; this will happen in the case of two critically mismatched
				// versions that contain a symbol where a number is expected - in this case, lexical
				// is the best we can do
				ac = ac == null ? null : ac.toString();
				bc = bc == null ? null : bc.toString();
			}
			int c;
			if (bc == null && isSemverPrerelease(ac)) {
				// special case: compatibility with semver, which sorts "pre-releases" differently
				c = -1;
			} else if (ac == null && isSemverPrerelease(bc)) {
				c = 1;
			} else if (ac == null) {
				// special case: nulls are *always* lesser
				// bc cannot be null here, don't need to check
				c = -1;
			} else {
				c = bc == null ? 1 : ac.compareTo(bc);
			}
			if (c != 0) return c;
		}
		return 0;
	}
	
	private static boolean isSemverPrerelease(Object o) {
		if (o instanceof String) {
			String s = (String)o;
			return s.length() > 1 && s.charAt(0) == '-';
		}
		return false;
	}

	private static Class<?> typeof(Object o) {
		return o == null ? null : o.getClass();
	}

	private static void test(String a, String b) {
		int c = signum(compare(a, b));
		int c2 = signum(compare(b, a));
		if (-c2 != c) {
			throw new IllegalArgumentException("Comparison method violates its general contract! ("+a+" <=> "+b+" is not commutative)");
		}
		String res = "";
		if (c < 0) res = "<";
		if (c == 0) res = "=";
		if (c > 0) res = ">";
		System.out.println(represent(a)+"\u001B[0m "+res+" "+represent(b));
	}
	
	private static int signum(int i) {
		return i < 0 ? -1 : i > 0 ? 1 : 0;
	}

	private static String represent(String str) {
		List<Comparable<?>> d = decompose(str, true);
		boolean odd = true;
		StringBuilder out = new StringBuilder();
		for (Object o : d) {
			int color = o instanceof Number ? 96 : 95;
			out.append("\u001B[").append(color).append("m");
			out.append(o);
			odd = !odd;
		}
		if (str.contains("+")) {
			out.append("\u001B[90m");
			out.append(str.substring(str.indexOf('+')));
		}
		return out.toString();
	}

	public static void main(String[] args) {
		test("b1.7.3", "a1.2.6");
		test("b1.2.6", "a1.7.3");
		test("a1.1.2", "a1.1.2_01");
		test("1.16.5-0.00.5", "1.14.2-1.3.7");
		test("1.0.0", "1.0.0_01");
		test("1.0.1", "1.0.0_01");
		test("1.0.0_01", "1.0.1");
		test("0.17.1-beta.1", "0.17.1");
		test("0.17.1-beta.1", "0.17.1-beta.2");
		test("1.4.5_01", "1.4.5_01+fabric-1.17");
		test("1.4.5_01", "1.4.5_01+fabric-1.17+ohgod");
		test("14w16a", "18w40b");
		test("18w40a", "18w40b");
		test("1.4.5_01+fabric-1.17", "18w40b");
		test("13w02a", "c0.3.0_01");
		test("0.6.0-1.18.x", "0.9.beta-1.18.x");
	}

}