package de.siphalor.was;

import de.siphalor.was.content.ContentManager;
import de.siphalor.was.content.lang.I18n;
import de.siphalor.was.content.pack.FileContentPack;
import de.siphalor.was.content.pack.JarContentPack;
import de.siphalor.was.content.product.Product;
import de.siphalor.was.content.product.ProductManager;
import de.siphalor.was.content.quest.Quest;
import de.siphalor.was.content.quest.QuestGenerator;
import de.siphalor.was.content.quest.QuestManager;
import de.siphalor.was.content.quest.RandomQuestGenerator;
import de.siphalor.was.game.Balance;
import de.siphalor.was.game.Options;
import de.siphalor.was.game.Storage;
import de.siphalor.was.game.Transaction;
import de.siphalor.was.util.Pair;
import de.siphalor.was.util.Util;
import de.siphalor.was.visual.Visual;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

/**
 * The main game class. This is a singleton class.
 */
public class WhatAStorage {
	/**
	 * The maximum number of quests at a time
	 */
	private static final int MAX_QUESTS = 3;
	/**
	 * The size of the storage grid
	 */
	private static final int GRID_SIZE = 3;

	/**
	 * The window title
	 */
	public static final String TITLE = "What a Storage";
	/**
	 * The singleton instance
	 */
	private static final WhatAStorage INSTANCE = new WhatAStorage();

	/**
	 * Gets the singleton instance.
	 * @return The instance
	 */
	public static WhatAStorage getInstance() {
		return INSTANCE;
	}

	/**
	 * The main content manager
	 */
	private final ContentManager contentManager;
	/**
	 * The internal content pack
	 */
	private final JarContentPack mainPack;
	/**
	 * The main product manager
	 */
	private final ProductManager productManager;
	/**
	 * The main quest manager
	 */
	private final QuestManager questManager;

	/**
	 * The game's options
	 */
	private final Options options;

	/**
	 * The used visual
	 */
	private Visual visual;

	/**
	 * Whether the game has been requested to stop
	 */
	private boolean stopScheduled;

	/**
	 * The current balance
	 */
	private Balance balance;
	/**
	 * The current quest generator
	 */
	private QuestGenerator questGenerator;
	/**
	 * The current quest list
	 */
	private final List<Quest> quests = new ArrayList<>(MAX_QUESTS);
	/**
	 * The current storage
	 */
	private Storage storage;

	/**
	 * Constructs a new instance.
	 */
	private WhatAStorage() {
		contentManager = new ContentManager();
		mainPack = new JarContentPack("", "content");
		contentManager.addPack(mainPack);
		productManager = new ProductManager();
		questManager = new QuestManager();
		options = new Options();
	}

	/**
	 * Sets the visual to use. <b>Must be called before {@link WhatAStorage#setup()}</b>
	 * @param visual The visual to use
	 */
	public void setVisual(Visual visual) {
		this.visual = visual;
	}

	/**
	 * Gets the content manager.
	 * @return The content manager
	 */
	public ContentManager getContentManager() {
		return contentManager;
	}

	/**
	 * Gets the product manager.
	 * @return The product manager
	 */
	public ProductManager getProductManager() {
		return productManager;
	}

	/**
	 * Gets the quest manager.
	 * @return The quest manager
	 */
	public QuestManager getQuestManager() {
		return questManager;
	}

	/**
	 * Gets the game's options
	 * @return The game's options
	 */
	public Options getOptions() {
		return options;
	}

	/**
	 * Reloads all resources from the packs.
	 */
	public void reload() {
		contentManager.clear();

		contentManager.addPack(mainPack);

		File[] packDirs = Path.of("packs").toFile().listFiles(File::isDirectory);
		if (packDirs != null) {
			for (File dir : packDirs) {
				contentManager.addPack(new FileContentPack(dir.getName(), dir.toPath()));
			}
		}

		options.load();
		I18n.getInstance().reload(contentManager);

		productManager.clear();
		questManager.clear();
		productManager.reload(contentManager);
		questManager.reload(contentManager, productManager);
		questManager.register("random", new RandomQuestGenerator());

		Util.LOGGER.log(Level.INFO, "Reloaded game content");
	}

	/**
	 * Setups the game.
	 */
	public void setup() {
		if (visual == null) {
			throw new IllegalStateException("No visual set for WhatAStorage!");
		}
		visual.setup(this);
	}

	/**
	 * Runs the game. Hands over control to the visual.
	 */
	public void run() {
		visual.run();
	}

