// This file is part of MicropolisJ.
// Copyright (C) 2013 Jason Long
// Portions Copyright (C) 1989-2007 Electronic Arts Inc.
//
// MicropolisJ is free software; you can redistribute it and/or modify
// it under the terms of the GNU GPLv3, with additional terms.
// See the README file, included in this distribution, for details.

package micropolisj.engine;

import java.util.*;

import static micropolisj.engine.TileConstants.*;
import static micropolisj.engine.TrafficGen.ZoneType;

/**
 * Process individual tiles of the map for each cycle.
 * In each sim cycle each tile will get activated, and this
 * class contains the activation code.
 */
class MapScanner extends TileBehavior
{
	final B behavior;
	TrafficGen traffic;

	MapScanner(Micropolis city, B behavior)
	{
		super(city);
		this.behavior = behavior;
		this.traffic = new TrafficGen(city);
	}

	public static enum B
	{
		RESIDENTIAL,
		HOSPITAL_CHURCH,
		COMMERCIAL,
		INDUSTRIAL,
		COAL,
		NUCLEAR,
		FIRESTATION,
		POLICESTATION,
		STADIUM_EMPTY,
		STADIUM_FULL,
		AIRPORT,
		SEAPORT;
	}

	@Override
	public void apply()
	{
		switch (behavior) {
		case RESIDENTIAL:
			doResidential();
			return;
		case HOSPITAL_CHURCH:
			doHospitalChurch();
			return;
		case COMMERCIAL:
			doCommercial();
			return;
		case INDUSTRIAL:
			doIndustrial();
			return;
		case COAL:
			doCoalPower();
			return;
		case NUCLEAR:
			doNuclearPower();
			return;
		case FIRESTATION:
			doFireStation();
			return;
		case POLICESTATION:
			doPoliceStation();
			return;
		case STADIUM_EMPTY:
			doStadiumEmpty();
			return;
		case STADIUM_FULL:
			doStadiumFull();
			return;
		case AIRPORT:
			doAirport();
			return;
		case SEAPORT:
			doSeaport();
			return;
		default:
			assert false;
		}
	}

	boolean checkZonePower()
	{
		boolean zonePwrFlag = setZonePower();

		if (zonePwrFlag)
		{
			city.poweredZoneCount++;
		}
		else
		{
			city.unpoweredZoneCount++;
		}

		return zonePwrFlag;
	}

	boolean setZonePower()
	{
		boolean oldPower = city.isTilePowered(xpos, ypos);
		boolean newPower = (
			tile == NUCLEAR ||
			tile == POWERPLANT ||
			city.hasPower(xpos,ypos)
			);

		if (newPower && !oldPower)
		{
			city.setTilePower(xpos, ypos, true);
			city.powerZone(xpos, ypos, getZoneSizeFor(tile));
		}
		else if (!newPower && oldPower)
		{
			city.setTilePower(xpos, ypos, false);
			city.shutdownZone(xpos, ypos, getZoneSizeFor(tile));
		}

		return newPower;
	}

	/**
	 * Place a 3x3 zone on to the map, centered on the current location.
	 * Note: nothing is done if part of this zone is off the edge
	 * of the map or is being flooded or radioactive.
	 *
	 * @param base The "zone" tile value for this zone.
	 * @return true iff the zone was actually placed.
	 */
	boolean zonePlop(int base)
	{
		assert isZoneCenter(base);

		TileSpec.BuildingInfo bi = Tiles.get(base).getBuildingInfo();
		assert bi != null;
		if (bi == null)
			return false;

		int xorg = xpos-1;
		int yorg = ypos-1;

		for (int y = yorg; y < yorg+bi.height; y++)
		{
			for (int x = xorg; x < xorg+bi.width; x++)
			{
				if (!city.testBounds(x, y)) {
					return false;
				}
				if (isIndestructible(city.getTile(x,y))) {
					// radioactive, on fire, or flooded
					return false;
				}
			}
		}

		assert bi.members.length == bi.width * bi.height;
		int i = 0;
		for (int y = yorg; y < yorg+bi.height; y++)
		{
			for (int x = xorg; x < xorg+bi.width; x++)
			{
				city.setTile(x, y, (char) bi.members[i]);
				i++;
			}
		}

		// refresh own tile property
		this.tile = city.getTile(xpos, ypos);

		setZonePower();
		return true;
	}

