// 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.gui;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.net.URL;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

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

public class OverlayMapView extends JComponent
	implements Scrollable, MapListener
{
	Micropolis engine;
	ArrayList<ConnectedView> views = new ArrayList<ConnectedView>();
	MapState mapState = MapState.ALL;

	public OverlayMapView(Micropolis _engine)
	{
		assert _engine != null;

		MouseAdapter mouse = new MouseAdapter() {
			@Override
			public void mousePressed(MouseEvent ev)
			{
				onMousePressed(ev);
			}
			@Override
			public void mouseDragged(MouseEvent ev)
			{
				onMouseDragged(ev);
			}
		};
		addMouseListener(mouse);
		addMouseMotionListener(mouse);

		setEngine(_engine);
	}

	public Micropolis getEngine()
	{
		return engine;
	}

	public void setEngine(Micropolis newEngine)
	{
		assert newEngine != null;

		if (engine != null) { //old engine
			engine.removeMapListener(this);
		}
		engine = newEngine;
		if (engine != null) { //new engine
			engine.addMapListener(this);
		}

		invalidate();  //map size may have changed
		repaint();
		engine.calculateCenterMass();
		dragViewToCityCenter();
	}

	public MapState getMapState()
	{
		return mapState;
	}

	@Override
	public Dimension getPreferredSize()
	{
		return new Dimension(
			getInsets().left + getInsets().right + TILE_WIDTH*engine.getWidth(),
			getInsets().top + getInsets().bottom + TILE_HEIGHT*engine.getHeight()
			);
	}

	public void setMapState(MapState newState)
	{
		if (mapState == newState)
			return;

		mapState = newState;
		repaint();
	}

	static BufferedImage tileArrayImage = loadImage("/tilessm.png");
	static final int TILE_WIDTH = 3;
	static final int TILE_HEIGHT = 3;
	static final int TILE_OFFSET_Y = 3;

	static BufferedImage loadImage(String resourceName)
	{
		URL iconUrl = MicropolisDrawingArea.class.getResource(resourceName);
		Image refImage = new ImageIcon(iconUrl).getImage();

		BufferedImage bi = new BufferedImage(refImage.getWidth(null), refImage.getHeight(null),
					BufferedImage.TYPE_INT_RGB);
		Graphics2D gr = bi.createGraphics();
		gr.drawImage(refImage, 0, 0, null);

		return bi;
	}

	static final Color VAL_LOW       = new Color(0xbfbfbf);
	static final Color VAL_MEDIUM    = new Color(0xffff00);
	static final Color VAL_HIGH      = new Color(0xff7f00);
	static final Color VAL_VERYHIGH  = new Color(0xff0000);
	static final Color VAL_PLUS      = new Color(0x007f00);
	static final Color VAL_VERYPLUS  = new Color(0x00e600);
	static final Color VAL_MINUS     = new Color(0xff7f00);
	static final Color VAL_VERYMINUS = new Color(0xffff00);

	private Color getCI(int x)
	{
		if (x < 50)
			return null;
		else if (x < 100)
			return VAL_LOW;
		else if (x < 150)
			return VAL_MEDIUM;
		else if (x < 200)
			return VAL_HIGH;
		else
			return VAL_VERYHIGH;
	}

	private Color getCI_rog(int x)
	{
		if (x > 100)
			return VAL_VERYPLUS;
		else if (x > 20)
			return VAL_PLUS;
		else if (x < -100)
			return VAL_VERYMINUS;
		else if (x < -20)
			return VAL_MINUS;
		else
			return null;
	}

	private void drawLandMap(Graphics gr)
	{
		int [][] A = engine.landValueMem;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6);
			}
		}
	}

	private void drawPollutionMap(Graphics gr)
	{
		int [][] A = engine.pollutionMem;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI(10 + A[y][x]),x*6,y*6,6,6);
			}
		}
	}

	private void drawCrimeMap(Graphics gr)
	{
		int [][] A = engine.crimeMem;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6);
			}
		}
	}

	private void drawTrafficMap(Graphics gr)
	{
		int [][] A = engine.trfDensity;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6);
			}
		}
	}

	private void drawPopDensity(Graphics gr)
	{
		int [][] A = engine.popDensity;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI(A[y][x]),x*6,y*6,6,6);
			}
		}
	}

	private void drawRateOfGrowth(Graphics gr)
	{
		int [][] A = engine.rateOGMem;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI_rog(A[y][x]),x*24,y*24,24,24);
			}
		}
	}

	private void drawFireRadius(Graphics gr)
	{
		int [][] A = engine.fireRate;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI(A[y][x]),x*24,y*24,24,24);
			}
		}
	}

	private void drawPoliceRadius(Graphics gr)
	{
		int [][] A = engine.policeMapEffect;

		for (int y = 0; y < A.length; y++) {
			for (int x = 0; x < A[y].length; x++) {
				maybeDrawRect(gr, getCI(A[y][x]),x*24,y*24,24,24);
			}
		}
	}

	private void maybeDrawRect(Graphics gr, Color col, int x, int y, int width, int height)
	{
		if (col != null) {
			gr.setColor(col);
			gr.fillRect(x,y,width,height);
		}
	}

	static final int UNPOWERED  = 0x6666e6;   //lightblue
	static final int POWERED    = 0xff0000;   //red
	static final int CONDUCTIVE = 0xbfbfbf;   //lightgray

	private int checkPower(BufferedImage img, int x, int y, int rawTile)
	{
		int pix;

		if ((rawTile & LOMASK) <= 63) {
			return rawTile & LOMASK;
		}
		else if ((rawTile & ZONEBIT) != 0) {
			// zone
			pix = ((rawTile & PWRBIT) != 0) ? POWERED : UNPOWERED;
		}
		else if ((rawTile & CONDBIT) != 0) {
			pix = CONDUCTIVE;
		}
		else {
			return DIRT;
		}

		for (int yy = 0; yy < TILE_HEIGHT; yy++)
		{
			for (int xx = 0; xx < TILE_WIDTH; xx++)
			{
				img.setRGB(x*TILE_WIDTH+xx,y*TILE_HEIGHT+yy, pix);
			}
		}
		return -1; //this special value tells caller to skip the tile bitblt,
		           //since it was performed here
	}

	@Override
	public void paintComponent(Graphics gr)
	{
		final int width = engine.getWidth();
		final int height = engine.getHeight();

		BufferedImage img = new BufferedImage(width*TILE_WIDTH, height*TILE_HEIGHT,
				BufferedImage.TYPE_INT_RGB);

		final Insets INSETS = getInsets();
		Rectangle clipRect = gr.getClipBounds();
		int minX = Math.max(0, (clipRect.x - INSETS.left) / TILE_WIDTH);
		int minY = Math.max(0, (clipRect.y - INSETS.top) / TILE_HEIGHT);
		int maxX = Math.min(width, 1 + (clipRect.x - INSETS.left + clipRect.width-1) / TILE_WIDTH);
		int maxY = Math.min(height, 1 + (clipRect.y - INSETS.top + clipRect.height-1) / TILE_HEIGHT);

		for (int y = minY; y < maxY; y++)
		{
			for (int x = minX; x < maxX; x++)
			{
				int tile = engine.getTile(x,y) & LOMASK;
				switch (mapState) {
				case RESIDENTIAL:
					if (tile >= COMBASE) { tile = DIRT; }
					break;
				case COMMERCIAL:
					if (tile > COMLAST || (tile >= RESBASE && tile < COMBASE)) { tile = DIRT; }
					break;
				case INDUSTRIAL:
					if ((tile >= RESBASE && tile < INDBASE) ||
						(tile >= PORTBASE && tile < SMOKEBASE) ||
						(tile >= TINYEXP && tile < 884) ||
						tile >= FOOTBALLGAME1)
						{ tile = DIRT; }
					break;
				case POWER_OVERLAY:
					tile = checkPower(img, x, y, engine.getTile(x,y));
					break;
				case TRANSPORT:
				case TRAFFIC_OVERLAY:
					if (tile >= RESBASE ||
						(tile >= 207 && tile <= LVPOWER10) ||
						tile == 223) { tile = DIRT; }
					break;
				default:
				}

				// tile == -1 means it's already been drawn
				// in the checkPower function

				if (tile != -1) {
				if (tile >= 0 && tile <= LAST_TILE) {
				for (int yy = 0; yy < TILE_HEIGHT; yy++)
				{
					for (int xx = 0; xx < TILE_WIDTH; xx++)
					{
						img.setRGB(x*TILE_WIDTH+xx,y*TILE_HEIGHT+yy,
							tileArrayImage.getRGB(xx,tile*TILE_OFFSET_Y+yy));
					}
				}
				}
				}
			}
		}

		gr.drawImage(img, INSETS.left, INSETS.top, null);

		gr = gr.create();
		gr.translate(INSETS.left, INSETS.top);

		switch (mapState) {
		case POLICE_OVERLAY:
			drawPoliceRadius(gr); break;
		case FIRE_OVERLAY:
			drawFireRadius(gr); break;
		case LANDVALUE_OVERLAY:
			drawLandMap(gr); break;
		case CRIME_OVERLAY:
			drawCrimeMap(gr); break;
		case POLLUTE_OVERLAY:
			drawPollutionMap(gr); break;
		case TRAFFIC_OVERLAY:
			drawTrafficMap(gr); break;
		case GROWTHRATE_OVERLAY:
			drawRateOfGrowth(gr); break;
		case POPDEN_OVERLAY:
			drawPopDensity(gr); break;
		default:
		}

		for (ConnectedView cv : views)
		{
			Rectangle rect = getViewRect(cv);
			gr.setColor(Color.WHITE);
			gr.drawRect(rect.x-2,rect.y-2,rect.width+2,rect.height+2);

			gr.setColor(Color.BLACK);
			gr.drawRect(rect.x-0,rect.y-0,rect.width+2,rect.height+2);

			gr.setColor(Color.YELLOW);
			gr.drawRect(rect.x-1,rect.y-1,rect.width+2,rect.height+2);
		}
	}

	Rectangle getViewRect(ConnectedView cv)
	{
		Rectangle rawRect = cv.scrollPane.getViewport().getViewRect();
		return new Rectangle(
			rawRect.x * 3 / 16, 
			rawRect.y * 3 / 16,
			rawRect.width * 3 / 16,
			rawRect.height * 3 / 16
			);
	}

	private void dragViewTo(Point p)
	{
		if (views.isEmpty())
			return;

		ConnectedView cv = views.get(0);
		Dimension d = cv.scrollPane.getViewport().getExtentSize();
		Dimension mapSize = cv.scrollPane.getViewport().getViewSize();

		Point np = new Point(
				p.x * 16 / 3 - d.width / 2,
				p.y * 16 / 3 - d.height / 2
				);
		np.x = Math.max(0, Math.min(np.x, mapSize.width - d.width));
		np.y = Math.max(0, Math.min(np.y, mapSize.height - d.height));

		cv.scrollPane.getViewport().setViewPosition(np);
	}

	//implements Scrollable
	public Dimension getPreferredScrollableViewportSize()
	{
		return new Dimension(120,120);
	}

	//implements Scrollable
	public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction)
	{
		if (orientation == SwingConstants.VERTICAL)
			return visibleRect.height;
		else
			return visibleRect.width;
	}

	//implements Scrollable
	public boolean getScrollableTracksViewportWidth()
	{
		return false;
	}

	//implements Scrollable
	public boolean getScrollableTracksViewportHeight()
	{
		return false;
	}

	//implements Scrollable
	public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction)
	{
		if (orientation == SwingConstants.VERTICAL)
			return TILE_HEIGHT;
		else
			return TILE_WIDTH;
	}

	//implements MapListener
	public void mapOverlayDataChanged(MapState overlayDataType)
	{
		repaint();
	}

	//implements MapListener
	public void spriteMoved(Sprite sprite)
	{
	}

	//implements MapListener
	public void tileChanged(int xpos, int ypos)
	{
		Rectangle r = new Rectangle(xpos*TILE_WIDTH, ypos * TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT);
		repaint(r);
	}

	//implements MapListener
	public void wholeMapChanged()
	{
		repaint();
		engine.calculateCenterMass();
		dragViewToCityCenter();
	}

	public void dragViewToCityCenter()
	{
		dragViewTo(new Point(TILE_WIDTH * engine.centerMassX + 1,
			TILE_HEIGHT * engine.centerMassY + 1));
	}

	class ConnectedView implements ChangeListener
	{
		JScrollPane scrollPane;

		ConnectedView(MicropolisDrawingArea view, JScrollPane scrollPane)
		{
			this.scrollPane = scrollPane;
			scrollPane.getViewport().addChangeListener(this);
		}

		public void stateChanged(ChangeEvent ev)
		{
			repaint();
		}
	}

	public void connectView(MicropolisDrawingArea view, JScrollPane scrollPane)
	{
		ConnectedView cv = new ConnectedView(view, scrollPane);
		views.add(cv);
		repaint();
	}

	private void onMousePressed(MouseEvent ev)
	{
		if (ev.getButton() == MouseEvent.BUTTON1)
			dragViewTo(ev.getPoint());
	}

	private void onMouseDragged(MouseEvent ev)
	{
		if ((ev.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0)
			return;

		dragViewTo(ev.getPoint());
	}
}
