/*
 * Copyright (C) 2015 SoftIndex LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.datakernel.common;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.Math.round;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;

public final class StringFormatUtils {

	public static String formatMemSize(MemSize memSize) {
		long bytes = memSize.toLong();

		if (bytes == 0) {
			return "0";
		}

		for (long unit = MemSize.TB; ; unit /= 1024L) {
			long divideResult = bytes / unit;
			long remainder = bytes % unit;

			if (divideResult == 0) {
				continue;
			}
			if (remainder == 0) {
				return divideResult + getUnit(unit);
			}
		}
	}

	private static String getUnit(long unit) {
		if (unit == MemSize.TB) {
			return "Tb";
		} else {
			switch ((int) unit) {
				case (int) MemSize.GB:
					return "Gb";
				case (int) MemSize.MB:
					return "Mb";
				case (int) MemSize.KB:
					return "Kb";
				case 1:
					return "";
				default:
					throw new IllegalArgumentException("Wrong unit");
			}
		}
	}

	private static final Pattern MEM_SIZE_PATTERN = Pattern.compile("(?<size>\\d+)([\\.](?<floating>\\d+))?\\s*(?<unit>(|g|m|k|t)b?)?(\\s+|$)", Pattern.CASE_INSENSITIVE);

	public static MemSize parseMemSize(String string) {
		Set<String> units = new HashSet<>();
		Matcher matcher = MEM_SIZE_PATTERN.matcher(string.trim().toLowerCase());
		long result = 0;

		int lastEnd = 0;
		while (!matcher.hitEnd()) {
			if (!matcher.find() || matcher.start() != lastEnd) {
				throw new IllegalArgumentException("Invalid MemSize: " + string);
			}
			lastEnd = matcher.end();
			String unit = matcher.group("unit");

			if (unit == null) {
				unit = "";
			}

			if (!unit.endsWith("b")) {
				unit += "b";
			}

			if (!units.add(unit)) {
				throw new IllegalArgumentException("Memory unit " + unit + " occurs more than once in: " + string);
			}

			long memsize = Long.parseLong(matcher.group("size"));
			long numerator = 0;
			long denominator = 1;
			String floatingPoint = matcher.group("floating");
			if (floatingPoint != null) {
				if (unit.equals("") || unit.equals("b")) {
					throw new IllegalArgumentException("MemSize unit bytes cannot be fractional");
				}
				numerator = Long.parseLong(floatingPoint);
				for (int i = 0; i < floatingPoint.length(); i++) {
					denominator *= 10;
				}
			}

			double fractional = (double) numerator / denominator;
			switch (unit) {
				case "tb":
					result += memsize * MemSize.TB;
					result += round(MemSize.TB * fractional);
					break;
				case "gb":
					result += memsize * MemSize.GB;
					result += round(MemSize.GB * fractional);
					break;
				case "mb":
					result += memsize * MemSize.MB;
					result += round(MemSize.MB * fractional);
					break;
				case "kb":
					result += memsize * MemSize.KB;
					result += round(MemSize.KB * fractional);
					break;
				case "b":
				case "":
					result += memsize;
					break;
			}
		}
		return MemSize.of(result);
	}

	public static String formatDuration(Duration value) {
		if (value.isZero()) {
			return "0 seconds";
		}
		String result = "";
		long days, hours, minutes, seconds, nano, milli;
		days = value.toDays();
		if (days != 0) {
			result += days + " days ";
		}
		hours = value.toHours() - days * 24;
		if (hours != 0) {
			result += hours + " hours ";
		}
		minutes = value.toMinutes() - days * 1440 - hours * 60;
		if (minutes != 0) {
			result += minutes + " minutes ";
		}
		seconds = value.getSeconds() - days * 86400 - hours * 3600 - minutes * 60;
		if (seconds != 0) {
			result += seconds + " seconds ";
		}
		nano = value.getNano();
		milli = (nano - nano % 1000000) / 1000000;
		if (milli != 0) {
			result += milli + " millis ";
		}
		nano = nano % 1000000;
		if (nano != 0) {
			result += nano + " nanos ";
		}
		return result.trim();
	}

	private final static Pattern DURATION_PATTERN = Pattern.compile("(?<time>-?\\d+)([\\.](?<floating>\\d+))?\\s+(?<unit>days?|hours?|minutes?|seconds?|millis?|nanos?)(\\s+|$)");
	private final static int NANOS_IN_MILLI = 1000000;
	private final static int MILLIS_IN_SECOND = 1000;
	private final static int SECONDS_PER_MINUTE = 60;
	private final static int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60;
	private final static int SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;

	public static Duration parseDuration(String string) {
		string = string.trim();
		if (string.startsWith("-P") || string.startsWith("P")) {
			return Duration.parse(string);
		}
		Set<String> units = new HashSet<>();
		int days = 0, hours = 0, minutes = 0;
		long seconds = 0, millis = 0, nanos = 0;
		double doubleSeconds = 0.0;
		long result;

		Matcher matcher = DURATION_PATTERN.matcher(string.trim().toLowerCase());
		int lastEnd = 0;
		while (!matcher.hitEnd()) {
			if (!matcher.find() || matcher.start() != lastEnd) {
				throw new IllegalArgumentException("Invalid duration: " + string);
			}
			lastEnd = matcher.end();
			String unit = matcher.group("unit");
			if (!unit.endsWith("s")) {
				unit += "s";
			}
			if (!units.add(unit)) {
				throw new IllegalArgumentException("Time unit " + unit + " occurs more than once in: " + string);
			}

			result = Long.parseLong(matcher.group("time"));
			int numerator = 0;
			int denominator = 1;
			String floatingPoint = matcher.group("floating");
			if (floatingPoint != null) {
				if (unit.equals("nanos")) {
					throw new IllegalArgumentException("Time unit nanos cannot be fractional");
				}
				numerator = Integer.parseInt(floatingPoint);
				for (int i = 0; i < floatingPoint.length(); i++) {
					denominator *= 10;
				}
			}

			double fractional = (double) numerator / denominator;
			switch (unit) {
				case "days":
					days = (int) result;
					doubleSeconds += SECONDS_PER_DAY * fractional;
					break;
				case "hours":
					hours += (int) result;
					doubleSeconds += SECONDS_PER_HOUR * fractional;
					break;
				case "minutes":
					minutes += (int) result;
					doubleSeconds += SECONDS_PER_MINUTE * fractional;
					break;
				case "seconds":
					seconds += (int) result;
					doubleSeconds += fractional;
					break;
				case "millis":
					millis += result;
					doubleSeconds += fractional / MILLIS_IN_SECOND;
					break;
				case "nanos":
					nanos += result;
					break;
			}
		}

		return Duration.ofDays(days)
				.plusHours(hours)
				.plusMinutes(minutes)
				.plusSeconds(seconds)
				.plusMillis(millis)
				.plusNanos(nanos)
				.plusSeconds(round(doubleSeconds))
				.plusNanos(round((doubleSeconds - round(doubleSeconds)) * (NANOS_IN_MILLI * MILLIS_IN_SECOND)));
	}

	public static String formatPeriod(Period value) {
		if (value.isZero()) {
			return "0 days";
		}
		String result = "";
		int years = value.getYears(), months = value.getMonths(),
				days = value.getDays();
		if (years != 0) {
			result += years + " years ";
		}
		if (months != 0) {
			result += months + " months ";
		}
		if (days != 0) {
			result += days + " days ";
		}
		return result.trim();
	}

	private final static Pattern PERIOD_PATTERN = Pattern.compile("(?<str>((?<time>-?\\d+)([\\.](?<floating>\\d+))?\\s+(?<unit>years?|months?|days?))(\\s+|$))");

	/**
	 * Parses value to Period.
	 * 1 year 2 months 3 days == Period.of(1, 2, 3)
	 * Every value can be negative, but you can't make all Period negative by negating year.
	 * In ISO format you can write -P1Y2M, which means -1 years -2 months in this format
	 * There can't be any spaces between '-' and DIGIT:
	 * -1  - Right
	 * - 2 - Wrong
	 */
	public static Period parsePeriod(String string) {
		string = string.trim();
		if (string.startsWith("-P") || string.startsWith("P")) {
			return Period.parse(string);
		}
		int years = 0, months = 0, days = 0;
		Set<String> units = new HashSet<>();

		Matcher matcher = PERIOD_PATTERN.matcher(string.trim().toLowerCase());
		int lastEnd = 0;
		while (!matcher.hitEnd()) {
			if (!matcher.find() || matcher.start() != lastEnd) {
				throw new IllegalArgumentException("Invalid period: " + string);
			}
			lastEnd = matcher.end();
			String unit = matcher.group("unit");
			if (!unit.endsWith("s")) {
				unit += "s";
			}
			if (!units.add(unit)) {
				throw new IllegalArgumentException("Time unit: " + unit + " occurs more than once.");
			}
			int result = Integer.parseInt(matcher.group("time"));
			switch (unit) {
				case "years":
					years = result;
					break;
				case "months":
					months = result;
					break;
				case "days":
					days = result;
					break;
			}
		}
		return Period.of(years, months, days);
	}

	private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
			.parseCaseInsensitive()
			.append(ISO_LOCAL_DATE)
			.appendLiteral(' ')
			.append(ISO_LOCAL_TIME)
			.toFormatter();

	public static String formatLocalDateTime(LocalDateTime value) {
		value.format(DATE_TIME_FORMATTER);
		return value.toString();
	}

	public static LocalDateTime parseLocalDateTime(String string) {
		try {
			return LocalDateTime.parse(string, DATE_TIME_FORMATTER);
		} catch (DateTimeParseException e) {
			return LocalDateTime.parse(string);
		}
	}

	public static String formatInstant(Instant value) {
		String result = value.toString().replace('T', ' ');
		return result.substring(0, result.length() - 1);
	}

	public static Instant parseInstant(String string) {
		string = string.trim();
		return Instant.parse(string.replace(' ', 'T') + "Z");
	}

}