package twilightforest.data.helpers;

import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.Nullable;

import java.text.ChoiceFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Borrowed from <a href="https://github.com/mekanism/Mekanism/blob/1.20.x/src/datagen/main/java/mekanism/client/lang/FormatSplitter.java">Mekanism</a>
 */
public class LangFormatSplitter {
	//Pattern from Formatter: %[argument_index$][flags][width][.precision][t]conversion
	// Note: This probably supports more formats than MC's formatter does, except things like local translation for I18n seems to
	// go through String.format which would end up using this. So I believe these are technically supported
	// The string pattern from the Formatter is: "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"
	// we modify it to remove the trailing % as MC declares things like %% as invalid
	private static final Pattern fsPattern = Pattern.compile("%(\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z])");

	public static List<Component> split(String text) {
		Matcher matcher = fsPattern.matcher(text);
		List<Component> components = new ArrayList<>();
		int start = 0;
		while (matcher.find()) {
			int curStart = matcher.start();
			if (curStart > start) {
				//There is a gap, so we need to grab the piece in between
				components.add(new TextComponent(text.substring(start, curStart)));
			}
			String piece = matcher.group();
			components.add(new FormatComponent(piece));
			start = matcher.end();
		}
		if (start < text.length()) {
			if (start == 0) {
				//If there were no formatting codes, see if there are any potential MessageFormat codes
				// which will get picked up by forge if it could not find any normal formatting codes
				// Note: This assumes every thing between two {} is valid, instead of bothering to check against the set that java + forge declare
				components.addAll(splitMessageFormatInternal(text));
			} else {
				components.add(new TextComponent(text.substring(start)));
			}
		}
		return ImmutableList.copyOf(components);
	}

	public static List<Component> splitMessageFormat(String text) {
		return ImmutableList.copyOf(splitMessageFormatInternal(text));
	}

	/**
	 * @apiNote Can return two TextComponents neighboring each other it doesn't bother combining them
	 */
	private static List<Component> splitMessageFormatInternal(String text) {
		List<Component> components = new ArrayList<>();
		StringBuilder formattingCode = new StringBuilder();
		StringBuilder rawText = new StringBuilder();
		char[] exploded = text.toCharArray();
		int leftBrackets = 0;
		int firstBracket = -1;
		int secondBracket = -1;
		for (int i = 0; i < exploded.length; i++) {
			char c = exploded[i];
			if (c == '{') {
				if (leftBrackets == 0) {
					firstBracket = i;
					String raw = rawText.toString();
					if (!raw.isEmpty()) {
						//If we have text and run into a left bracket, then add our text
						components.add(new TextComponent(raw));
						rawText = new StringBuilder();
					}
				} else if (leftBrackets == 1) {
					secondBracket = i;
				}
				leftBrackets++;
				formattingCode.append(c);
			} else if (leftBrackets > 0) {
				formattingCode.append(c);
				if (c == '}') {
					leftBrackets--;
					if (leftBrackets == 0) {
						//If we finish closing our brackets add our formatting code
						String piece = formattingCode.toString();
						MessageFormatComponent component = MessageFormatComponent.fromContents(piece);
						if (component == null) {
							if (secondBracket != -1) {
								//Add the text from the first bracket up to the second bracket as raw text
								// and reset our state to as if we were starting at that point
								components.add(new TextComponent(text.substring(firstBracket, secondBracket)));
								//We use subtract one here so when it is incremented at the end of the loop, it starts in the right place
								i = secondBracket - 1;
							} else {
								//If we only have a depth of one, and it is not a valid message format, then we just add it as a raw string
								components.add(new TextComponent(piece));
							}
						} else {
							//The string we found represents a valid MessageFormat so add the component for it to our components
							components.add(component);
						}
						//Reset the various variables we use to keep track of where we are
						formattingCode = new StringBuilder();
						firstBracket = -1;
						secondBracket = -1;
					}
				}
			} else {
				rawText.append(c);
			}
		}
		if (leftBrackets == 0) {
			String raw = rawText.toString();
			if (!raw.isEmpty()) {
				//Add any remaining text
				components.add(new TextComponent(raw));
			}
		} else {
			if (secondBracket != -1) {
				//At least part of our remaining has a valid MessageFormat representation
				//Add the part before the brackets as raw text
				components.add(new TextComponent(text.substring(firstBracket, secondBracket)));
				//and then try to add the remaining stuff directly
				components.addAll(splitMessageFormatInternal(text.substring(secondBracket)));
			} else {
				//If we don't have a closing bracket, and we didn't have more brackets at some point, add what we have as raw text
				String remainingString = formattingCode.toString();
				if (!remainingString.isEmpty()) {
					components.add(new TextComponent(remainingString));
				}
			}
		}
		return components;
	}

	public interface Component {

		String contents();
	}

	private record TextComponent(String contents) implements Component {
	}

	public static class FormatComponent implements Component {

		private final String formattingCode;

		private FormatComponent(String formattingCode) {
			this.formattingCode = formattingCode;
		}

		@Override
		public String contents() {
			return formattingCode;
		}
	}