	/**
	 * Start a new game.
	 */
	public void startGame(String questGen) {
		questGenerator = questManager.get(questGen);
		quests.clear();
		storage = new Storage(GRID_SIZE, GRID_SIZE, GRID_SIZE);
		balance = new Balance();

		visual.onGameStart(GRID_SIZE, GRID_SIZE, GRID_SIZE);

		if (options.getAutoRefillQuests()) {
			for (int i = 0; i < MAX_QUESTS; i++) {
				nextQuest();
			}
		}
	}

	/**
	 * Request a game stop.
	 */
	public void scheduleStop() {
		stopScheduled = true;
		visual.onScheduleStop();
	}

	/**
	 * Gets the balance.
	 * @return The balance
	 */
	public Balance getBalance() {
		return balance;
	}

	/**
	 * Gets all available language codes.
	 * @return The language codes.
	 */
	@NotNull
	public String[] getLangs() {
		return contentManager.getResources("lang", "properties").map(resource -> {
			int index = resource.getId().lastIndexOf('.');
			return resource.getId().substring(Math.max(index + 1, 0));
		}).distinct().toArray(String[]::new);
	}

	/**
	 * Add a new transaction and propagate to the visual.
	 * @param type The transaction type
	 * @param change The money change
	 * @param product The reference product if any
	 */
	private void addTransaction(@NotNull Transaction.Type type, int change, @Nullable Product product) {
		Transaction transaction = balance.add(type, change, product);
		visual.onBalanceChanged(balance.getBudget(), transaction, balance.getTotalIncome(), balance.getTotalLoss());
	}

	/**
	 * Indicate that the language has been changed.
	 */
	public void invalidateI18n() {
		visual.invalidateI18n();
	}

	/**
	 * Requests that the main visual data (storage and budget) should be resend to the visual.
	 */
	public void resendVisualGameData() {
		for (int x = 0; x < storage.getWidth(); x++) {
			for (int y = 0; y < storage.getHeight(); y++) {
				for (Pair<Integer, Product> entry : storage.get(x, y).getProductsIndexed()) {
					visual.onProductSet(x, y, entry.getFirst(), entry.getSecond());
				}
			}
		}
		for (Quest quest : quests) {
			visual.onQuestAdded(quest, quests.size() < MAX_QUESTS);
		}
		visual.onBalanceChanged(balance.getBudget(), new Transaction(Transaction.Type.NOOP, 0, ""), balance.getTotalIncome(), balance.getTotalLoss());
	}

	/**
	 * Gets and adds the next quest if possible.
	 * @return Returns whether a quest has been added.
	 */
	private boolean nextQuest() {
		if (quests.size() >= MAX_QUESTS) return false;

		if (!questGenerator.hasMoreElements()) {
			questGenerator.restart();
		}

		Quest next = questGenerator.nextElement();
		quests.add(next);

		visual.onQuestAdded(next, quests.size() < MAX_QUESTS);
		return true;
	}

	/**
	 * Indicate that the user requested the next quest.
	 * @return Returns whether a quest has been added
	 */
	public boolean userRequestQuest() {
		return nextQuest();
	}

	/**
	 * Indicate that the user abandoned a quest.
	 * @param index The quest's index in the quest list
	 */
	public void userAbandonQuest(int index) {
		if (index >= quests.size()) {
			Util.LOGGER.log(Level.SEVERE, "Attempted to abandon non-existent quest!");
			return;
		}

		Quest quest = quests.remove(index);
		addTransaction(Transaction.Type.ABANDON, -quest.getReward(), quest.getProduct());

		visual.onQuestRemoved(index);

		if (options.getAutoRefillQuests()) nextQuest();
	}

	/**
	 * Checks whether a product can be stored at the given storage location.
	 * @param product The product to check
	 * @param x The storage x position
	 * @param y The storage y position
	 * @return Returns whether the product is legal at that position
	 */
	public boolean canStoreProduct(@Nullable Product product, int x, int y) {
		return product != null && storage.get(x, y).fits(product) && product.testY(y);
	}

	/**
	 * Checks whether a quest's product can be stored at the given storage location.
	 * @param questIndex The quest's index in the quest list
	 * @param x The storage x position
	 * @param y The storage y position
	 * @return Returns whether the operation is legal
	 */
	public boolean canStoreProduct(int questIndex, int x, int y) {
		return canStoreProduct(quests.get(questIndex).getProduct(), x, y);
	}

	/**
	 * Checks whether a product can be moved from one storage position to the other.
	 * @param x1 The origin x position
	 * @param y1 The origin y position
	 * @param x2 The destination x position
	 * @param y2 The destination y position
	 * @return Returns whether the operation is legal
	 */
	public boolean canMoveProduct(int x1, int y1, int x2, int y2) {
		return canStoreProduct(storage.get(x1, y1).front(), x2, y2);
	}

