package com.taxonic.carml.util;

import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * Creates IRI-safe values, by percent-encoding any characters in
 * a {@code String} that are NOT one of the following:
 * - alphabetic, as per {@code Character::isAlphabetic}
 * - a digit, as per {@code Character::isDigit}
 * - a dash (-), dot (.), underscore (_) or tilde (~)
 * - part of any of the ranges of character codes specified when
 *   creating the {@code IriSafeMaker} through its constructor {@link #IriSafeMaker(List, Form, boolean)}.
 *   When creating an {@code IriSafeMaker} through {@link #create}, these ranges
 *   correspond to the {@code ucschar} production rule from RFC-3987, as
 *   specified by <a href="https://www.w3.org/TR/r2rml/#from-template">https://www.w3.org/TR/r2rml/#from-template</a>.
 */
public class IriSafeMaker implements Function<String, String> {

	public static String makeSafe(String input, Form normalizationForm, boolean upperCaseHex) {
		return create(normalizationForm, upperCaseHex).apply(input);
	}

	static class Range {

		final int from;
		final int to;

		Range(int from, int to) {
			this.from = from;
			this.to = to;
		}

		boolean includes(int value) {
			return value >= from && value <= to;
		}
	}

	public static IriSafeMaker create(Form normalizationForm, boolean upperCaseHex) {

		/* percent-encode any char not in the 'iunreserved' production rule:
		   iunreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~" / ucschar
		   ucschar        = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
		                  / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
		                  / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
		                  / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
		                  / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
		                  / %xD0000-DFFFD / %xE1000-EFFFD			 */

		String rangesStr =
			"%xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF " +
				"/ %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD " +
				"/ %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD " +
				"/ %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD " +
				"/ %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD " +
				"/ %xD0000-DFFFD / %xE1000-EFFFD";

		List<Range> ranges =
			Arrays.asList(rangesStr.split("/")).stream().map(p -> {
				String[] range = p.trim().substring(2).split("-");
				return new Range(
					Integer.parseInt(range[0], 16),
					Integer.parseInt(range[1], 16)
				);
			})
			.collect(Collectors.toList());

		return new IriSafeMaker(ranges, normalizationForm, upperCaseHex);
	}

	public static IriSafeMaker create() {
		return create(Form.NFC, true);
	}

	private List<Range> ranges;
	private Form normalizationForm;
	private boolean upperCaseHex;

	public IriSafeMaker(List<Range> ranges, Form normalizationForm, boolean upperCaseHex) {
		this.ranges = ranges;
		this.normalizationForm = normalizationForm;
		this.upperCaseHex = upperCaseHex;
	}

	@Override
	public String apply(String s) {
		StringBuilder result = new StringBuilder();

		s = Normalizer.normalize(s, normalizationForm);
		s.codePoints().flatMap(c -> {

			if (
				Character.isAlphabetic(c) ||
				Character.isDigit(c) ||
				c == '-' ||
				c == '.' ||
				c == '_' ||
				c == '~' ||
				ranges.stream().anyMatch(r -> r.includes(c))
			)
				return IntStream.of(c);

			String hex = Integer.toHexString(c);

			hex = upperCaseHex ? hex.toUpperCase() : hex;

			// percent-encode
			return ("%" + hex).codePoints();
		})
		.forEach(c -> result.append((char) c));
		return result.toString();
	}

}
