/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * http://www.gnu.org/copyleft/gpl.html
 */
package com.l2jfrozen.util;

import java.util.Hashtable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.l2jfrozen.gameserver.controllers.GameTimeController;
import com.l2jfrozen.gameserver.model.actor.instance.L2PcInstance;
import com.l2jfrozen.gameserver.network.L2GameClient;
import com.l2jfrozen.loginserver.L2LoginClient;
import com.l2jfrozen.loginserver.LoginController;
import com.l2jfrozen.loginserver.network.serverpackets.LoginFail.LoginFailReason;
import com.l2jfrozen.netcore.Config;
import com.l2jfrozen.netcore.MMOClient;

/**
 * 
 * 
 * @author Enzo
 */
public class PacketsFloodProtector
{
	private final static int MAX_CONCURRENT_ACTIONS_PER_PLAYER = 10; 

	private static Hashtable<String, AtomicInteger> clients_concurrent_actions = 
		new Hashtable<String, AtomicInteger>();
	
	private static final Logger _log = Logger.getLogger(PacketsFloodProtector.class.getName());
	
	private static Hashtable<String, Hashtable<Integer, AtomicInteger>> clients_actions = 
		new Hashtable<String, Hashtable<Integer, AtomicInteger>>();
	
	private static Hashtable<String, Hashtable<Integer, Integer>> clients_nextGameTick = 
		new Hashtable<String, Hashtable<Integer, Integer>>();

	private static Hashtable<String, Boolean> punishes_in_progress = 
		new Hashtable<String, Boolean>();


	/**
	 * Checks whether the request is flood protected or not.
	 * 
	 * @return true if action is allowed, otherwise false
	 */
	public static boolean tryPerformAction(final int opcode, final int opcode2, MMOClient<?> client)
	{
		if(Config.getInstance().DISABLE_FULL_PACKETS_FLOOD_PROTECTOR){
			return true;
		}
		
		//filter on opcodes
		if(!isOpCodeToBeTested(opcode, opcode2, client instanceof L2LoginClient)){
			return true;
		}
		
		String account = "";
		
		if(client instanceof L2LoginClient){
			
			L2LoginClient login_cl = (L2LoginClient) client;
			account = login_cl.getAccount();
			
		}else if(client instanceof L2GameClient){
			
			L2GameClient game_cl = (L2GameClient) client;
			account = game_cl.accountName;
			
		}

		if(account==null)
			return true;
			
		//get actual concurrent actions number for account
		AtomicInteger actions_per_account = clients_concurrent_actions.get(account);
		if(actions_per_account == null){
			actions_per_account = new AtomicInteger(0);
		}
		if(actions_per_account.get() < MAX_CONCURRENT_ACTIONS_PER_PLAYER){
			
			int actions = actions_per_account.incrementAndGet();
			
			if (Config.getInstance().ENABLE_MMOCORE_DEBUG)
			{
				_log.info(" -- account "+account+" has performed "+actions+" concurrent actions until now");
			}
			
			clients_concurrent_actions.put(account, actions_per_account);
		}else
			return false;
		
		final int curTick = GameTimeController.getGameTicks();
		
		Hashtable<Integer, Integer> account_nextGameTicks = clients_nextGameTick.get(account);
		if(account_nextGameTicks == null){
			account_nextGameTicks = new Hashtable<Integer, Integer>();
		}
		Integer _nextGameTick = account_nextGameTicks.get(opcode);
		if(_nextGameTick == null){
			_nextGameTick = curTick;
			account_nextGameTicks.put(opcode,_nextGameTick);
		}
		clients_nextGameTick.put(account, account_nextGameTicks);
		
		Boolean _punishmentInProgress = punishes_in_progress.get(account);
		if(_punishmentInProgress == null){
			_punishmentInProgress = false;
		}else if(_punishmentInProgress == true){
			AtomicInteger actions = clients_concurrent_actions.get(account);
			actions.decrementAndGet();
			clients_concurrent_actions.put(account,actions);
			return false;
		}
		punishes_in_progress.put(account,_punishmentInProgress);
		
		Hashtable<Integer, AtomicInteger> received_commands_actions = clients_actions.get(account);
		if(received_commands_actions == null){
			received_commands_actions = new Hashtable<Integer, AtomicInteger>();
		}
		AtomicInteger command_count = null;
		if((command_count = received_commands_actions.get(opcode))==null){
			command_count = new AtomicInteger(0);
			received_commands_actions.put(opcode, command_count);
		}
		clients_actions.put(account,received_commands_actions);
		
		if (curTick <= _nextGameTick && !_punishmentInProgress) //time to check operations
		{
			command_count.incrementAndGet();
			clients_actions.get(account).put(opcode, command_count);
			
			if (Config.getInstance().ENABLE_MMOCORE_DEBUG)
			{
				_log.info("-- called OpCode "+ Integer.toHexString(opcode)+ " ~"+ String.valueOf((Config.getInstance().FLOOD_PACKET_PROTECTION_INTERVAL - (_nextGameTick - curTick)) * GameTimeController.MILLIS_IN_TICK)+ " ms after first command...");
				_log.info("   total received packets with OpCode "+Integer.toHexString(opcode)+" into the Interval: "+command_count.get());
			}
			
			if (Config.getInstance().PACKET_FLOODING_PUNISHMENT_LIMIT > 0 && command_count.get() >= Config.getInstance().PACKET_FLOODING_PUNISHMENT_LIMIT  && Config.getInstance().PACKET_FLOODING_PUNISHMENT_TYPE != null)
			{
				punishes_in_progress.put(account, true);
				
				if(!isOpCodeToBeTested(opcode, opcode2, client instanceof L2LoginClient)){
					
					if (Config.getInstance().LOG_PACKET_FLOODING && _log.isLoggable(Level.WARNING))
						_log.warning("ATTENTION: Account "+account+" is flooding the server...");
					
					
					if ("kick".equals(Config.getInstance().PACKET_FLOODING_PUNISHMENT_TYPE))
					{
						if (Config.getInstance().LOG_PACKET_FLOODING && _log.isLoggable(Level.WARNING))
							_log.warning(" ------- kicking account "+account);
						kickPlayer(client, opcode);
					}
					else if ("ban".equals(Config.getInstance().PACKET_FLOODING_PUNISHMENT_TYPE))
					{
						if (Config.getInstance().LOG_PACKET_FLOODING && _log.isLoggable(Level.WARNING))
							_log.warning(" ------- banning account "+account);
						banAccount(client, opcode);
					}
					
				}
				
				//clear already punished account
				punishes_in_progress.remove(account);
				clients_nextGameTick.remove(account);
				clients_actions.remove(account);
				clients_concurrent_actions.remove(account);
				
				return false;
				
			}
			
			if(curTick == _nextGameTick){ //if is the first time, just calculate the next game tick
				_nextGameTick = curTick + Config.getInstance().FLOOD_PACKET_PROTECTION_INTERVAL;
				clients_nextGameTick.get(account).put(opcode, _nextGameTick);
			}
			
			AtomicInteger actions = clients_concurrent_actions.get(account);
			actions.decrementAndGet();
			clients_concurrent_actions.put(account,actions);
			
			return true;
			
		}else{ //cur time more then checked one  --> restart opcode
			
			punishes_in_progress.put(account, false);
			clients_nextGameTick.get(account).remove(opcode);
			clients_actions.get(account).remove(opcode);
			
			AtomicInteger actions = clients_concurrent_actions.get(account);
			actions.decrementAndGet();
			clients_concurrent_actions.put(account,actions);
			
			return true;
			
		}
		
	}
	
