/*******************************************************************************
 * 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;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.RandomAccessFile;

import javax.imageio.ImageIO;

import mobac.exceptions.AtlasTestException;
import mobac.exceptions.MapCreationException;
import mobac.mapsources.mapspace.MercatorPower2MapSpace;
import mobac.program.annotations.AtlasCreatorName;
import mobac.program.annotations.SupportedParameters;
import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider;
import mobac.program.atlascreators.tileprovider.TileProvider;
import mobac.program.interfaces.AtlasInterface;
import mobac.program.interfaces.MapInterface;
import mobac.program.interfaces.MapSource;
import mobac.program.interfaces.MapSpace;
import mobac.program.interfaces.MapSpace.ProjectionCategory;
import mobac.program.interfaces.TileImageDataWriter;
import mobac.program.model.TileImageParameters.Name;
import mobac.utilities.Utilities;
import mobac.utilities.stream.ArrayOutputStream;

/**
 * Creates maps using the MGM pack file format (.mgm).
 * 
 * Each zoom level in a different directory, 64 tiles per mgm file.
 * 
 * <h3>Format</h3>
 * <ul>
 * <li>2 bytes: number of tiles in this file for each tile (even for tiles that are not used, in which case the data is
 * left null)</li>
 * <li>1 byte: tile x within this file; add tileX * tilesPerFileX to get global tile number</li>
 * <li>1 byte: tile y within this file; add tileY * tilesPerFileY to get global tile number</li>
 * <li>4 bytes: offset of the end of the tile data within this file (to get the offset for the start of the tile data,
 * subtract the value for the previous tile, or 2 + 6 * tilesPerFile tile data 1 tile data 2 ...</li>
 * </ul>
 * 
 * @author paour
 */
@AtlasCreatorName(value = "MGMaps/MyTrails (MGM)", type = "MGM")
@SupportedParameters(names = { Name.format })
public class MGMaps extends AtlasCreator {

	private static final int TILES_PER_FILE_X = 8;
	private static final int TILES_PER_FILE_Y = 8;
	private static final int TILES_PER_FILE = TILES_PER_FILE_X * TILES_PER_FILE_Y;

	private double xResizeRatio = 1.0;
	private double yResizeRatio = 1.0;

	@Override
	public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws AtlasTestException, IOException,
			InterruptedException {
		super.startAtlasCreation(atlas, customAtlasDir);

		File cache_conf = new File(atlasDir, "cache.conf");
		PrintWriter pw = new PrintWriter(new FileWriter(cache_conf));
		try {
			pw.println("version=3");
			pw.println("tiles_per_file=" + TILES_PER_FILE);
			pw.println("hash_size=1");
		} finally {
			pw.close();
		}
	}

