package jp.wda.gpss.samples.games;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import jp.wda.gpss.ColonCommandSocklet;
import jp.wda.gpss.SocketProcessor;
import jp.wda.gpss.SockletDeployInfo;
import jp.wda.gpss.SockletException;
import jp.wda.gpss.util.Finder;
import jp.wda.gpss.util.SimpleXMLCreator;

/**
 * 海戦ゲームSocklet
 *
 * @version	1.00β008	2003/06/14
 * @since		1.00β007	2003/06/08
 * @author	amoi
 */
public class NavalBattle extends ColonCommandSocklet {
	// 公開フィールド ///////////////////////////////////////////////////////////////////
	//                                                            Public Static Fields //
	/////////////////////////////////////////////////////////////////////////////////////

	/* ***********************************************************************>> */;
	public static final String ADMIN_FOTTER = "_Admin";
	
	public static final int STAGE_WIDTH  = 40;
	public static final int STAGE_HEIGHT = 20;
	
	public String adminSockletName = null;
	
	public static final int STATUS_MESSAGE = 1;
	public static final int STATUS_HIT     = 8;
	public static final int STATUS_WOUNDED = 16;
	public static final int STATUS_IGNORE  = 32;
	
	// 内部フィールド定義 ///////////////////////////////////////////////////////////////
	//                                                                          Fields //
	/////////////////////////////////////////////////////////////////////////////////////

	/* ***********************************************************************>> */;
	
	private NavalBattleAreaAdministrator adminSocklet;
	private Hashtable areamap;
	
	// オーヴァーライド /////////////////////////////////////////////////////////////////
	//                                                               Over Ride Methods //
	/////////////////////////////////////////////////////////////////////////////////////

	/* ***********************************************************************>> */;
	/**
	 * BasicChat Sockletを初期化するためのメソッド。<BR>
	 * ソケット待受けサーバの起動時に、1回だけ呼ばれます。<BR><BR>
	 * 
	 * このメソッドでは、入室管理用Sockletを起動します。<BR>
	 *
	 * @see jp.wda.gpss.GeneralSocklet#init(List)
	 */
	protected void init(List linkedSocklets) {
		areamap = new Hashtable();
		this.adminSockletName = this.getName() + ADMIN_FOTTER;
		
		SockletDeployInfo appinfo = 
			this.copyInfo(adminSockletName, "jp.wda.gpss.samples.games.NavalBattleAreaAdministrator");
		
		// コントローラの方のタイムアウト時間は無限に
		appinfo.setTimeout(0);
		
		linkedSocklets.add(appinfo);
	}
	
	/**
	 * 追加配備された(はずの)入室管理用Sockletを予め取得しておきます。
	 */
	public void afterDeployedLinks(Map linkedSocklets){
		this.adminSocklet = (NavalBattleAreaAdministrator)linkedSocklets.get(adminSockletName);
	}

