/*******************************************************************************
 * Copyright (c) MOBAC developers
 * 
 * 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 of the License, 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, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package mobac.program.atlascreators.impl;

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import javax.imageio.ImageIO;

import mobac.exceptions.MapCreationException;
import mobac.program.atlascreators.AtlasCreator;
import mobac.program.atlascreators.tileprovider.TileProvider;
import mobac.program.interfaces.MapInterface;
import mobac.program.interfaces.MapSource;
import mobac.program.interfaces.TileImageDataWriter;
import mobac.program.model.TileImageParameters;
import mobac.utilities.MyMath;

import org.apache.log4j.Logger;

public class MapTileBuilder {

	private static final Logger log = Logger.getLogger(MapTileBuilder.class);

	private final AtlasCreator atlasCreator;
	private final TileProvider mapDlTileProvider;
	private final MapInterface map;
	private final MapSource mapSource;
	private final TileImageParameters parameters;
	private final TileImageDataWriter tileImageDataWriter;
	private final int tileSize;
	private final int xMin;
	private final int xMax;
	private final int yMin;
	private final int yMax;

	private final boolean useRealTileSize;

	private int realWidth;
	private int realHeight;
	int mergedWidth;
	int mergedHeight;

	final int customTileCount;
	final int xStart;
	final int yStart;
	final int xEnd;
	final int yEnd;

	protected final MapTileWriter mapTileWriter;

	/**
	 * @param atlasCreator
	 * @param mapTileWriter
	 * @param useRealTileSize
	 *            affects the tile size at the left and bottom border of the map. If <code>true</code> those tile will
	 *            have the size of the remaining image data available size (they can be smaller than the size
	 *            specified). If <code>false</code> the tile size will be as specified by the map's
	 *            {@link TileImageParameters}.
	 */
	public MapTileBuilder(AtlasCreator atlasCreator, MapTileWriter mapTileWriter, boolean useRealTileSize) {
		this(atlasCreator, atlasCreator.getParameters().getFormat().getDataWriter(), mapTileWriter, useRealTileSize);
	}

	/**
	 * 
	 * @param atlasCreator
	 * @param tileImageDataWriter
	 * @param mapTileWriter
	 * @param useRealTileSize
	 *            affects the tile size at the left and bottom border of the map. If <code>true</code> those tile will
	 *            have the size of the remaining image data available size (they can be smaller than the size
	 *            specified). If <code>false</code> the tile size will be as specified by the map's
	 *            {@link TileImageParameters}.
	 */
	public MapTileBuilder(AtlasCreator atlasCreator, TileImageDataWriter tileImageDataWriter,
			MapTileWriter mapTileWriter, boolean useRealTileSize) {
		this.atlasCreator = atlasCreator;
		this.tileImageDataWriter = tileImageDataWriter;
		this.mapTileWriter = mapTileWriter;
		this.mapDlTileProvider = atlasCreator.getMapDlTileProvider();
		this.useRealTileSize = useRealTileSize;
		map = atlasCreator.getMap();
		mapSource = map.getMapSource();
		tileSize = mapSource.getMapSpace().getTileSize();
		xMax = atlasCreator.getXMax();
		xMin = atlasCreator.getXMin();
		yMax = atlasCreator.getYMax();
		yMin = atlasCreator.getYMin();
		parameters = atlasCreator.getParameters();
		// left upper point on the map in pixels
		// regarding the current zoom level
		xStart = xMin * tileSize;
		yStart = yMin * tileSize;

		// lower right point on the map in pixels
		// regarding the current zoom level
		xEnd = xMax * tileSize + (tileSize - 1);
		yEnd = yMax * tileSize + (tileSize - 1);

		mergedWidth = xEnd - xStart + 1;
		mergedHeight = yEnd - yStart + 1;

		realWidth = parameters.getWidth();
		realHeight = parameters.getHeight();
		if (useRealTileSize) {
			// Reduce tile size of overall map height/width
			// if it is smaller than one tile
			if (realWidth > mergedWidth)
				realWidth = mergedWidth;
			if (realHeight > mergedHeight)
				realHeight = mergedHeight;
		}
		customTileCount = MyMath.divCeil(mergedWidth, realWidth) * MyMath.divCeil(mergedHeight, realHeight);
	}

	public void createTiles() throws MapCreationException, InterruptedException {

		// Absolute positions
		int xAbsPos = xStart;
		int yAbsPos = yStart;

		log.trace("tile size: " + realWidth + " * " + realHeight);
		log.trace("X: from " + xStart + " to " + xEnd);
		log.trace("Y: from " + yStart + " to " + yEnd);

		// We don't work with large images, therefore we can disable the (file)
		// cache of ImageIO. This will speed up the creation process a bit
		ImageIO.setUseCache(false);
		ByteArrayOutputStream buf = new ByteArrayOutputStream(32768);
		tileImageDataWriter.initialize();
		int currentTileHeight = realHeight;
		int currentTileWidth = realWidth;
		try {
			String tileType = tileImageDataWriter.getType().getFileExt();
			int tiley = 0;
			while (yAbsPos < yEnd) {
				int tilex = 0;
				xAbsPos = xStart;
				if (useRealTileSize)
					currentTileHeight = Math.min(realHeight, yEnd - yAbsPos + 1);
				while (xAbsPos < xEnd) {
					if (useRealTileSize)
						currentTileWidth = Math.min(realWidth, xEnd - xAbsPos + 1);
					atlasCreator.checkUserAbort();
					atlasCreator.getAtlasProgress().incMapCreationProgress();
					BufferedImage tileImage = new BufferedImage(currentTileWidth, currentTileHeight,
							BufferedImage.TYPE_3BYTE_BGR);
					buf.reset();
					try {
						Graphics2D graphics = tileImage.createGraphics();
						prepareTile(graphics);
						paintCustomTile(graphics, xAbsPos, yAbsPos);
						graphics.dispose();
						tileImageDataWriter.processImage(tileImage, buf);
						mapTileWriter.writeTile(tilex, tiley, tileType, buf.toByteArray());
					} catch (IOException e) {
						throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e);
					}

					tilex++;
					xAbsPos += realWidth;
				}
				tiley++;
				yAbsPos += realHeight;
			}
		} finally {
			tileImageDataWriter.dispose();
		}
	}

	protected void prepareTile(Graphics2D graphics) {
		graphics.setColor(mapSource.getBackgroundColor());
		graphics.fillRect(0, 0, realWidth, realHeight);
	}

	/**
	 * Paints the graphics of the custom tile specified by the pixel coordinates <code>xAbsPos</code> and
	 * <code>yAbsPos</code> on the currently selected map & layer.
	 * 
	 * @param graphics
	 * @param xAbsPos
	 * @param yAbsPos
	 */
	private void paintCustomTile(Graphics2D graphics, int xAbsPos, int yAbsPos) {
		int xTile = xAbsPos / tileSize;
		int xTileOffset = -(xAbsPos % tileSize);

		for (int x = xTileOffset; x < realWidth; x += tileSize) {
			int yTile = yAbsPos / tileSize;
			int yTileOffset = -(yAbsPos % tileSize);
			for (int y = yTileOffset; y < realHeight; y += tileSize) {
				try {
					BufferedImage orgTileImage = loadOriginalMapTile(xTile, yTile);
					if (orgTileImage != null) {
						//int w = orgTileImage.getWidth();
						//int h = orgTileImage.getHeight();
						graphics.drawImage(orgTileImage, xTileOffset, yTileOffset, tileSize, tileSize, null);
					}
				} catch (Exception e) {
					log.error("Error while painting sub-tile", e);
				}
				yTile++;
				yTileOffset += tileSize;
			}
			xTile++;
			xTileOffset += tileSize;
		}
	}

	public int getCustomTileCount() {
		return customTileCount;
	}

	/**
	 * A simple local cache holding the last 10 loaded original tiles. If the custom tile size is smaller than 256x256
	 * the efficiency of this cache is very high (~ 75% hit rate).
	 */
	private CachedTile[] cache = new CachedTile[10];
	private int cachePos = 0;

	private BufferedImage loadOriginalMapTile(int xTile, int yTile) throws Exception {
		for (CachedTile ct : cache) {
			if (ct == null)
				continue;
			if (ct.xTile == xTile && ct.yTile == yTile) {
				// log.trace("cache hit");
				return ct.image;
			}
		}
		// log.trace("cache miss");
		BufferedImage image = mapDlTileProvider.getTileImage(xTile, yTile);
		if (image == null)
			return null;
		cache[cachePos] = new CachedTile(image, xTile, yTile);
		cachePos = (cachePos + 1) % cache.length;
		return image;
	}

	private static class CachedTile {
		BufferedImage image;
		int xTile;
		int yTile;

		public CachedTile(BufferedImage image, int tile, int tile2) {
			super();
			this.image = image;
			xTile = tile;
			yTile = tile2;
		}
	}
}