	@Override
	public void initializeMap(final MapInterface map, final TileProvider mapTileProvider) {
		super.initializeMap(map, mapTileProvider);

		xResizeRatio = 1.0;
		yResizeRatio = 1.0;

		if (parameters != null) {
			int mapTileSize = map.getMapSource().getMapSpace().getTileSize();
			if ((parameters.getWidth() != mapTileSize) || (parameters.getHeight() != mapTileSize)) {
				// handle image re-sampling + image re-sizing
				xResizeRatio = (double) parameters.getWidth() / (double) mapTileSize;
				yResizeRatio = (double) parameters.getHeight() / (double) mapTileSize;
			} else {
				// handle only image re-sampling
				mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat());
			}
		}
	}

	@Override
	public void createMap() throws MapCreationException, InterruptedException {
		MGMTileWriter mgmTileWriter = null;
		try {
			if ((xResizeRatio != 1.0) || (yResizeRatio != 1.0))
				mgmTileWriter = new MGMResizeTileWriter();
			else
				mgmTileWriter = new MGMTileWriter();

			String name = map.getLayer().getName();

			// safe naming: replace all non-word characters: [^a-zA-Z_0-9]
			name = name.replaceAll("[^a-zA-Z_0-9]", "_");

			// crate directory if necessary
			File folder = new File(atlasDir, name + "_" + map.getZoom());
			Utilities.mkDirs(folder);

			atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1));

			ImageIO.setUseCache(false);

			int pxMin = xMin / TILES_PER_FILE_X;
			int pxMax = xMax / TILES_PER_FILE_X;
			int pyMin = yMin / TILES_PER_FILE_Y;
			int pyMax = yMax / TILES_PER_FILE_Y;

			for (int px = pxMin; px <= pxMax; px++) {
				for (int py = pyMin; py <= pyMax; py++) {
					int count = 0;
					int pos = 2 + TILES_PER_FILE * 6;
					File pack = new File(folder, px + "_" + py + ".mgm");
					RandomAccessFile raf = null;

					try {
						for (int i = 0; i < TILES_PER_FILE_X; i++) {
							int x = px * TILES_PER_FILE_X + i;
							if (x < xMin || x > xMax) {
								continue;
							}

							for (int j = 0; j < TILES_PER_FILE_Y; j++) {
								int y = py * TILES_PER_FILE_Y + j;
								if (y < yMin || y > yMax) {
									continue;
								}

								if (raf == null)
									// Only create a file when needed
									raf = new RandomAccessFile(pack, "rw");

								checkUserAbort();
								atlasProgress.incMapCreationProgress();
								int res = mgmTileWriter.writeTile(x, y, i, j, raf, pos, count);
								if (res >= 0) {
									pos = res;
									count++;
								}
							}
						}

						if (raf != null) {
							// POSITION 0: number of tiles
							raf.seek(0);
							raf.writeChar(count);
						}
					} finally {
						Utilities.closeFile(raf);
					}
					if (count == 0) {
						// the file doesn't contain any tiles
						if (pack.exists())
							Utilities.deleteFile(pack);
					}
				}
			}

		} catch (Exception e) {
			throw new MapCreationException(map, e);
		} finally {
			if (mgmTileWriter != null)
				mgmTileWriter.dispose();
		}
	}

	@Override
	public boolean testMapSource(final MapSource mapSource) {
		MapSpace mapSpace = mapSource.getMapSpace();
		return (mapSpace instanceof MercatorPower2MapSpace)
				&& (ProjectionCategory.SPHERE.equals(mapSource.getMapSpace().getProjectionCategory()) || ProjectionCategory.ELLIPSOID
						.equals(mapSource.getMapSpace().getProjectionCategory()));
	}

	/**
	 * Simply writes the tile to the file without resizing
	 */
	private class MGMTileWriter {

		protected byte[] getSourceTileData(int x, int y) throws IOException {
			return mapDlTileProvider.getTileData(x, y);
		}

		public int writeTile(int x, int y, int i, int j, RandomAccessFile raf, int startPos, int count)
				throws MapCreationException {
			try {
				byte[] sourceTileData = getSourceTileData(x, y);
				if (sourceTileData == null)
					return -1;
				raf.seek(startPos);
				raf.write(sourceTileData);

				// write the tile index
				raf.seek(2 + count * 6);
				raf.writeByte(i);
				raf.writeByte(j);
				int pos = startPos + sourceTileData.length;
				raf.writeInt(pos);
				return pos;
			} catch (IOException e) {
				throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e);
			}
		}

		public void dispose() {
			// Nothing to do
		}
	}

	/**
	 * Resizes the tile and saves it to the file
	 */
	private class MGMResizeTileWriter extends MGMTileWriter {

		private final BufferedImage tileImage;
		private final Graphics2D graphics;
		private final TileImageDataWriter writer;
		private final ArrayOutputStream buffer;

		public MGMResizeTileWriter() {
			// resize image
			tileImage = new BufferedImage(parameters.getWidth(), parameters.getHeight(), BufferedImage.TYPE_3BYTE_BGR);

			// associated graphics with affine transform
			graphics = tileImage.createGraphics();
			graphics.setTransform(AffineTransform.getScaleInstance(xResizeRatio, yResizeRatio));
			graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

			// image compression writer
			writer = parameters.getFormat().getDataWriter();

			// buffer to store compressed image
			buffer = new ArrayOutputStream(3 * parameters.getWidth() * parameters.getHeight());
		}

		@Override
		protected byte[] getSourceTileData(int x, int y) throws IOException {
			// need to resize the tile
			final BufferedImage tile = mapDlTileProvider.getTileImage(x, y);
			graphics.drawImage(tile, 0, 0, null);
			buffer.reset();
			writer.initialize();
			writer.processImage(tileImage, buffer);

			byte[] processedTileData = buffer.toByteArray();
			buffer.reset();
			return processedTileData;
		}

		@Override
		public void dispose() {
			buffer.reset();
			graphics.dispose();
		}

	}
}