	/* ***********************************************************************>> */;
	/**
	 * 接続用初期コマンド受信後、クライアントの検査を行うためにメソッド。<BR>
	 * このメソッドは、サーバが新しいクライアントからの接続を確認した時点で、
	 * そのクライアントを検査するために呼ばれます。<BR><BR>
	 * 
	 * このメソッドは<BR>
	 * 
	 * @param client 接続を試みているクライアント
	 * @return 検査後、即切断する場合は偽。
	 * 
	 * @see jp.wda.gpss.Socklet#checkConnection(jp.wda.gpss.SocketProcessor)
	 */
	public boolean checkConnection(SocketProcessor client) {
		// ログイン中のユーザ確認
		Boolean loginFailure = new Boolean(false);
		client.setAttribute("loginFailure", Boolean.FALSE);
		
		String area = client.getInitParam("AREA");
		if(area == null || area.length() == 0){
			client.send("-Login failure user \"" + client.getUserName() + "\"");
			client.setAttribute("loginFailure", Boolean.TRUE);
			chatlog(area, client.getUserName(), "参戦海域が指定されていません。");
			return false;
		}
		
		List cls = getClients("username==\"" + client.getUserName() + "\"&a.AREA==\"" + area + "\"");
		if(cls.size() > 0){
			client.send("-Login failure user \"" + client.getUserName() + "\"");
			client.setAttribute("loginFailure", Boolean.TRUE);
			chatlog(area, client.getUserName(), "多重ログインはできません。");
			return false;
		}
		
		// エリアマップ設定
		ArrayList[][] map;
		if(!areamap.containsKey(area)){
			map = new ArrayList[STAGE_WIDTH][STAGE_HEIGHT];
			for(int i = 0; i < STAGE_WIDTH; i++){
				for(int j = 0; j < STAGE_HEIGHT; j++){
					map[i][j] = new ArrayList();
				}
			}
			areamap.put(area, map);
		}else{
			map = (ArrayList[][])areamap.get(area);
		}
		int newx = (int)((Math.random() * 1000) % STAGE_WIDTH);
		int newy = (int)((Math.random() * 1000) % STAGE_HEIGHT);
		client.setAttribute("POSITION", new Position(newx, newy));
		map[newx][newy].add(client);
		
		// 自分以外検索用Finder
		Finder withoutMe = preCreateFinder("a.AREA==\"" + area + "\"" + "&username!=\"" + client.getUserName() + "\"");
		client.setAttribute("FinderWithoutMe", withoutMe);
		
		client.setAttribute("AREA", area);
		setPoint(client, 100);
		enemyNotify2All(client, client.getUserName() + "が参戦しました。");
		
		client.send("+OK welcome user \"" + client.getUserName() + "\"");
		
		if(adminSocklet != null){
			int members = adminSocklet.addToRoom(area);
			if(members == 1){
				chatlog(area, client.getUserName(), "<<開戦しました>>");
			}else{
				chatlog(area, client.getUserName(), "<<参戦しました>>");
			}
		}
		return true;
	}

	/* ***********************************************************************>> */;
	/**
	 * クライアント削除前に呼び出されるメソッド。<BR>
	 * 接続中のクライアントが切断され、このアプリケーションから除外される直前に呼ばれます。<BR><BR>
	 * 
	 * このメソッドは<BR>
	 *
	 * @param client これから削除されるクライアント
	 * 
	 * @see jp.wda.gpss.Socklet#preRemoveClient(jp.wda.gpss.SocketProcessor)
	 */
	public void preRemoveClient(SocketProcessor client) {
		if(((Boolean)client.getAttribute("loginFailure")).booleanValue()){ return; }
		String area = (String)client.getAttribute("AREA");
		
		Position pos = (Position)client.getAttribute("POSITION");
		ArrayList[][] map = (ArrayList[][])areamap.get(area);
		if(map.length > pos.x && map[pos.x].length > pos.y){
			map[pos.x][pos.y].remove(client);
		}
		
		setPoint(client, -1);
		enemyNotify2All(client, client.getUserName() + "が退却しました。");
		
		if(adminSocklet != null && area != null){
			int members = adminSocklet.removeFromRoom(area);
			if(members == 0){
				chatlog(area, client.getUserName(), "<<停戦しました>>");
			}else{
				chatlog(area, client.getUserName(), "<<退却しました>>");
			}
		}
	}
	
	/* ***********************************************************************>> */;
	/**
	 * コマンド名処理リフレクションメソッド未定義字の処理を行ないます。
	 * 
	 * @param client コマンドを送信してきたクライアントソケット
	 * @param command 未定義の実行コマンド名
	 * @param params コマンド値
	 * @return コマンド実行に成功した場合は真、失敗した場合は偽
	 * 	偽を返すと直ちにクライアントソケットを終了します。
	 * @see jp.wda.gpss.ColonCommandSocklet#cmdUndefined(SocketProcessor, String, Object[])
	 */
	public boolean cmdUndefined(SocketProcessor client, String command, Object[] params){
		log("receive " + command + " command. method not found. " + client.getUserName());
		return true;
	}
	
	/**
	 * コマンド実行前共通処理を行ないます。<BR>
	 * 
	 * @param client コマンドを送信してきたクライアントソケット
	 * @param command 受信したコマンド
	 * @return 前処理結果オブジェクト
	 * @throws SockletException コマンド解釈処理を中止したい場合。
	 * @see jp.wda.gpss.ColonCommandSocklet#preProcess(SocketProcessor, String)
	 */
	public Object preProcess(SocketProcessor client, String command) throws SockletException{
		if(((Boolean)client.getAttribute("loginFailure")).booleanValue()){
			throw new SockletException("ログインに失敗しています。");
		}
		try{
			return new Position(command);
		}catch(PositionParseException e){ return null; }
	}

