package com.aionemu.gameserver.services;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aionemu.gameserver.configs.main.LoggingConfig;
import com.aionemu.gameserver.dao.InventoryDAO;
import com.aionemu.gameserver.model.gameobjects.Item;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.items.storage.Storage;
import com.aionemu.gameserver.model.items.storage.StorageType;
import com.aionemu.gameserver.model.trade.Exchange;
import com.aionemu.gameserver.model.trade.ExchangeItem;
import com.aionemu.gameserver.network.aion.serverpackets.*;
import com.aionemu.gameserver.restrictions.PlayerRestrictions;
import com.aionemu.gameserver.services.item.ItemFactory;
import com.aionemu.gameserver.services.item.ItemPacketService;
import com.aionemu.gameserver.taskmanager.AbstractFIFOPeriodicTaskManager;
import com.aionemu.gameserver.taskmanager.tasks.TemporaryTradeTimeTask;
import com.aionemu.gameserver.utils.PacketSendUtility;
import com.aionemu.gameserver.utils.audit.AuditLogger;
import com.aionemu.gameserver.utils.idfactory.IDFactory;

/**
 * @author ATracer
 */
public class ExchangeService {

	private static final Logger log = LoggerFactory.getLogger("EXCHANGE_LOG");

	private Map<Integer, Exchange> exchanges = new HashMap<>();

	private ExchangePeriodicTaskManager saveManager;

	private final int DELAY_EXCHANGE_SAVE = 5000;

	public static final ExchangeService getInstance() {
		return SingletonHolder.instance;
	}

	/**
	 * Default constructor
	 */
	private ExchangeService() {
		saveManager = new ExchangePeriodicTaskManager(DELAY_EXCHANGE_SAVE);
	}

	/**
	 * @param objectId
	 * @param objectId2
	 */
	public void registerExchange(Player player1, Player player2) {
		if (!validateParticipants(player1, player2))
			return;

		exchanges.put(player1.getObjectId(), new Exchange(player1, player2));
		exchanges.put(player2.getObjectId(), new Exchange(player2, player1));

		PacketSendUtility.sendPacket(player2, new SM_EXCHANGE_REQUEST(player1.getName()));
		PacketSendUtility.sendPacket(player1, new SM_EXCHANGE_REQUEST(player2.getName()));
	}

	/**
	 * @param player1
	 * @param player2
	 */
	private boolean validateParticipants(Player player1, Player player2) {
		return PlayerRestrictions.canTrade(player1) && PlayerRestrictions.canTrade(player2);
	}

	private Player getCurrentParter(Player player) {
		Exchange exchange = exchanges.get(player.getObjectId());
		return exchange != null ? exchange.getTargetPlayer() : null;
	}

	/**
	 * @param player
	 * @return Exchange
	 */
	private Exchange getCurrentExchange(Player player) {
		return exchanges.get(player.getObjectId());
	}

	/**
	 * @param player
	 * @return Exchange
	 */
	public Exchange getCurrentParnterExchange(Player player) {
		Player partner = getCurrentParter(player);
		return partner != null ? getCurrentExchange(partner) : null;
	}

	public boolean isPlayerInExchange(Player player) {
		return getCurrentExchange(player) != null;
	}

	/**
	 * @param activePlayer
	 * @param itemCount
	 */
	public void addKinah(Player activePlayer, long itemCount) {
		Exchange currentExchange = getCurrentExchange(activePlayer);
		if (currentExchange == null || currentExchange.isLocked())
			return;

		if (itemCount < 1)
			return;

		// count total amount in inventory
		long availableCount = activePlayer.getInventory().getKinah();

		// count amount that was already added to exchange
		availableCount -= currentExchange.getKinahCount();

		long countToAdd = availableCount > itemCount ? itemCount : availableCount;

		if (countToAdd > 0) {
			Player partner = getCurrentParter(activePlayer);
			PacketSendUtility.sendPacket(activePlayer, new SM_EXCHANGE_ADD_KINAH(countToAdd, 0));
			PacketSendUtility.sendPacket(partner, new SM_EXCHANGE_ADD_KINAH(countToAdd, 1));
			currentExchange.addKinah(countToAdd);
		}
	}

