/*******************************************************************************
 * 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.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Date;

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.interfaces.AtlasInterface;
import mobac.program.interfaces.MapSource;
import mobac.program.interfaces.RequiresSQLite;
import mobac.program.model.Settings;
import mobac.program.model.TileImageParameters.Name;
import mobac.utilities.Utilities;
import mobac.utilities.jdbc.SQLiteLoader;

@AtlasCreatorName("NaviComputer (NMAP)")
@SupportedParameters(names = { Name.format })
public class NaviComputer extends AtlasCreator implements RequiresSQLite {

	private static final String NAVI_TABLES = "CREATE TABLE  MapInfo (MapType TEXT, Zoom INTEGER NOT NULL, MinX INTEGER, MaxX INTEGER, MinY INTEGER, MaxY INTEGER);\n"
			+ "CREATE TABLE  Tiles (id INTEGER NOT NULL PRIMARY KEY, X INTEGER NOT NULL, Y INTEGER NOT NULL, Zoom INTEGER NOT NULL);\n"
			+ "CREATE TABLE  TilesData (id INTEGER NOT NULL PRIMARY KEY CONSTRAINT fk_Tiles_id REFERENCES Tiles(id) ON DELETE CASCADE, Tile BLOB NULL);\n"

			+ "CREATE TRIGGER fkdc_TilesData_id_Tiles_id " + "BEFORE DELETE ON Tiles " + "FOR EACH ROW BEGIN "
			+ "DELETE FROM TilesData WHERE TilesData.id = OLD.id; " + "END;\n"
			+ "CREATE TRIGGER fki_TilesData_id_Tiles_id " + "BEFORE INSERT ON [TilesData] " + "FOR EACH ROW BEGIN "
			+ "SELECT RAISE(ROLLBACK, 'insert on table TilesData violates foreign key constraint fki_TilesData_id_Tiles_id') "
			+ "WHERE (SELECT id FROM Tiles WHERE id = NEW.id) IS NULL; " + "END;\n"
			+ "CREATE TRIGGER fku_TilesData_id_Tiles_id " + "BEFORE UPDATE ON [TilesData] " + "FOR EACH ROW BEGIN "
			+ "SELECT RAISE(ROLLBACK, 'update on table TilesData violates foreign key constraint fku_TilesData_id_Tiles_id') "
			+ "WHERE (SELECT id FROM Tiles WHERE id = NEW.id) IS NULL; " + "END;\n"
			+ "CREATE INDEX IndexOfTiles ON Tiles (X, Y, Zoom);";

	private static final String INSERT_TILES = "INSERT INTO Tiles (id,X,Y,Zoom) VALUES (?,?,?,?)";
	private static final String INSERT_TILES_DATA = "INSERT  INTO TilesData (id,Tile) VALUES (?,?)";
	private static final String INSERT_MAP_INFO = "INSERT INTO MapInfo (MapType,Zoom,MinX,MaxX,MinY,MaxY) "
			+ "SELECT ?,Min(Zoom),Min(x),Max(x),Min(y),Max(y) FROM Tiles WHERE Zoom=?;";

	private File databaseFile;
	private int wmsTileCount = 1;

	private static final int COMMIT_RATE = 100;
	private int tileCommitCounter = 0;
	protected Connection conn = null;
	private PreparedStatement prepTilesData = null, prepTiles = null;

	@Override
	public boolean testMapSource(MapSource mapSource) {
		return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace());
	}

	@Override
	protected void testAtlas() throws AtlasTestException {
		performTest_MaxMapZoom(18);
	}

	@Override
	public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir)
			throws IOException, AtlasTestException, InterruptedException {
		if (customAtlasDir == null)
			customAtlasDir = Settings.getInstance().getAtlasOutputDirectory();
		super.startAtlasCreation(atlas, customAtlasDir);

		databaseFile = getDatabaseFileName();
		log.debug("SQLite Database file: " + databaseFile);

		try {
			SQLiteLoader.loadSQLite();
		} catch (SQLException e) {
			throw new IOException(SQLiteLoader.getMsgSqliteMissing(), e);
		}
		try {
			Utilities.mkDir(atlasDir);
			openConnection();
			initializeDB();

			prepTilesData = conn.prepareStatement(INSERT_TILES_DATA);
			prepTiles = conn.prepareStatement(INSERT_TILES);

		} catch (SQLException e) {
			throw new AtlasTestException("Error creating SQL database \"" + databaseFile + "\": " + e.getMessage(), e);
		}

	}

	@Override
	public void createMap() throws MapCreationException, InterruptedException {
		if (parameters != null)
			mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat());

		createTiles();

	}

	private void openConnection() throws SQLException, IOException {
		if (conn == null || conn.isClosed()) {
			String url = "jdbc:sqlite:" + databaseFile.getCanonicalPath();
			conn = DriverManager.getConnection(url);
		}
	}

	@Override
	public void abortAtlasCreation() throws IOException {
		SQLiteLoader.closeConnection(conn);
		conn = null;
		super.abortAtlasCreation();
	}

	@Override
	public void finishAtlasCreation() throws IOException, InterruptedException {
		String mapName = mapSource.getName();
		try {
			PreparedStatement prepStat = conn.prepareStatement(INSERT_MAP_INFO);
			Statement stat = conn.createStatement();
			ResultSet rs = stat.executeQuery("SELECT Distinct Zoom From Tiles");
			while (rs.next()) {
				int zoom = rs.getInt(1);
				prepStat.setString(1, mapName);
				prepStat.setInt(2, zoom);
				prepStat.execute();
			}
			conn.commit();
			prepStat.close();
		} catch (SQLException e) {
			log.error(e.getMessage());
		} finally {
			SQLiteLoader.closeConnection(conn);
			conn = null;
		}
		super.finishAtlasCreation();
	}

	protected void initializeDB() throws SQLException {
		Statement stat = conn.createStatement();
		String[] sqlList = NAVI_TABLES.split("\\n");
		for (String sql : sqlList)
			stat.addBatch(sql);
		stat.executeBatch();
		stat.close();
		log.debug("Database initialization complete: tables, trigges and index created");
	}

	protected void createTiles() throws InterruptedException, MapCreationException {
		int maxMapProgress = (xMax - xMin + 1) * (yMax - yMin + 1);
		atlasProgress.initMapCreation(maxMapProgress);
		try {
			tileCommitCounter = 0;
			conn.setAutoCommit(false);
			Runtime r = Runtime.getRuntime();

			for (int x = xMin; x <= xMax; x++) {
				for (int y = yMin; y <= yMax; y++) {
					checkUserAbort();
					atlasProgress.incMapCreationProgress();
					try {

						byte[] sourceTileData = mapDlTileProvider.getTileData(x, y);
						if (sourceTileData != null) {
							tileCommitCounter++;
							writeTile(wmsTileCount++, x, y, zoom, sourceTileData);

							long heapAvailable = r.maxMemory() - r.totalMemory() + r.freeMemory();
							if (heapAvailable < HEAP_MIN || tileCommitCounter > COMMIT_RATE) {
								conn.commit();
								tileCommitCounter = 0;
								System.gc();
							}
						}
					} catch (IOException e) {
						throw new MapCreationException(map, e);
					}
				}
			}
			conn.commit();
			atlasProgress.setMapCreationProgress(maxMapProgress);
		} catch (SQLException e) {
			throw new MapCreationException(map, e);
		}
	}

	protected void writeTile(int id, int x, int y, int z, byte[] tileData) throws SQLException, IOException {
		prepTiles.setInt(1, id);
		prepTiles.setInt(2, x);
		prepTiles.setInt(3, y);
		prepTiles.setInt(4, z);
		prepTiles.execute();
		prepTilesData.setInt(1, id);
		prepTilesData.setBytes(2, tileData);
		prepTilesData.execute();
	}

	protected File getDatabaseFileName() {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss_");
		return new File(atlasDir, sdf.format(new Date()) + atlas.getName() + ".nmap");
	}

}