	/**
	 * Represents information about a MessageFormat formatting code. Valid MessageFormat styles:
	 * <p><ul>
	 * <li> { ArgumentIndex }
	 * <li> { ArgumentIndex, FormatType }
	 * <li> { ArgumentIndex, FormatType, FormatStyle }
	 * </ul>
	 */
	public static class MessageFormatComponent extends FormatComponent {

		private final int argumentIndex;
		@Nullable
		private final String formatType;
		@Nullable
		private final String formatStyle;
		private final boolean isChoice;

		private MessageFormatComponent(String contents, int argumentIndex, @Nullable String formatType, @Nullable String formatStyle, boolean isChoice) {
			super(contents);
			this.argumentIndex = argumentIndex;
			this.formatType = formatType;
			this.formatStyle = formatStyle;
			this.isChoice = isChoice;
		}

		public int getArgumentIndex() {
			return argumentIndex;
		}

		/**
		 * @apiNote This will not be null if {@link #getFormatStyle()} is not null
		 */
		@Nullable
		public String getFormatType() {
			return formatType;
		}

		@Nullable
		public String getFormatStyle() {
			return formatStyle;
		}

		public boolean isChoice() {
			return isChoice;
		}

		/**
		 * @param contents Contents to create a {@link MessageFormatComponent} from.
		 * @return A {@link MessageFormatComponent} representing the given contents, or {@code null} if the contents do not represent a valid
		 * {@link MessageFormatComponent}
		 */
		@Nullable
		private static MessageFormatComponent fromContents(String contents) {
			int length = contents.length();
			if (length < 3 || contents.charAt(0) != '{' || contents.charAt(length - 1) != '}') {
				//If we don't have at least one digit between the two brackets, or we don't start and end with a bracket
				// then this is not a valid
				return null;
			}
			int firstComma = contents.indexOf(',');
			int argumentIndex;
			try {
				argumentIndex = Integer.parseInt(contents.substring(1, firstComma == -1 ? length - 1 : firstComma));
			} catch (NumberFormatException e) {
				//If the argument is not a valid number, just exit as it is probably not meant to be a
				return null;
			}
			if (argumentIndex < 0 || argumentIndex > 9) {
				//MessageFormat only supports up to 10 total arguments
				return null;
			}
			if (firstComma == -1) {
				//If we don't have a comma, so it is only an argument index we can just exit now
				return new MessageFormatComponent(contents, argumentIndex, null, null, false);
			}
			//Look for the next comma
			int secondComma = contents.indexOf(',', firstComma + 1);
			String formatType = contents.substring(firstComma + 1, secondComma == -1 ? length - 1 : secondComma);
			//Set the format style based on the format type or to null if we do not have one
			String formatStyle = secondComma == -1 ? null : contents.substring(secondComma + 1, length - 1);
			String trimmedFormatType = formatType.trim();
			boolean isChoice = false;
			switch (trimmedFormatType) {
				//Built in Java Format Types
				case "number" -> {
					if (formatStyle != null && !formatStyle.equals("integer") && !formatStyle.equals("currency") && !formatStyle.equals("percent")) {
						//If it is not a valid format style for number check if it is a valid SubformatPattern
						// number uses DecimalFormat as a SubformatPattern
						try {
							new DecimalFormat(formatStyle);
						} catch (IllegalArgumentException e) {
							//If it is not a valid DecimalFormat then it is not a valid format overall, so return null
							return null;
						}
					}
				}
				case "date", "time" -> {
					if (formatStyle != null && !formatStyle.equals("short") && !formatStyle.equals("medium") && !formatStyle.equals("long") && !formatStyle.equals("full")) {
						//If it is not a valid format style for date or time check if it is a valid SubformatPattern
						// time and date both use SimpleDateFormat as a SubformatPattern
						try {
							new SimpleDateFormat(formatStyle, Locale.ENGLISH);
						} catch (IllegalArgumentException e) {
							//If it is not a valid SimpleDateFormat then it is not a valid format overall, so return null
							return null;
						}
					}
				}
				case "choice" -> {
					if (formatStyle == null) {
						return null;
					}
					//Choice is only valid when it has a SubformatPattern, so we return null if we don't have a formatStyle
					try {
						new ChoiceFormat(formatStyle);
					} catch (IllegalArgumentException e) {
						//If it is not a valid ChoiceFormat to begin with then it is not a valid format overall, so return null
						return null;
					}
					isChoice = true;
				}
				//Forge added Format types
				case "modinfo" -> {
					if (formatStyle == null || (!formatStyle.equals("id") && !formatStyle.equals("name"))) {
						//modinfo only supports id, and name, and is not valid if the type is missing
						return null;
					}
				}
				case "featurebound", "lower", "upper", "vr" -> {
					if (formatStyle != null) {
						//featurebound, lower, upper, and vr do not support any format style
						return null;
					}
				}
				case "exc" -> {
					if (formatStyle == null || (!formatStyle.equals("class") && !formatStyle.equals("msg"))) {
						//exc only supports class, and msg, and is not valid if the type is missing
						return null;
					}
				}
				case "i18n", "ornull" -> {
					if (formatStyle == null) {
						//i18n, and ornull both require a format style
						return null;
					}
				}
				default -> {
					//Not a valid format type
					return null;
				}
			}
			return new MessageFormatComponent(contents, argumentIndex, formatType, formatStyle, isChoice);
		}
	}
}