	public void addItem(Player activePlayer, int itemObjId, long itemCount) {
		Item item = activePlayer.getInventory().getItemByObjId(itemObjId);
		if (item == null)
			return;

		Player partner = getCurrentParter(activePlayer);
		if (partner == null)
			return;
		if (item.getPackCount() <= 0 && !item.isTradeable() && !TemporaryTradeTimeTask.getInstance().canTrade(item, partner.getObjectId())) {
			if (!item.isLegionTradeable() || activePlayer.getLegion() == null || !activePlayer.getLegion().equals(partner.getLegion()))
				return;
		}

		if (itemCount < 1)
			return;

		if (itemCount > item.getItemCount())
			return;

		Exchange currentExchange = getCurrentExchange(activePlayer);

		if (currentExchange == null)
			return;

		if (currentExchange.isLocked())
			return;

		if (currentExchange.isExchangeListFull())
			return;

		if (!AdminService.getInstance().canOperate(activePlayer, partner, item, "trade"))
			return;

		ExchangeItem exchangeItem = currentExchange.getItems().get(item.getObjectId());

		long actuallAddCount = 0;
		// item was not added previosly
		if (exchangeItem == null) {
			Item newItem = null;
			if (itemCount < item.getItemCount()) {
				newItem = ItemFactory.newItem(item.getItemId(), itemCount);
			} else {
				newItem = item;
			}
			exchangeItem = new ExchangeItem(itemObjId, itemCount, newItem);
			currentExchange.addItem(itemObjId, exchangeItem);
			actuallAddCount = itemCount;
		}
		// item was already added
		else {
			// if player add item count that is more than possible
			// happens with exploits
			if (item.getItemCount() == exchangeItem.getItemCount())
				return;

			long possibleToAdd = item.getItemCount() - exchangeItem.getItemCount();
			actuallAddCount = itemCount > possibleToAdd ? possibleToAdd : itemCount;
			exchangeItem.addCount(actuallAddCount);
		}

		if (!item.getItemTemplate().isStackable() || item.getItemCount() == exchangeItem.getItemCount()) {
			PacketSendUtility.sendPacket(activePlayer, new SM_DELETE_ITEM(itemObjId, ItemPacketService.ItemDeleteType.PUT_TO_EXCHANGE));
		} else {
			Item fakeItem = new Item(itemObjId, item.getItemTemplate());
			fakeItem.setItemCount(item.getItemCount() - exchangeItem.getItemCount());
			PacketSendUtility.sendPacket(activePlayer, new SM_INVENTORY_UPDATE_ITEM(activePlayer, fakeItem,
				ItemPacketService.ItemUpdateType.PUT_TO_EXCHANGE));
		}

		PacketSendUtility.sendPacket(activePlayer, new SM_EXCHANGE_ADD_ITEM(0, exchangeItem.getItem(), activePlayer));
		PacketSendUtility.sendPacket(partner, new SM_EXCHANGE_ADD_ITEM(1, exchangeItem.getItem(), partner));
	}

	/**
	 * @param activePlayer
	 */
	public void lockExchange(Player activePlayer) {
		Exchange exchange = getCurrentExchange(activePlayer);
		if (exchange != null) {
			exchange.lock();
			Player currentParter = getCurrentParter(activePlayer);
			PacketSendUtility.sendPacket(currentParter, new SM_EXCHANGE_CONFIRMATION(3));
		}
	}

	/**
	 * @param activePlayer
	 */
	public void cancelExchange(Player activePlayer) {
		Player currentPartner = getCurrentParter(activePlayer);
		returnItems(activePlayer);

		if (currentPartner != null) {
			returnItems(currentPartner);
			PacketSendUtility.sendPacket(currentPartner, new SM_EXCHANGE_CONFIRMATION(1));
		}

		cleanUpExchanges(true, activePlayer, currentPartner);
	}

	private void returnItems(Player player) {
		Exchange exchange = getCurrentExchange(player);
		if (exchange == null) {
			return;
		}
		if (!exchange.getItems().isEmpty()) {
			for (ExchangeItem exItem : exchange.getItems().values()) {
				Item realItem = player.getInventory().getItemByObjId(exItem.getItemObjId());
				if (realItem == null) {
					log.warn("Player " + player.getName() + " is trying to return fake item on exchange cancel!");
					return;
				}
				if (realItem.getItemCount() == exItem.getItemCount()) {
					PacketSendUtility.sendPacket(player, new SM_INVENTORY_ADD_ITEM(Arrays.asList(realItem), player, ItemPacketService.ItemAddType.PLAYER_EXCHANGE_GET_BACK));
				} else {
					PacketSendUtility.sendPacket(player, new SM_INVENTORY_UPDATE_ITEM(player, realItem, ItemPacketService.ItemUpdateType.INC_PLAYER_EXCHANGE_GET_BACK));
				}
			}
			PacketSendUtility.sendPacket(player, SM_CUBE_UPDATE.cubeSize(StorageType.CUBE, player));
		}
	}