	// コマンド名処理リフレクションメソッド /////////////////////////////////////////////
	//                                                      Command Reflection Methods //
	/////////////////////////////////////////////////////////////////////////////////////
	
	/* ***********************************************************************>> */;
	/**
	 * init:コマンド処理メソッド
	 * 
	 * @param client コマンドを送信してきたクライアントソケット
	 * @param value コマンド値
	 * @param preprocessed preProcessから渡された前処理結果オブジェクト
	 * @return 常に真
	 */
	public boolean cmdInit(SocketProcessor client, String value, Object preprocessed) {
		String area  = (String)client.getAttribute("AREA");
		Position pos = (Position)client.getAttribute("POSITION");

		SimpleXMLCreator xml = new SimpleXMLCreator("INIT", this);
		xml.setAttribute("point", getPoint(client));
		xml.setAttribute("area", area);
		xml.setAttribute("name", client.getUserName());
		xml.setAttribute("x", pos.x);
		xml.setAttribute("y", pos.y);
		xml.send(client);
		
		xml = new SimpleXMLCreator("ENEMY", this);
		List cls = getClients((Finder)client.getAttribute("FinderWithoutMe"));
		for(int i = 0; i < cls.size(); i++){
			SocketProcessor enemy = (SocketProcessor)cls.get(i);
			int point = getPoint(enemy);
			xml.setAttribute("point", getPoint(enemy));
			xml.setAttribute("name", enemy.getUserName());
			xml.send(client);
		}
		
		chatlog(area, client.getUserName(), "Initialize");
		return true;
	}
	