	private static boolean isOpCodeToBeTested(int opcode, int opcode2, boolean loginclient){
		
		if(loginclient){
			
			return !Config.getInstance().LS_LIST_PROTECTED_OPCODES.contains(opcode);
			
		}else{
			
			if(opcode == 0xd0){
				
				if(Config.getInstance().GS_LIST_PROTECTED_OPCODES.contains(opcode)){
					
					return !Config.getInstance().GS_LIST_PROTECTED_OPCODES2.contains(opcode2);
					
				}else
					return true;
				
			}else{
				
				return !Config.getInstance().GS_LIST_PROTECTED_OPCODES.contains(opcode);
				
			}
			
		}

	}
	
	/**
	 * Kick player from game (close network connection).
	 */
	private static void kickPlayer(MMOClient<?> _client, int opcode)
	{
		if(_client instanceof L2LoginClient){
			
			L2LoginClient login_cl = (L2LoginClient) _client;
			login_cl.close(LoginFailReason.REASON_SYSTEM_ERROR);
			
			_log.warning("Player with account "+login_cl.getAccount() +" kicked for flooding with packet "+Integer.toHexString(opcode));
			
		}else if(_client instanceof L2GameClient){
			
			L2GameClient game_cl = (L2GameClient) _client;
			game_cl.closeNow();
			
			_log.warning("Player with account "+game_cl.accountName +" kicked for flooding with packet "+Integer.toHexString(opcode));
			
		}
		
	}

	/**
	 * Bans char account and logs out the char.
	 */
	private static void banAccount(MMOClient<?> _client, int opcode)
	{
		
		if(_client instanceof L2LoginClient){
			
			L2LoginClient login_cl = (L2LoginClient) _client;
			LoginController.getInstance().setAccountAccessLevel(login_cl.getAccount(), -100);
			login_cl.close(LoginFailReason.REASON_SYSTEM_ERROR);
			
			_log.warning("Player with account "+login_cl.getAccount() +" banned for flooding forever with packet "+Integer.toHexString(opcode));
			
		}else if(_client instanceof L2GameClient){
			
			L2GameClient game_cl = (L2GameClient) _client;
			
			if (game_cl.getActiveChar() != null)
			{
				game_cl.getActiveChar().setPunishLevel(L2PcInstance.PunishLevel.ACC, 0);

				_log.warning("Player "+game_cl.getActiveChar()+" of account "+game_cl.accountName +" banned forever for flooding with packet "+Integer.toHexString(opcode));

				game_cl.getActiveChar().logout();
			}
			
			game_cl.closeNow();
			_log.warning("Player with account "+game_cl.accountName +" kicked for flooding with packet "+Integer.toHexString(opcode));
			
		}

		
	}


}