	/**
	 * Indicate that the user wants to store a quest's product.
	 * @param questIndex The quest's index in the quest list
	 * @param x The storage x position
	 * @param y The storage y position
	 */
	public void userStoreProduct(int questIndex, int x, int y) {
		Quest quest = quests.get(questIndex);
		if (quest != null && quest.getType() == Quest.Type.IN) {
			Product product = quest.getProduct();

			int z = storage.get(x, y).add(product);
			if (z >= 0 && product.testY(y)) {
				quests.remove(questIndex);
				addTransaction(Transaction.Type.STORE, quest.getReward(), product);
				visual.onQuestRemoved(questIndex);
				visual.onProductSet(x, y, z, quest.getProduct());

				if (options.getAutoRefillQuests()) nextQuest();
			}
		}
	}

	/**
	 * Checks whether a product can fulfill a quest.
	 * @param questIndex The quest's index in the quest list
	 * @param x The storage x position of the product origin
	 * @param y The storage y position of the product origin
	 * @return Returns whether the operation is legal
	 */
	public boolean canDeliverProduct(int questIndex, int x, int y) {
		Quest quest = quests.get(questIndex);
		if (quest != null && quest.getType() == Quest.Type.OUT) {
			return quest.getProduct().equals(storage.get(x, y).front());
		}
		return false;
	}

	/**
	 * Indicate that that the user wants to deliver a product
	 * @param questIndex The quest's index in the quest list
	 * @param x The storage x position of the product origin
	 * @param y The storage y position of the product origin
	 * @return Returns whether the operation was successful
	 */
	public boolean userDeliverProduct(int questIndex, int x, int y) {
		if (canDeliverProduct(questIndex, x, y)) {
			Product product = storage.get(x, y).front();
			int z = storage.get(x, y).pop();
			addTransaction(Transaction.Type.DELIVER, quests.get(questIndex).getReward(), product);
			quests.remove(questIndex);
			visual.onQuestRemoved(questIndex);
			visual.onProductCleared(x, y, z, product);

			if (options.getAutoRefillQuests()) nextQuest();
			return true;
		}
		return false;
	}

	/**
	 * Checks whether two quests can resolve against each other.
	 * @param in The first quest's index in the quest list
	 * @param out The first quest's index in the quest list
	 * @return Returns whether the operation is legal
	 */
	public boolean canQuestsResolve(int in, int out) {
		int s = quests.size();
		if (in < s && in >= 0 && out < s && out >= 0) {
			if (quests.get(in).getType() == Quest.Type.IN && quests.get(out).getType() == Quest.Type.OUT) {
				Product product = quests.get(in).getProduct();
				return product.equals(quests.get(out).getProduct());
			}
		}
		return false;
	}

	/**
	 * Indicate that the user wants to resolve two quests.
	 * @param in The first quest's index in the quest list
	 * @param out The first quest's index in the quest list
	 * @return Returns whether the operation was successful
	 */
	public boolean userResolveQuests(int in, int out) {
		if (options.getAllowQuestResolving()) {
			int s = quests.size();
			if (in < s && in >= 0 && out < s && out >= 0) {
				if (quests.get(in).getType() == Quest.Type.IN && quests.get(out).getType() == Quest.Type.OUT) {
					Product product = quests.get(in).getProduct();
					if (product.equals(quests.get(out).getProduct())) {
						addTransaction(Transaction.Type.RESOLVE, quests.get(in).getReward() + quests.get(out).getReward(), product);

						if (out > in) out--;
						quests.remove(in);
						quests.remove(out);

						visual.onQuestRemoved(in);
						visual.onQuestRemoved(out);

						if (options.getAutoRefillQuests()) {
							nextQuest();
							nextQuest();
						}
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * Indicate that the user want to move a product.
	 * @param x1 The origin x position
	 * @param y1 The origin y position
	 * @param x2 The destination x position
	 * @param y2 The destination y position
	 */
	public void userMoveProduct(int x1, int y1, int x2, int y2) {
		Product product = storage.get(x1, y1).front();
		if (product != null && product.testY(y2)) {
			int z2 = storage.get(x2, y2).add(product);
			if (z2 >= 0) {
				int z1 = storage.get(x1, y1).pop();

				addTransaction(Transaction.Type.MOVE, -100, product);
				visual.onProductCleared(x1, y1, z1, product);
				visual.onProductSet(x2, y2, z2, product);
			}
		}
	}

	/**
	 * Indicate that the user wants to destroy a product.
	 * @param x The storage x position
	 * @param y The storage y position
	 */
	public void userDestroyProduct(int x, int y) {
		Product product = storage.get(x, y).front();
		int z = storage.get(x, y).pop();
		if (z >= 0) {
			addTransaction(Transaction.Type.DESTROY, -500, product);
			visual.onProductCleared(x, y, z, product);
		}
	}
}
