/* 
 *    Copyright 2013 Mimisuke
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import java.io.*;
import java.util.*;

public class Terrarium {	
	public static final int MAX_X = 300;
	public static final int MAX_Y = 300;
	public static final int MAX_Z = 100;

	public static ArrayList<Body> bodyList = new ArrayList<Body>();
	public static ArrayList<Food> foodList = new ArrayList<Food>();
	public static ArrayList<Shit> shitList = new ArrayList<Shit>();
	public static ArrayList<Toilet>	toiletList = new ArrayList<Toilet>();
	public static ArrayList<Barrier> barrierList = new ArrayList<Barrier>();

	private static int map[][] = new int[MAX_X+1][MAX_Y+1];
	private static Random rnd = new Random();
	private static int alarmPeriod = 0;
	private static boolean alarm = false;
	private static ArrayList<Body> babyList = new ArrayList<Body>();
	private final static int ALARM_PERIOD = 300; // 30 seconds
	
	public static void saveState(File file) throws IOException {
		ObjectOutputStream out =
				new ObjectOutputStream(
						new BufferedOutputStream(
								new FileOutputStream(file)));
		try {
			out.writeUTF(Terrarium.class.getCanonicalName());
			out.writeInt(alarmPeriod);
			out.writeBoolean(alarm);
			out.writeObject(rnd);
			out.writeObject(bodyList);
			out.writeObject(foodList);
			out.writeObject(shitList);
			out.writeObject(toiletList);
			out.writeObject(barrierList);
			out.writeObject(map);
			out.flush();
		} finally {
			out.close();
		}
	}

	@SuppressWarnings("unchecked")
	public static void loadState(File file) throws IOException, ClassNotFoundException {
		ObjectInputStream in =
				new ObjectInputStream(
						new BufferedInputStream(
								new FileInputStream(file)));
		int alarmPeriod;
		boolean alarm;
		Random rnd;
		ArrayList<Body> bodyList;
		ArrayList<Food> foodList;
		ArrayList<Shit> shitList;
		ArrayList<Toilet> toiletList;
		ArrayList<Barrier> barrierList;
		int map[][];
		try {
			String s = in.readUTF();
			if(!Terrarium.class.getCanonicalName().equals(s)) {
				String errMsg = "Bad save: "+s;
				throw new IOException(errMsg);
			}
			alarmPeriod = in.readInt();
			alarm = in.readBoolean();
			rnd = (Random)in.readObject();
			bodyList = (ArrayList<Body>)in.readObject();
			foodList = (ArrayList<Food>)in.readObject();
			shitList = (ArrayList<Shit>)in.readObject();
			toiletList = (ArrayList<Toilet>)in.readObject();
			barrierList = (ArrayList<Barrier>)in.readObject();
			map = (int [][])in.readObject();
		} finally {
			in.close();
		}
		Terrarium.rnd = rnd;
		Terrarium.alarmPeriod = alarmPeriod;
		Terrarium.alarm = alarm;
		Terrarium.bodyList = bodyList;
		Terrarium.foodList = foodList;
		Terrarium.shitList = shitList;
		Terrarium.toiletList = toiletList;
		Terrarium.barrierList = barrierList;
		Terrarium.map = map;
	}
	
	private static void setLine(int x1, int y1, int x2, int y2, boolean setFlag) {
		int distance = (int)Math.sqrt(distance(x1, y1, x2, y2));
		double deltaX = (double)(x2 - x1)/(double)distance;
		double deltaY = (double)(y2 - y1)/(double)distance;
		int sX = x1;
		int sY = y1;
		for (int t = 0; t <= distance; t++) {
			int x = sX + (int)(deltaX * t);
			int y = sY + (int)(deltaY * t);
			setPoint(x, y, setFlag);
			setPoint(Math.min(x+1, MAX_X), y, setFlag);
			setPoint(x, Math.min(y+1, MAX_Y), setFlag);
		}		
	}

	private static void setPoint(int x, int y, boolean setFlag) {
		if (setFlag) {
			map[x][y]++;
		}
		else {
			map[x][y]--;
			if (map[x][y] < 0) {
				map[x][y] = 0;
			}
		}
	}

	public static void setBarrier(int x1, int y1, int x2, int y2) {
		x1 = Math.max(0, Math.min(x1, MAX_X));
		x2 = Math.max(0, Math.min(x2, MAX_X));
		y1 = Math.max(0, Math.min(y1, MAX_Y));
		y2 = Math.max(0, Math.min(y2, MAX_Y));
		setLine(x1, y1, x2, y2, true);
		barrierList.add(new Barrier(x1, y1, x2, y2));
	}
	
	public static void clearBarrier(Barrier b) {
		int x1 = b.getSX();
		int y1 = b.getSY();
		int x2 = b.getEX();
		int y2 = b.getEY();
		setLine(x1, y1, x2, y2, false);
		barrierList.remove(b);
	}
	
	public static ArrayList<Barrier> getBarriers() {
		return barrierList;
	}
	
	public static boolean onBarrier(int cx, int cy, int thx, int thy) {
		int sx = Math.max(0, cx - thx/2);
		int sy = Math.max(0, cy - thy/2);
		int ex = Math.min(cx + thx/2, MAX_X);
		int ey = Math.min(cy + thy/2, MAX_Y);
		for (int x = sx; x < ex; x++) {
			for (int y = sy; y < ey; y++) {
				if (map[x][y] != 0) {
					return true;
				}
			}
		}
		return false;		
	}

	public static boolean onBarrier(int cx, int cy, int thickness) {
		return onBarrier(cx, cy, thickness, thickness);
	}
	
	public static Barrier getBarrier(int cx, int cy, int thickness) {
		for (Barrier b: barrierList) {
			int x1 = b.getSX();
			int y1 = b.getSY();
			int x2 = b.getEX();
			int y2 = b.getEY();
			int distance = (int)Math.sqrt(distance(x1, y1, x2, y2));
			double deltaX = (double)(x2 - x1)/(double)distance;
			double deltaY = (double)(y2 - y1)/(double)distance;
			int sX = x1;
			int sY = y1;
			for (int t = 0; t <= distance; t++) {
				int x = sX + (int)(deltaX * t);
				int y = sY + (int)(deltaY * t);
				if ((Math.abs(x - cx) <= thickness) && (Math.abs(y - cy) <= thickness)) {
					return b;
				}
			}
		}
		return null;
	}
	
	public static boolean acrossBarrier(int x1, int y1, int x2, int y2) {
		x1 = Math.max(0, Math.min(x1, MAX_X));
		x2 = Math.max(0, Math.min(x2, MAX_X));
		y1 = Math.max(0, Math.min(y1, MAX_Y));
		y2 = Math.max(0, Math.min(y2, MAX_Y));
		int distance = (int)Math.sqrt(distance(x1, y1, x2, y2));
		double deltaX = (double)(x2 - x1)/(double)distance;
		double deltaY = (double)(y2 - y1)/(double)distance;
		int sX = x1;
		int sY = y1;
		for (int t = 0; t <= distance; t++) {
			int x = sX + (int)(deltaX * t);
			int y = sY + (int)(deltaY * t);
			if (map[x][y] != 0) {
				return true;
			}
		}
		return false;
	}

	private static int distance(int x1, int y1, int x2, int y2) {
		return ((x2 - x1)*(x2 - x1)+(y2 - y1)*(y2 - y1));
	}

	private boolean checkPartner(Body b) {
		if (b.isDead() || b.isSleeping() || (!b.isExciting() && !b.isRude() && b.wantToShit())) {
			return false;
		}
		boolean ret = false;
		Body found = null;
		int minDistance = b.getEyesight();
		if (b.isExciting() && b.partner != null && !b.partner.isDead() && !b.isRaper()) {
			found = b.partner;
			minDistance = distance(b.getX(), b.getY(), found.getX(), found.getY());
		}
		else {
			// find nearest neighbor
			for (Body p:bodyList) {
				if (p == b) {
					continue;
				}
				if (b.getZ() != p.getZ()) {
					continue;
				}
				if (b.isExciting()) {
					if (!b.isRaper()) {
						if (p.isDead() || !p.isAdult() || p.isChild(b) || p.isParent(b)) {
							continue;
						}
					}
				}
				else if (p.isDead() && (!p.hasAccessory() || b.isIdiot())) {
					// yukkuri recognizes it as food.
					continue;
				}
				int dist = distance(b.getX(), b.getY(), p.getX(), p.getY());
				if (minDistance > dist) {
					if (!b.isRude()) {
						if (acrossBarrier(b.getX(), b.getY(), p.getX(), p.getY())) {
							continue;
						}
					}
					found = p;
					minDistance = dist;
				}
			}
		}
		if (found != null) {
			if (minDistance <= distance(0, 0, b.getStep(), b.getStep())) {
				if (!found.isDead()) {
					if (b.isExciting()) {
						if (!b.isTalking()) {
							b.doSukkiri(found);
						}
						ret = true;
					}
					else if (!found.hasAccessory() && b.hasAccessory() && (b.isRude() || !b.isMotherhood(found)) && !b.isDamaged()) {
						if (!b.isTalking()) {
							b.showHateYukkuri();
							found.strike(b.getStrength());
						}
					}
					else if (b.isAdult() && !found.isAdult() && found.isDirty() && (found.isChild(b) || b.isMotherhood(found))) {
						if (!b.isTalking()) {
							b.doPeropero(found);
						}
					}
					else if (b.isParent(found) && !found.isAdult()) {
						if (!b.isTalking()) {
							b.doPeropero(found);
						}
					}
					else if (found.isPartner(b)) {
						if (!b.isTalking()) {
							b.doSurisuri(found);
						}
					}
					else if (!b.isAdult() && b.isSister(found)) {
						if (!b.isTalking()) {
							if (b.isElderSister(found)) {
								b.doPeropero(found);
							}
							else {
								b.doSurisuri(found);
							}
						}
					}
				}
				else {
					if (b.isExciting()) {
						if (!b.isTalking()) {
							b.doSukkiri(found);
						}						
						ret = true;
					}
					else if (b.isAdult()) {
						if (!b.isTalking()) {
							if (b.isParent(found)) {
								b.showSadnessForChild();
							}
							else if (b.isPartner(found)) {
								b.showSadnessForPartner(found);
							}
						}
					}
					else if (b.isSister(found)){
						if (!b.isTalking()) {
							b.showSadnessForSister(found);
						}
					}
				}
			}
			else {
				if (!found.isDead()) {
					if (b.isExciting()) {
						// Exciting yukkuri goes to partner.
						b.moveToSukkiri(found.getX(), found.getY());
						ret = true;
					}
					else if (!found.hasAccessory() && b.hasAccessory() && (b.isRude() || !b.isMotherhood(found)) && !b.isDamaged()) {
						if (rnd.nextInt(20) == 0) {
							if (!b.isTalking()) {
								// Yukkuris strikes a yukkuri without accessory.
								b.showHateYukkuri();
							}
							b.moveTo(found.getX(), found.getY());
						}
					}
					else if (b.isAdult() && !found.isAdult() && found.isDirty() && (found.isChild(b) || b.isMotherhood(found))) {
						// Adult yukkuri takes care of dirty koyukkuri.
						b.moveTo(found.getX(), found.getY());
					}
					else if (b.isChild(found) && !b.isAdult() && b.isDirty()) {
						// Dirty koyukkuri goes to Parents.
						b.moveTo(found.getX(), found.getY());
					}
					else if (found.isPartner(b)) {
						if (rnd.nextInt(100) == 0) {
							// Yukkuri goes to partner.
							b.moveTo(found.getX(), found.getY());
						}
					}
					else if (!b.isAdult() && b.isSister(found)) {
						if (rnd.nextInt(100) == 0) {
							// Koyukkuri and Babyyukkuri goes to sister.
							b.moveTo(found.getX(), found.getY());
						}
					}
				}
				else {
					if (b.isExciting()) {
						b.moveToSukkiri(found.getX(), found.getY());
						ret = true;						
					}
					else if (rnd.nextInt(10) == 0) {
						if (b.isAdult()) {
							if (b.isParent(found) || b.isPartner(found)) {
								b.moveTo(found.getX(), found.getY());
							}
							else {
								b.lookTo(found.getX(), found.getY());
							}
						}
						else {
							if (b.isSister(found)) {
								b.moveTo(found.getX(), found.getY());
							}
							else {
								b.runAway(found.getX(), found.getY());
							}
						}
						if (!b.isTalking()) {
							b.showScare();
						}
					}
				}
			}
		}
		return ret;
	}

	private boolean checkFood(Body b) {
		boolean ret = false;
		if (b.isExciting() || b.isSleeping() || b.isDead() || b.isFull() || b.isScare() || b.isSick()) {
			return false;
		}
		if (!b.isRude() && !b.isIdiot() && b.wantToShit()) {
			return false;
		}
		Obj found = null;
		int minDistance = b.getEyesight();
		for (Food f:foodList) {
			if (f.isEmpty()) {
				continue;
			}
			if (b.getZ() != f.getZ()) {
				continue;
			}
			int distance = distance(b.getX(), b.getY(), f.getX(), f.getY());
			if (minDistance > distance) {
				if (!b.isRude()) {
					if (acrossBarrier(b.getX(), b.getY(), f.getX(), f.getY())) {
						continue;
					}
				}
				found = f;
				minDistance = distance;
			}
		}
		for (Shit s:shitList) {
			if (!b.isTooHungry() && !b.isIdiot()) {
				break;
			}
			if (b.getZ() != s.getZ()) {
				continue;
			}
			int distance = distance(b.getX(), b.getY(), s.getX(), s.getY());
			if (minDistance > distance) {
				if (!b.isRude()) {
					if (acrossBarrier(b.getX(), b.getY(), s.getX(), s.getY())) {
						continue;
					}
				}
				found = s;
				minDistance = distance;
			}
		}
		for (Body d:bodyList) {
			if (b == d) {
				continue;
			}
			if (b.getZ() != d.getZ()) {
				continue;
			}
			if (!d.isDead() || (d.hasAccessory() && !b.isIdiot())) {
				continue;
			}
			int distance = distance(b.getX(), b.getY(), d.getX(), d.getY());
			if (minDistance > distance) {
				if (!b.isRude()) {
					if (acrossBarrier(b.getX(), b.getY(), d.getX(), d.getY())) {
						continue;
					}
				}
				found = d;
				minDistance = distance;
			}			
		}
		if (found != null) {
			if (minDistance <= distance(0, 0, b.getStep(), b.getStep())) {
				if (!b.isTalking()) {
					if (found instanceof Food) {
						Food f = (Food)found;
						b.eatFood(f.getFoodType(), Math.min(b.getEatAmount(), f.getAmount()));
						f.eatFood(Math.min(b.getEatAmount(), f.getAmount()));
					}
					else if (found instanceof Shit) {
						Shit f = (Shit)found;
						b.eatFood(Food.type.SHIT, b.getEatAmount());
						f.eatShit(b.getEatAmount());						
					}
					else if (found instanceof Body) {
						Body f = (Body)found;
						b.eatFood(Food.type.BODY, Math.min(b.getEatAmount(), f.getAmount()));
						f.eatBody(Math.min(b.getEatAmount(), f.getAmount()));
						if (f.isSick()) {
							b.setSick();
						}
					}
				}
				ret = true;
			}
			else {
				if (b.isHungry()) {
					if (!b.isTalking()) { // moveToFood() will overwrite the message, so it needs to check.
						// go to nearest food
						if (found instanceof Food) {
							b.moveToFood(found.getX(), found.getY());
						}
						else if (found instanceof Shit){
							b.moveToShit(found.getX(), found.getY());
						}
						else if (found instanceof Body) {
							b.moveToFood(found.getX(), found.getY());
						}
					}
					ret = true;
				}
			}
		}
		else {
			if (b.isHungry()) {
				if (!b.isTalking() && (rnd.nextInt(10) == 0)) {
					b.showNoFood();
				}
			}
		}
		return ret;
	}

	private boolean checkShit(Body b) {
		if (b.isDead() || b.isIdiot() || b.isSleeping() || b.isExciting()) {
			return false;
		}
		boolean ret = false;
		Shit found = null;
		int minDistance = b.getEyesight();
		for (Shit s:shitList) {
			if (b.getZ() != s.getZ()) {
				continue;
			}
			int distance = distance(b.getX(), b.getY(), s.getX(), s.getY());
			if (minDistance > distance) {
				if (!b.isRude()) {
					if (acrossBarrier(b.getX(), b.getY(), s.getX(), s.getY())) {
						continue;
					}
				}
				found = s;
				minDistance = distance;
			}
		}
		if (found != null) {
			if (minDistance <= distance(0, 0, b.getStep(), b.getStep())) {
				if (!b.isTalking()) {
					b.showHateShit();
				}
			}
		}
		return ret;
	}

	private boolean checkToilet(Body b) {
		if (b.isDead() || b.isIdiot() || b.isSleeping() || !b.wantToShit()) {
			return false;
		}
		boolean ret = false;
		Toilet found = null;
		int minDistance = b.getEyesight();
		for (Toilet t:toiletList) {
			if (b.getZ() != t.getZ()) {
				continue;
			}
			int distance = distance(b.getX(), b.getY(), t.getX(), t.getY() - t.getSize()/6);
			if (minDistance > distance) {
				if (!b.isRude()) {
					if (acrossBarrier(b.getX(), b.getY(), t.getX(), t.getY() - t.getSize()/6)) {
						continue;
					}
				}
				found = t;
				minDistance = distance;
			}
		}
		if (found != null) {
			b.moveToToilet(found.getX(), found.getY() - found.getSize()/6);
			ret = true;
		}
		return ret;
	}

	private void addBaby(int x, int y, int z, int type, Body p1, Body p2) {
		babyList.add(makeBody(x, y, z, type, Body.AgeState.BABY, p1, p2));
	}

	public Body makeBody(int x, int y, int z, int type, Body.AgeState age, Body p1, Body p2) {
		Body b;
		switch (type) {
		case Marisa.type:
			b = new Marisa(x, y, z, age, p1, p2);
			break;
		case Reimu.type:
			b = new Reimu(x, y, z, age, p1, p2);
			break;
		case Alice.type:
			b = new Alice(x, y, z, age, p1, p2);
			break;
		case WasaReimu.type:
			b = new WasaReimu(x, y ,z, age, p1, p2);
			break;
		case Tarinai.type:
			b = new Tarinai(x, y, z, age, p1, p2);
			break;
		case MarisaReimu.type:
			b = new MarisaReimu(x, y, z, age, p1, p2);
			break;
		case ReimuMarisa.type:
			b = new ReimuMarisa(x, y, z, age, p1, p2);
			break;
		default:
			throw new RuntimeException("Unknown yukkuri type.");
		}
		return b;
	}

	public void addBody(int x, int y, int z, int type, Body.AgeState age, Body p1, Body p2) {
		bodyList.add(makeBody(x, y, z, type, age, p1, p2));
	}
	
	public void addBody(Body b) {
		bodyList.add(b);
	}

	public void addFood(int x, int y, Food.type type) {
		foodList.add(new Food(x, y, type));
	}

	public void addShit(int x, int y, int z, Body.AgeState ageState) {
		shitList.add(new Shit(x, y, z, ageState));
	}

	public void addCrushedShit(int x, int y, int z, Body.AgeState ageState) {
		Shit s = new Shit(x, y, z, ageState);
		s.crushShit();
		shitList.add(s);
	}

	public void addToilet(int x, int y) {
		toiletList.add(new Toilet(x, y));
	}

	public static void setAlarm() {
		alarm = true;
		alarmPeriod = ALARM_PERIOD;
	}

	public static boolean getAlarm() {
		return alarm;
	}

	public void run() {
		if (alarmPeriod >= 0) {
			alarmPeriod--;
			if (alarmPeriod <= 0) {
				alarmPeriod = 0;
				alarm = false;
			}
		}
		Obj.Event ret = Obj.Event.DONOTHING;
		// Update food state.
		for (Iterator<Food> i = foodList.iterator(); i.hasNext();) {
			Food f = i.next();
			ret = f.clockTick();
			if (ret == Obj.Event.REMOVED) {
				i.remove();
			}
		}
		// Update toilet state.
		for (Iterator<Toilet> i = toiletList.iterator(); i.hasNext();) {
			Toilet t = i.next();
			ret = t.clockTick();
			if (ret == Obj.Event.REMOVED) {
				i.remove();
			}
		}
		// Update shit state.
		for (Iterator<Shit> i = shitList.iterator(); i.hasNext();) {
			Shit s = i.next();
			ret = s.clockTick();
			if (ret == Obj.Event.REMOVED) {
				i.remove();
			}
		}
		// update body state
		for (Iterator<Body> i = bodyList.iterator(); i.hasNext();) {
			Body b = i.next();
			b.putStress(bodyList.size()); // Yukkuri is getting stress according as number of bodies.
			ret = b.clockTick();
			switch (ret) {
			case DEAD:
				continue;
			case BIRTHBABY:
				for (int babyType:b.getBabyTypes()) {
					addBaby(b.getX(), b.getY(), b.getZ(), babyType, b, b.partner);
				}
				b.getBabyTypes().clear();
				break;
			case DOSHIT:
				addShit(b.getX(), b.getY(), b.getZ(), b.getAgeState());
				break;
			case REMOVED:
				i.remove();
				continue;
			default:
				break;
			}
			// check Food
			if (!checkFood(b)) {
				// check Sukkiri
				if (!checkPartner(b)) {
					// check shit
					if (!checkShit(b)) {
						// check toilet
						checkToilet(b);
					}
				}
			}
		}
		// add babies.
		if (!babyList.isEmpty()) {
			bodyList.addAll(babyList);
			babyList.clear();
		}
	}
}