	void doCoalPower()
	{
		boolean powerOn = checkZonePower();
		city.coalCount++;
		if ((city.cityTime % 8) == 0) {
			repairZone(POWERPLANT);
		}

		city.powerPlants.add(new CityLocation(xpos,ypos));
	}

	void doNuclearPower()
	{
		boolean powerOn = checkZonePower();
		if (!city.noDisasters && PRNG.nextInt(city.MltdwnTab[city.gameLevel]+1) == 0) {
			city.doMeltdown(xpos, ypos);
			return;
		}

		city.nuclearCount++;
		if ((city.cityTime % 8) == 0) {
			repairZone(NUCLEAR);
		}

		city.powerPlants.add(new CityLocation(xpos, ypos));
	}

	void doFireStation()
	{
		boolean powerOn = checkZonePower();
		city.fireStationCount++;
		if ((city.cityTime % 8) == 0) {
			repairZone(FIRESTATION);
		}

		int z;
		if (powerOn) {
			z = city.fireEffect;  //if powered, get effect
		} else {
			z = city.fireEffect/2; // from the funding ratio
		}

		traffic.mapX = xpos;
		traffic.mapY = ypos;
		if (!traffic.findPerimeterRoad()) {
			z /= 2;
		}

		city.fireStMap[ypos/8][xpos/8] += z;
	}

	void doPoliceStation()
	{
		boolean powerOn = checkZonePower();
		city.policeCount++;
		if ((city.cityTime % 8) == 0) {
			repairZone(POLICESTATION);
		}

		int z;
		if (powerOn) {
			z = city.policeEffect;
		} else {
			z = city.policeEffect / 2;
		}

		traffic.mapX = xpos;
		traffic.mapY = ypos;
		if (!traffic.findPerimeterRoad()) {
			z /= 2;
		}

		city.policeMap[ypos/8][xpos/8] += z;
	}

	void doStadiumEmpty()
	{
		boolean powerOn = checkZonePower();
		city.stadiumCount++;
		if ((city.cityTime % 16) == 0) {
			repairZone(STADIUM);
		}

		if (powerOn)
		{
			if (((city.cityTime + xpos + ypos) % 32) == 0) {
				drawStadium(FULLSTADIUM);
				city.setTile(xpos+1,ypos, (char)(FOOTBALLGAME1));
				city.setTile(xpos+1,ypos+1,(char)(FOOTBALLGAME2));
			}
		}
	}

	void doStadiumFull()
	{
		boolean powerOn = checkZonePower();
		city.stadiumCount++;
		if (((city.cityTime + xpos + ypos) % 8) == 0) {
			drawStadium(STADIUM);
		}
	}

	void doAirport()
	{
		boolean powerOn = checkZonePower();
		city.airportCount++;
		if ((city.cityTime % 8) == 0) {
			repairZone(AIRPORT);
		}

		if (powerOn) {

			if (PRNG.nextInt(6) == 0) {
				city.generatePlane(xpos, ypos);
			}

			if (PRNG.nextInt(13) == 0) {
				city.generateCopter(xpos, ypos);
			}
		}
	}

	void doSeaport()
	{
		boolean powerOn = checkZonePower();
		city.seaportCount++;
		if ((city.cityTime % 16) == 0) {
			repairZone(PORT);
		}

		if (powerOn && !city.hasSprite(SpriteKind.SHI)) {
			city.generateShip();
		}
	}

