package de.siphalor.was.content.product.dynamic;

import de.siphalor.was.content.product.Product;
import de.siphalor.was.content.product.ProductType;
import de.siphalor.was.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.function.IntPredicate;
import java.util.stream.Collectors;

/**
 * An implementation for {@link ProductType} that dynamically reads its data and products from a properties file.
 */
public class DynamicProductType implements ProductType<DynamicProduct> {
	/**
	 * The type's id
	 */
	private final String id;
	/**
	 * All of the products mapped by their property specifiers.
	 * @see Product#getPropertySpecifier()
	 */
	private final Map<String, DynamicProduct> variations;
	/**
	 * The property ids
	 */
	private final String[] properties;

	/**
	 * Constructs a new dynamic product type.
	 * @param id The id
	 * @param variations All of the products that are derived of this type
	 * @param properties The property ids
	 */
	public DynamicProductType(String id, List<DynamicProduct> variations, String[] properties) {
		this.id = id;
		this.variations = new HashMap<>();
		variations.forEach(p -> {
			p.setType(this);
			this.variations.put(p.getPropertySpecifier(), p);
		});
		this.properties = properties;
	}

	/**
	 * Deserializes a product type from an input stream with a properties file
	 * @param id The type's id
	 * @param inputStream The input stream containing a properties file
	 * @return Returns the constructed product type
	 */
	@NotNull
	public static DynamicProductType from(@NotNull String id, @NotNull InputStream inputStream) {
		Properties propertiesFile = new Properties();

		try {
			propertiesFile.load(inputStream);

			String[] propNames = ((String) propertiesFile.get("properties")).split(",");

			if (propNames.length > 0) {
				// propNameList = propNames without blank elements & all elements trimmed
				List<String> propNameList = new ArrayList<>(propNames.length);
				List<List<ProductPrototype>> properties = new ArrayList<>(propNames.length);
				for (String propName : propNames) {
					propName = propName.trim();
					if (propName.isEmpty()) {
						continue;
					}
					propNameList.add(propName);
					if (propertiesFile.containsKey(propName + ".variants")) {
						String[] variants = ((String) propertiesFile.get(propName + ".variants")).split(",");
						List<ProductPrototype> propertyVariants = new ArrayList<>(variants.length);
						for (String variant : variants) {
							variant = variant.trim();
							propertyVariants.add(makePrototype(propertiesFile, propName, variant));
						}
						properties.add(propertyVariants);
					} else {
						// if there are no variants assume true/false flag
						properties.add(List.of(
								makePrototype(propertiesFile, propName, "true"),
								makePrototype(propertiesFile, propName, "false")
						));
					}
				}

				List<ProductPrototype> prototypes = properties.stream().reduce((pts1, pts2) -> {
					return pts1.stream().flatMap(pt -> {
						return pts2.stream().map(pt::combine);
					}).collect(Collectors.toList());
				}).get();

				List<DynamicProduct> products = prototypes.stream().map(pt -> {
					DynamicProduct product = new DynamicProduct(pt.value.split(";"));
					pt.apply(product);
					return product;
				}).collect(Collectors.toList());

				return new DynamicProductType(id, products, propNameList.toArray(String[]::new));
			}
		} catch (IOException | NullPointerException e) {
			e.printStackTrace();
		}

		return new DynamicProductType(id, List.of(new DynamicProduct(Util.emptyArray())), Util.emptyArray());
	}

	/**
	 * Creates a new product prototype for the given property value.
	 * @param propertiesFile The properties file to load from
	 * @param property The property id to target
	 * @param variant The property value to target.
	 * @return Returns a new prototype
	 */
	private static ProductPrototype makePrototype(Properties propertiesFile, String property, String variant) {
		ProductPrototype prototype = new ProductPrototype(variant);
		String base = property + "." + variant + ".";
		if (propertiesFile.containsKey(base + "depth")) {
			prototype.depth = Integer.parseInt((String) propertiesFile.get(base + "depth"));
		}
		if (propertiesFile.containsKey(base + "y")) {
			String val = (String) propertiesFile.get(base + "y");
			switch (val.charAt(0)) {
				case '>': {
					int i = Integer.parseInt(val.substring(1));
					prototype.yPredicate = value -> value > i;
					break;
				}
				case '<': {
					int i = Integer.parseInt(val.substring(1));
					prototype.yPredicate = value -> value < i;
					break;
				}
				default:
					int i = Integer.parseInt(val);
					prototype.yPredicate = value -> value == i;
					break;
			}
		}
		return prototype;
	}

	/**
	 * Gets the product with the given values.
	 * @param values The properties' values
	 * @return Returns the associated product or <code>null</code> if it's nonexistent
	 */
	@Override
	@Nullable
	public DynamicProduct getProduct(@NotNull String[] values) {
		return variations.get(String.join("_", values));
	}

	/**
	 * Gets the type's id
	 * @return The id
	 */
	@Override
	public @NotNull String getId() {
		return id;
	}

	/**
	 * Gets the property ids
	 * @return The property ids
	 */
	@Override
	public @NotNull String[] getProperties() {
		return properties;
	}

	/**
	 * Retrieves a random product from the variations.
	 * @param random An instance of {@link Random} to use for the calculation
	 * @return A random product
	 */
	@Override
	public @NotNull DynamicProduct randomProduct(Random random) {
		List<DynamicProduct> products = new ArrayList<>(variations.values());
		return products.get(random.nextInt(products.size()));
	}

	/**
	 * Gets all of the products mapped by their property specifier.
	 * @return A map of those
	 * @see DynamicProduct#getPropertySpecifier()
	 */
	public Map<String, DynamicProduct> getVariations() {
		return variations;
	}

	/**
	 * A prototype used for building the product variations
	 */
	private static class ProductPrototype {
		/**
		 * The property specifier with all properties that already have been used
		 */
		String value;

		/**
		 * The depth of the prototype. <code>null</code> if it should be inherited
		 */
		Integer depth = null;
		/**
		 * The y tester of the prototype. <code>null</code> if it should be inherited
		 */
		IntPredicate yPredicate = null;

		/**
		 * Constructs a new prototype with the given property specifier
		 * @param value The property specifier
		 */
		public ProductPrototype(String value) {
			this.value = value;
		}

		/**
		 * Applies the prototypes' data to the given product
		 * @param product The product which shall get adapted
		 */
		public void apply(DynamicProduct product) {
			if (depth != null)
				product.setDepth(depth);
			if (yPredicate != null)
				product.setYPredicate(yPredicate);
		}

		/**
		 * Merges two prototypes into each other. The result has the properties of both. The other prototype is recessive
		 * @param prototype The other prototype
		 * @return A newly constructed prototype with the calculated properties.
		 */
		public ProductPrototype combine(ProductPrototype prototype) {
			ProductPrototype result = new ProductPrototype(value + ";" + prototype.value);
			if (prototype.depth != null)
				result.depth = prototype.depth;
			else
				result.depth = depth;
			if (prototype.yPredicate != null)
				result.yPredicate = prototype.yPredicate;
			else
				result.yPredicate = yPredicate;
			return result;
		}
	}

	/**
	 * Converts to string. That's all.
	 * @return The stringified product type.
	 */
	@Override
	public String toString() {
		return "DynamicProductType{" +
				"properties=" + String.join(" & ", properties) +
				", variations=\n" + variations.values().stream().map(Object::toString).collect(Collectors.joining(System.lineSeparator())) +
				'}';
	}
}