	/**
	 * @param activePlayer
	 */
	public void confirmExchange(Player activePlayer) {
		if (activePlayer == null || !activePlayer.isOnline())
			return;

		Exchange currentExchange = getCurrentExchange(activePlayer);

		// TODO: Why is exchange null =/
		if (currentExchange == null)
			return;
		currentExchange.confirm();

		Player currentPartner = getCurrentParter(activePlayer);
		PacketSendUtility.sendPacket(currentPartner, new SM_EXCHANGE_CONFIRMATION(2));

		if (getCurrentExchange(currentPartner).isConfirmed()) {
			performTrade(activePlayer, currentPartner);
		}
	}

	/**
	 * @param activePlayer
	 * @param currentPartner
	 */
	private void performTrade(Player activePlayer, Player currentPartner) {
		Exchange exchange1 = getCurrentExchange(activePlayer);
		Exchange exchange2 = getCurrentExchange(currentPartner);

		if (!validateExchange(activePlayer, currentPartner)) {
			if (!validateInventorySize(currentPartner, exchange1))
				PacketSendUtility.sendPacket(activePlayer, SM_SYSTEM_MESSAGE.STR_EXCHANGE_CANT_EXCHANGE_HEAVY_TO_ADD_EXCHANGE_ITEM());
			else
				PacketSendUtility.sendPacket(activePlayer, SM_SYSTEM_MESSAGE.STR_PARTNER_TOO_HEAVY_TO_EXCHANGE());
			cleanUpExchanges(true, activePlayer, currentPartner);
			return;
		}

		if (!removeItemsFromInventory(activePlayer, exchange1) || !removeItemsFromInventory(currentPartner, exchange2)) {
			cleanUpExchanges(true, activePlayer, currentPartner);
			AuditLogger.log(activePlayer, "tried to exploit kinah exchange with partner: " + currentPartner);
			return;
		}

		PacketSendUtility.sendPacket(activePlayer, new SM_EXCHANGE_CONFIRMATION(0));
		PacketSendUtility.sendPacket(currentPartner, new SM_EXCHANGE_CONFIRMATION(0));

		putItemToInventory(activePlayer, currentPartner, exchange1, exchange2);
		putItemToInventory(currentPartner, activePlayer, exchange2, exchange1);

		saveManager.add(new ExchangeOpSaveTask(exchange1.getActiveplayer().getObjectId(), exchange2.getActiveplayer().getObjectId(), exchange1
			.getItemsToUpdate(), exchange2.getItemsToUpdate()));

		cleanUpExchanges(false, activePlayer, currentPartner);
	}

	private void cleanUpExchanges(boolean releaseIds, Player... players) {
		if (players.length == 0)
			return;

		for (Player player : players) {
			if (player == null)
				continue;

			Exchange exchange = exchanges.remove(player.getObjectId());
			if (exchange != null && releaseIds) {
				for (ExchangeItem item : exchange.getItems().values()) {
					if (item.getItemObjId() != item.getItem().getObjectId() && player.getInventory().getItemByObjId(item.getItem().getObjectId()) == null)
						IDFactory.getInstance().releaseId(item.getItem().getObjectId()); // release ID if it was a newly allocated one
				}
			}
		}
	}

	/**
	 * @param player
	 * @param exchange
	 */
	private boolean removeItemsFromInventory(Player player, Exchange exchange) {
		Storage inventory = player.getInventory();

		for (ExchangeItem exchangeItem : exchange.getItems().values()) {
			Item item = exchangeItem.getItem();
			Item itemInInventory = inventory.getItemByObjId(exchangeItem.getItemObjId());
			if (itemInInventory == null) {
				AuditLogger.log(player, "tried to trade not existing item");
				return false;
			}

			long itemCount = exchangeItem.getItemCount();

			if (itemCount < itemInInventory.getItemCount()) {
				inventory.decreaseItemCount(itemInInventory, itemCount);
				exchange.addItemToUpdate(itemInInventory);
			} else {
				// remove from source inventory only
				inventory.remove(itemInInventory);
				exchangeItem.setItem(itemInInventory);
				// release when only part stack was added in the beginning -> full stack in the end
				if (item.getObjectId() != exchangeItem.getItemObjId()) {
					IDFactory.getInstance().releaseId(item.getObjectId());
				}
				PacketSendUtility.sendPacket(player, new SM_DELETE_ITEM(itemInInventory.getObjectId()));
			}
		}
		if (!player.getInventory().tryDecreaseKinah(exchange.getKinahCount()))
			return false;
		exchange.addItemToUpdate(player.getInventory().getKinahItem());
		return true;
	}