	/**
	 * Place hospital or church if needed.
	 */
	void makeHospital()
	{
		if (city.needHospital > 0)
		{
			zonePlop(HOSPITAL);
			city.needHospital = 0;
		}

//FIXME- should be 'else if'
		if (city.needChurch > 0)
		{
			zonePlop(CHURCH);
			city.needChurch = 0;
		}
	}

	/**
	 * Called when the current tile is the key tile of a
	 * hospital or church.
	 */
	void doHospitalChurch()
	{
		boolean powerOn = checkZonePower();
		if (tile == HOSPITAL)
		{
			city.hospitalCount++;

			if (city.cityTime % 16 == 0)
			{
				repairZone(HOSPITAL);
			}
			if (city.needHospital == -1)  //too many hospitals
			{
				if (PRNG.nextInt(21) == 0)
				{
					zonePlop(RESCLR);
				}
			}
		}
		else if (tile == CHURCH)
		{
			city.churchCount++;

			if (city.cityTime % 16 == 0)
			{
				repairZone(CHURCH);
			}
			if (city.needChurch == -1) //too many churches
			{
				if (PRNG.nextInt(21) == 0)
				{
					zonePlop(RESCLR);
				}
			}
		}
	}

	/**
	 * Regenerate the tiles that make up a building zone,
	 * repairing from fire, etc.
	 * Only tiles that are not rubble, radioactive, flooded, or
	 * on fire will be regenerated.
	 *
	 * @param base The "zone" tile spec for this zone.
	 */
	void repairZone(int base)
	{
		assert isZoneCenter(base);

		boolean powerOn = city.isTilePowered(xpos, ypos);

		TileSpec.BuildingInfo bi = Tiles.get(base).getBuildingInfo();
		assert bi != null;

		int xorg = xpos-1;
		int yorg = ypos-1;

		assert bi.members.length == bi.width * bi.height;
		int i = 0;
		for (int y = 0; y < bi.height; y++)
		{
			for (int x = 0; x < bi.width; x++, i++)
			{
				int xx = xorg + x;
				int yy = yorg + y;

				TileSpec ts = Tiles.get(bi.members[i]);
				if (powerOn && ts.onPower != null) {
					ts = ts.onPower;
				}

				if (city.testBounds(xx, yy))
				{
					int thCh = city.getTile(xx, yy);
					if (isZoneCenter(thCh)) {
						continue;
					}

					if (isAnimated(thCh))
						continue;

					if (isRubble(thCh))
						continue;

					if (!isIndestructible(thCh))
					{  //not radiactive, on fire or flooded

						city.setTile(xx,yy,(char) ts.tileNumber);
					}
				}
			}
		}
	}

	/**
	 * Called when the current tile is the key tile of a commercial
	 * zone.
	 */
	void doCommercial()
	{
		boolean powerOn = checkZonePower();
		city.comZoneCount++;

		int tpop = commercialZonePop(tile);
		city.comPop += tpop;

		int trafficGood;
		if (tpop > PRNG.nextInt(6))
		{
			trafficGood = makeTraffic(ZoneType.COMMERCIAL);
		}
		else
		{
			trafficGood = 1;
		}

		if (trafficGood == -1)
		{
			int value = getCRValue();
			doCommercialOut(tpop, value);
			return;
		}

		if (PRNG.nextInt(8) == 0)
		{
			int locValve = evalCommercial(trafficGood);
			int zscore = city.comValve + locValve;

			if (!powerOn)
				zscore = -500;

			if (trafficGood != 0 &&
				zscore > -350 &&
				zscore - 26380 > (PRNG.nextInt(0x10000)-0x8000))
			{
				int value = getCRValue();
				doCommercialIn(tpop, value);
				return;
			}

			if (zscore < 350 && zscore + 26380 < (PRNG.nextInt(0x10000)-0x8000))
			{
				int value = getCRValue();
				doCommercialOut(tpop, value);
			}
		}
	}