	/* ***********************************************************************>> */;
	/**
	 * move:コマンド処理メソッド
	 * 
	 * @param client コマンドを送信してきたクライアントソケット
	 * @param value コマンド値
	 * @param preprocessed preProcessから渡された前処理結果オブジェクト
	 * @return 常に真
	 */
	public boolean cmdMove(SocketProcessor client, String value, Object preprocessed) {
		if(preprocessed == null){ return true; }
		Position newpos = (Position)preprocessed;
		String area = (String)client.getAttribute("AREA");
		ArrayList[][] map = getMap(area, newpos);
		if(map == null){ return true; }
		
		Position oldpos = (Position)client.getAttribute("POSITION");
		client.setAttribute("POSITION", newpos);
		map[oldpos.x][oldpos.y].remove(client);
		map[newpos.x][newpos.y].add(client);
		
		SimpleXMLCreator xml = new SimpleXMLCreator("MOVE", this);
		xml.setAttribute("x", newpos.x);
		xml.setAttribute("y", newpos.y);
		xml.send(client);
		
		return true;
	}
	
	
	/* ***********************************************************************>> */;
	/**
	 * fire:コマンド処理メソッド
	 * 
	 * @param client コマンドを送信してきたクライアントソケット
	 * @param value コマンド値
	 * @param preprocessed preProcessから渡された前処理結果オブジェクト
	 * @return 常に真
	 */
	public boolean cmdFire(SocketProcessor client, String value, Object preprocessed) {
		if(preprocessed == null){ return true; }
		Position pos = (Position)preprocessed;
		String area = (String)client.getAttribute("AREA");
		ArrayList[][] map = getMap(area, pos);
		if(map == null){ return true; }
		
		Position cpos = (Position)client.getAttribute("POSITION");
		int xdis = Math.abs(cpos.x - pos.x);
		int ydis = Math.abs(cpos.y - pos.y);
		double distance = Math.sqrt(xdis * xdis + ydis * ydis);
		int pow = (int)Math.round(20.0 - (distance / Math.sqrt(5)));
		double dir = ((pos.x - cpos.x) < 0 ? 1 : -1) * 180 * Math.acos((pos.y - cpos.y) / distance) / Math.PI;
		
		addPoint(client, -1);
		SimpleXMLCreator xml = new SimpleXMLCreator("FIRED", this);
		xml.setAttribute("x", pos.x);
		xml.setAttribute("y", pos.y);
		xml.setAttribute("dir", dir);
		xml.send((Finder)client.getAttribute("FinderWithoutMe"));
		
		synchronized(map){
			// 着弾確認
			int bonus = 0, hit = 0;
			for(int i = 0; i < map[pos.x][pos.y].size(); i++){
				SocketProcessor enemy = (SocketProcessor)map[pos.x][pos.y].get(i);
				
				// 最終被弾時刻の検査
				if(enemy.containsAttributeKey("LastWounded")){
					try{
						long lastWounded = enemy.getAttributeLong("LastWounded");
						if(System.currentTimeMillis() - lastWounded < 1000){ continue; }
					}catch(Exception e){ ; }
					
					enemy.removeAttribute("LastWounded");
				}
				
				hit++;
				bonus += (20 - pow) / 2 + 1;
				addPoint(enemy, -pow);
				
				int now = getPoint(enemy);
				if(now <= 0){
					enemy.send("<GAMEOVER />");
					map[pos.x][pos.y].remove(enemy);
				}else{
					sendNotify(enemy, STATUS_WOUNDED, "", "");
					enemy.setAttribute("LastWounded", System.currentTimeMillis());
				}
				chatlog(area, client.getUserName(), enemy.getUserName() + "に着弾");
				
				enemyNotify2All(enemy, null);
			}
			
			if(hit > 0){
				addPoint(client, bonus + 1);
				sendNotify(client, STATUS_HIT, "" + bonus, "");
			}else{
				sendNotify(client, STATUS_IGNORE, "", "");
			}
			enemyNotify2All(client, null);
			
			// 周辺検査
			int w = map.length - 1, h = map[pos.x].length - 1;
			int f1 = 0;
			if(pos.x > 0)             { f1 += map[pos.x - 1][pos.y].size(); }
			if(pos.x > 0 && pos.y > 0){ f1 += map[pos.x - 1][pos.y - 1].size(); }
			if(             pos.y > 0){ f1 += map[pos.x][pos.y - 1].size(); }
			if(pos.x < w && pos.y > 0){ f1 += map[pos.x + 1][pos.y - 1].size(); }
			if(pos.x < w)             { f1 += map[pos.x + 1][pos.y].size(); }
			if(pos.x < w && pos.y < h){ f1 += map[pos.x + 1][pos.y + 1].size(); }
			if(             pos.y < h){ f1 += map[pos.x][pos.y + 1].size(); }
			if(pos.x > 0 && pos.y < h){ f1 += map[pos.x - 1][pos.y + 1].size(); }
			
			int f2 = 0;
			if(pos.x > 1)                     { f2 += map[pos.x - 2][pos.y].size(); }
			if(pos.x > 1     && pos.y > 0)    { f2 += map[pos.x - 2][pos.y - 1].size(); }
			if(pos.x > 1     && pos.y > 1)    { f2 += map[pos.x - 2][pos.y - 2].size(); }
			if(pos.x > 0     && pos.y > 1)    { f2 += map[pos.x - 1][pos.y - 2].size(); }
			if(                 pos.y > 1)    { f2 += map[pos.x][pos.y - 2].size(); }
			if(pos.x     < w && pos.y > 1)    { f2 += map[pos.x + 1][pos.y - 2].size(); }
			if(pos.x + 1 < w && pos.y > 1)    { f2 += map[pos.x + 2][pos.y - 2].size(); }
			if(pos.x + 1 < w && pos.y > 0)    { f2 += map[pos.x + 2][pos.y - 1].size(); }
			if(pos.x + 1 < w)                 { f2 += map[pos.x + 2][pos.y].size(); }
			if(pos.x + 1 < w && pos.y     < h){ f2 += map[pos.x + 2][pos.y + 1].size(); }
			if(pos.x + 1 < w && pos.y + 1 < h){ f2 += map[pos.x + 2][pos.y + 2].size(); }
			if(pos.x     < w && pos.y + 1 < h){ f2 += map[pos.x + 1][pos.y + 2].size(); }
			if(                 pos.y + 1 < h){ f2 += map[pos.x][pos.y + 2].size(); }
			if(pos.x > 0     && pos.y + 1 < h){ f2 += map[pos.x - 1][pos.y + 2].size(); }
			if(pos.x > 1     && pos.y + 1 < h){ f2 += map[pos.x - 2][pos.y + 2].size(); }
			if(pos.x > 1     && pos.y     < h){ f2 += map[pos.x - 2][pos.y + 1].size(); }
			
			// 自分が含まれている場合検査
			if(f2 > 0 && (distance <= Math.sqrt(8.0) && distance >= 2.0)){ f2--; }
			
			xml = new SimpleXMLCreator("FIRE_RESULT", this);
			xml.setAttribute("x", pos.x);
			xml.setAttribute("y", pos.y);
			xml.setAttribute("f1", f1);
			xml.setAttribute("f2", f2);
			xml.setAttribute("hit", hit);
			xml.send(client);
		}
		
		return true;
	}