	/**
	 * @param activePlayer
	 * @param currentPartner
	 * @return
	 */
	private boolean validateExchange(Player activePlayer, Player currentPartner) {
		Exchange exchange1 = getCurrentExchange(activePlayer);
		Exchange exchange2 = getCurrentExchange(currentPartner);
		boolean activePlayerCheck = validateInventorySize(activePlayer, exchange2);
		boolean currentPartnerCheck = validateInventorySize(currentPartner, exchange1);
		if(!activePlayerCheck) {
			PacketSendUtility.sendPacket(activePlayer, SM_SYSTEM_MESSAGE.STR_EXCHANGE_CANT_EXCHANGE_HEAVY_TO_ADD_EXCHANGE_ITEM());
			PacketSendUtility.sendPacket(currentPartner, SM_SYSTEM_MESSAGE.STR_PARTNER_TOO_HEAVY_TO_EXCHANGE());
		}	else if(!currentPartnerCheck) {
			PacketSendUtility.sendPacket(currentPartner, SM_SYSTEM_MESSAGE.STR_EXCHANGE_CANT_EXCHANGE_HEAVY_TO_ADD_EXCHANGE_ITEM());
			PacketSendUtility.sendPacket(activePlayer, SM_SYSTEM_MESSAGE.STR_PARTNER_TOO_HEAVY_TO_EXCHANGE());
		}
		return activePlayerCheck && currentPartnerCheck;
	}

	private boolean validateInventorySize(Player activePlayer, Exchange exchange) {
		int numberOfFreeSlots = activePlayer.getInventory().getFreeSlots();
		return numberOfFreeSlots >= exchange.getItems().size();
	}

	private void putItemToInventory(Player giver, Player partner, Exchange exchange1, Exchange exchange2) {
		for (ExchangeItem exchangeItem : exchange1.getItems().values()) {
			Item itemToPut = exchangeItem.getItem();
			itemToPut.setEquipmentSlot(0);
			if (itemToPut.getPackCount() > 0) // unpack
				itemToPut.setPackCount(itemToPut.getPackCount() * -1);
			partner.getInventory().add(itemToPut, ItemPacketService.ItemAddType.PLAYER_EXCHANGE_GET);
			exchange2.addItemToUpdate(itemToPut);
			if (LoggingConfig.LOG_PLAYER_EXCHANGE)
				log.info("Player " + giver.getName() + " exchanged item " + itemToPut.getItemId() + " [" + itemToPut.getItemName() + "] (count: "
					+ itemToPut.getItemCount() + ") with player " + partner.getName());
		}
		long kinahToExchange = exchange1.getKinahCount();
		if (kinahToExchange > 0) {
			partner.getInventory().increaseKinah(kinahToExchange);
			exchange2.addItemToUpdate(partner.getInventory().getKinahItem());
			if (LoggingConfig.LOG_PLAYER_EXCHANGE)
				log.info("Player " + giver.getName() + " exchanged " + kinahToExchange + " Kinah with player " + partner.getName());
		}
	}

	/**
	 * Frequent running save task
	 */
	public static final class ExchangePeriodicTaskManager extends AbstractFIFOPeriodicTaskManager<ExchangeOpSaveTask> {

		private static final String CALLED_METHOD_NAME = "exchangeOperation()";

		/**
		 * @param period
		 */
		public ExchangePeriodicTaskManager(int period) {
			super(period);
		}

		@Override
		protected void callTask(ExchangeOpSaveTask task) {
			task.run();
		}

		@Override
		protected String getCalledMethodName() {
			return CALLED_METHOD_NAME;
		}

	}

	/**
	 * This class is used for storing all items in one shot after any exchange operation
	 */
	public static final class ExchangeOpSaveTask implements Runnable {

		private int player1Id;
		private int player2Id;
		private List<Item> player1Items;
		private List<Item> player2Items;

		/**
		 * @param player1Id
		 * @param player2Id
		 * @param player1Items
		 * @param player2Items
		 */
		public ExchangeOpSaveTask(int player1Id, int player2Id, List<Item> player1Items, List<Item> player2Items) {
			this.player1Id = player1Id;
			this.player2Id = player2Id;
			this.player1Items = player1Items;
			this.player2Items = player2Items;
		}

		@Override
		public void run() {
			InventoryDAO.store(player1Items, player1Id);
			InventoryDAO.store(player2Items, player2Id);
		}
	}

	private static class SingletonHolder {

		protected static final ExchangeService instance = new ExchangeService();
	}
}