	/**
	 * Called when the current tile is the key tile of an
	 * industrial zone.
	 */
	void doIndustrial()
	{
		boolean powerOn = checkZonePower();
		city.indZoneCount++;

		int tpop = industrialZonePop(tile);
		city.indPop += tpop;

		int trafficGood;
		if (tpop > PRNG.nextInt(6))
		{
			trafficGood = makeTraffic(ZoneType.INDUSTRIAL);
		}
		else
		{
			trafficGood = 1;
		}

		if (trafficGood == -1)
		{
			doIndustrialOut(tpop, PRNG.nextInt(2));
			return;
		}

		if (PRNG.nextInt(8) == 0)
		{
			int locValve = evalIndustrial(trafficGood);
			int zscore = city.indValve + locValve;

			if (!powerOn)
				zscore = -500;

			if (zscore > -350 &&
				zscore - 26380 > (PRNG.nextInt(0x10000)-0x8000))
			{
				int value = PRNG.nextInt(2);
				doIndustrialIn(tpop, value);
				return;
			}

			if (zscore < 350 && zscore + 26380 < (PRNG.nextInt(0x10000)-0x8000))
			{
				int value = PRNG.nextInt(2);
				doIndustrialOut(tpop, value);
			}
		}
	}

	/**
	 * Called when the current tile is the key tile of a
	 * residential zone.
	 */
	void doResidential()
	{
		boolean powerOn = checkZonePower();
		city.resZoneCount++;

		int tpop; //population of this zone
		if (tile == RESCLR)
		{
			tpop = city.doFreePop(xpos, ypos);
		}
		else
		{
			tpop = residentialZonePop(tile);
		}

		city.resPop += tpop;

		int trafficGood;
		if (tpop > PRNG.nextInt(36))
		{
			trafficGood = makeTraffic(ZoneType.RESIDENTIAL);
		}
		else
		{
			trafficGood = 1;
		}

		if (trafficGood == -1)
		{
			int value = getCRValue();
			doResidentialOut(tpop, value);
			return;
		}

		if (tile == RESCLR || PRNG.nextInt(8) == 0)
		{
			int locValve = evalResidential(trafficGood);
			int zscore = city.resValve + locValve;

			if (!powerOn)
				zscore = -500;

			if (zscore > -350 && zscore - 26380 > (PRNG.nextInt(0x10000)-0x8000))
			{
				if (tpop == 0 && PRNG.nextInt(4) == 0)
				{
					makeHospital();
					return;
				}

				int value = getCRValue();
				doResidentialIn(tpop, value);
				return;
			}

			if (zscore < 350 && zscore + 26380 < (PRNG.nextInt(0x10000)-0x8000))
			{
				int value = getCRValue();
				doResidentialOut(tpop, value);
			}
		}
	}

	/**
	 * Consider the value of building a single-lot house at certain
	 * coordinates.
	 * @return integer; positive number indicates good place for
	 * house to go; zero or a negative number indicates a bad place.
	 */
	int evalLot(int x, int y)
	{
		// test for clear lot
		int aTile = city.getTile(x,y);
		if (aTile != DIRT && !isResidentialClear(aTile)) {
			return -1;
		}

		int score = 1;

		final int [] DX = { 0, 1, 0, -1 };
		final int [] DY = { -1, 0, 1, 0 };
		for (int z = 0; z < 4; z++)
		{
			int xx = x + DX[z];
			int yy = y + DY[z];

			// look for road
			if (city.testBounds(xx, yy)) {
				int tmp = city.getTile(xx, yy);
				if (isRoad(tmp) || isRail(tmp))
				{
					score++;
				}
			}
		}

		return score;
	}