	// 内部メソッド /////////////////////////////////////////////////////////////////////
	//                                                                 Private Methods //
	/////////////////////////////////////////////////////////////////////////////////////

	/* ***********************************************************************>> */;
	private void setPoint(SocketProcessor client, int point){
		client.setAttribute("POINT", point);
	}
	
	private int getPoint(SocketProcessor client){
		try{
			return client.getAttributeInt("POINT");
		}catch(Exception e){ return -1; }
	}
	
	private void addPoint(SocketProcessor client, int point){
		try{
			client.setAttribute("POINT", client.getAttributeInt("POINT") + point);
		}catch(Exception e){ return; }
	}
	
	private void enemyNotify2All(SocketProcessor client, String message){
		int point = getPoint(client);
		
		SimpleXMLCreator xml = new SimpleXMLCreator("ENEMY", this);
		xml.setAttribute("name", client.getUserName());
		xml.setAttribute("point", (point >= 0 ? "" + point : "retreat"));
		xml.send((Finder)client.getAttribute("FinderWithoutMe"));
		
		if(message == null){ return; }
		
		xml = new SimpleXMLCreator("NOTIFY", this);
		xml.setAttribute("status", STATUS_MESSAGE);
		xml.setAttribute("message", message);
		xml.send((Finder)client.getAttribute("FinderWithoutMe"));
	}
	
	private void sendNotify(SocketProcessor client, int status, String option, String message){
		SimpleXMLCreator xml = new SimpleXMLCreator("NOTIFY", this);
		xml.setAttribute("point", getPoint(client));
		xml.setAttribute("status", status);
		xml.setAttribute("option", option);
		xml.setAttribute("message", message);
		xml.send(client);
	}
	
	private ArrayList[][] getMap(String area, Position pos){
		ArrayList[][] map = (ArrayList[][])areamap.get(area);
		if(map               == null){ return null; }
		if(map.length        <= pos.x){ return null; }
		if(map[pos.x].length <= pos.y){ return null; }
		
		return map;
	}
	
	/* ***********************************************************************>> */;
	/**
	 * チャットログをとります。
	 * 
	 * @param room チャット部屋名
	 * @param user ユーザー
	 * @param memo 発言内容、またはシステムメッセージ
	 */
	private void chatlog(String room, String user, String memo){
		log(room + "," + user + "," + memo);
	}

	// 内部クラス ///////////////////////////////////////////////////////////////////////
	//                                                                     Inner Class //
	/////////////////////////////////////////////////////////////////////////////////////
	private class PositionParseException extends Exception{
		PositionParseException(String message){ super(message); }
	}
	private class Position{
		Position(int x, int y){
			this.x = x;
			this.y = y;
		}
		Position(String str) throws PositionParseException{
			int colonIdx = str.indexOf(':');
			if(colonIdx >= 0){
				str = str.substring(colonIdx + 1);
			}
			
			int idx = str.indexOf(',');
			if(idx < 0){ throw new PositionParseException("\",\" not found"); }
			int newx = 0, newy = 0;
			this.x = Integer.parseInt(str.substring(0, idx));
			this.y = Integer.parseInt(str.substring(idx + 1));
		}
		private int x;
		private int y;
	}
}