	/**
	 * Build a single-lot house on the current residential zone.
	 */
	private void buildHouse(int value)
	{
		assert value >= 0 && value <= 3;

		final int [] ZeX = { 0, -1, 0, 1, -1, 1, -1, 0, 1 };
		final int [] ZeY = { 0, -1, -1, -1, 0, 0, 1, 1, 1 };

		int bestLoc = 0;
		int hscore = 0;

		for (int z = 1; z < 9; z++)
		{
			int xx = xpos + ZeX[z];
			int yy = ypos + ZeY[z];

			if (city.testBounds(xx, yy))
			{
				int score = evalLot(xx, yy);

				if (score != 0)
				{
					if (score > hscore)
					{
						hscore = score;
						bestLoc = z;
					}

					if ((score == hscore) && PRNG.nextInt(8) == 0)
					{
						bestLoc = z;
					}
				}
			}
		}

		if (bestLoc != 0)
		{
			int xx = xpos + ZeX[bestLoc];
			int yy = ypos + ZeY[bestLoc];
			int houseNumber = value * 3 + PRNG.nextInt(3);
			assert houseNumber >= 0 && houseNumber < 12;

			assert city.testBounds(xx, yy);
			city.setTile(xx, yy, (char)(HOUSE + houseNumber));
		}
	}

	private void doCommercialIn(int pop, int value)
	{
		int z = city.getLandValue(xpos, ypos) / 32;
		if (pop > z)
			return;

		if (pop < 5)
		{
			comPlop(pop, value);
			adjustROG(8);
		}
	}

	private void doIndustrialIn(int pop, int value)
	{
		if (pop < 4)
		{
			indPlop(pop, value);
			adjustROG(8);
		}
	}

	private void doResidentialIn(int pop, int value)
	{
		assert value >= 0 && value <= 3;

		int z = city.pollutionMem[ypos/2][xpos/2];
		if (z > 128)
			return;

		if (tile == RESCLR)
		{
			if (pop < 8)
			{
				buildHouse(value);
				adjustROG(1);
				return;
			}

			if (city.getPopulationDensity(xpos, ypos) > 64)
			{
				residentialPlop(0, value);
				adjustROG(8);
				return;
			}
			return;
		}

		if (pop < 40)
		{
			residentialPlop(pop / 8 - 1, value);
			adjustROG(8);
		}
	}

	void comPlop(int density, int value)
	{
		int base = (value * 5 + density) * 9 + CZB;
		zonePlop(base);
	}

	void indPlop(int density, int value)
	{
		int base = (value * 4 + density) * 9 + IZB;
		zonePlop(base);
	}

	void residentialPlop(int density, int value)
	{
		int base = (value * 4 + density) * 9 + RZB;
		zonePlop(base);
	}

	private void doCommercialOut(int pop, int value)
	{
		if (pop > 1)
		{
			comPlop(pop-2, value);
			adjustROG(-8);
		}
		else if (pop == 1)
		{
			zonePlop(COMCLR);
			adjustROG(-8);
		}
	}

	private void doIndustrialOut(int pop, int value)
	{
		if (pop > 1)
		{
			indPlop(pop-2, value);
			adjustROG(-8);
		}
		else if (pop == 1)
		{
			zonePlop(INDCLR);
			adjustROG(-8);
		}
	}

	private void doResidentialOut(int pop, int value)
	{
		assert value >= 0 && value < 4;

		final char [] Brdr = { 0, 3, 6, 1, 4, 7, 2, 5, 8 };

		if (pop == 0)
			return;

		if (pop > 16)
		{
			// downgrade to a lower-density full-size residential zone
			residentialPlop((pop-24) / 8, value);
			adjustROG(-8);
			return;
		}

		if (pop == 16)
		{
			// downgrade from full-size zone to 8 little houses

			boolean pwr = city.isTilePowered(xpos, ypos);
			city.setTile(xpos, ypos, RESCLR);
			city.setTilePower(xpos, ypos, pwr);

			for (int x = xpos-1; x <= xpos+1; x++)
			{
				for (int y = ypos-1; y <= ypos+1; y++)
				{
					if (city.testBounds(x,y))
					{
						if (!(x == xpos && y == ypos))
						{
							// pick a random small house
							int houseNumber = value * 3 + PRNG.nextInt(3);
							city.setTile(x, y, (char) (HOUSE + houseNumber));
						}
					}
				}
			}

			adjustROG(-8);
			return;
		}

		if (pop < 16)
		{
			// remove one little house
			adjustROG(-1);
			int z = 0;

			for (int x = xpos-1; x <= xpos+1; x++)
			{
				for (int y = ypos-1; y <= ypos+1; y++)
				{
					if (city.testBounds(x,y))
					{
						int loc = city.map[y][x] & LOMASK;
						if (loc >= LHTHR && loc <= HHTHR)
						{ //little house
							city.setTile(x, y, (char)(Brdr[z] + RESCLR - 4));
							return;
						}
					}
					z++;
				}
			}
		}
	}

	/**
	 * Evaluates the zone value of the current commercial zone location.
	 * @return an integer between -3000 and 3000
	 * Same meaning as evalResidential.
	 */
	int evalCommercial(int traf)
	{
		if (traf < 0)
			return -3000;

		return city.comRate[ypos/8][xpos/8];
	}

	/**
	 * Evaluates the zone value of the current industrial zone location.
	 * @return an integer between -3000 and 3000.
	 * Same meaning as evalResidential.
	 */
	int evalIndustrial(int traf)
	{
		if (traf < 0)
			return -1000;
		else
			return 0;
	}

	/**
	 * Evaluates the zone value of the current residential zone location.
	 * @return an integer between -3000 and 3000. The higher the
	 * number, the more likely the zone is to GROW; the lower the
	 * number, the more likely the zone is to SHRINK.
	 */
	int evalResidential(int traf)
	{
		if (traf < 0)
			return -3000;

		int value = city.getLandValue(xpos, ypos);
		value -= city.pollutionMem[ypos/2][xpos/2];

		if (value < 0)
			value = 0;    //cap at 0
		else
			value *= 32;

		if (value > 6000)
			value = 6000; //cap at 6000

		return value - 3000;
	}

	/**
	 * Gets the land-value class (0-3) for the current
	 * residential or commercial zone location.
	 * @return integer from 0 to 3, 0 is the lowest-valued
	 * zone, and 3 is the highest-valued zone.
	 */
	int getCRValue()
	{
		int lval = city.getLandValue(xpos, ypos);
		lval -= city.pollutionMem[ypos/2][xpos/2];

		if (lval < 30)
			return 0;

		if (lval < 80)
			return 1;

		if (lval < 150)
			return 2;

		return 3;
	}

	/**
	 * Record a zone's population change to the rate-of-growth
	 * map.
	 * An adjustment of +/- 1 corresponds to one little house.
	 * An adjustment of +/- 8 corresponds to a full-size zone.
	 *
	 * @param amount the positive or negative adjustment to record.
	 */
	void adjustROG(int amount)
	{
		city.rateOGMem[ypos/8][xpos/8] += 4*amount;
	}

	/**
	 * Place tiles for a stadium (full or empty).
	 * @param zoneCenter either STADIUM or FULLSTADIUM
	 */
	void drawStadium(int zoneCenter)
	{
		int zoneBase = zoneCenter - 1 - 4;

		for (int y = 0; y < 4; y++)
		{
			for (int x = 0; x < 4; x++)
			{
				city.setTile(xpos - 1 + x, ypos - 1 + y, (char)zoneBase);
				zoneBase++;
			}
		}
		city.setTilePower(xpos, ypos, true);
	}

	/**
	 * @return 1 if traffic "passed", 0 if traffic "failed", -1 if no roads found
	 */
	int makeTraffic(ZoneType zoneType)
	{
		traffic.mapX = xpos;
		traffic.mapY = ypos;
		traffic.sourceZone = zoneType;
		return traffic.makeTraffic();
	}
}
