/*
 * Copyright (C) 2010.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * 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.
 */
package uk.me.parabola.mkgmap.combiners;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.FileExistsException;
import uk.me.parabola.imgfmt.FileNotWritableException;
import uk.me.parabola.imgfmt.FileSystemParam;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.MapTooBigException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.map.Map;
import uk.me.parabola.imgfmt.app.map.MapReader;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.imgfmt.app.trergn.Point;
import uk.me.parabola.imgfmt.app.trergn.Polygon;
import uk.me.parabola.imgfmt.app.trergn.Polyline;
import uk.me.parabola.imgfmt.app.trergn.Zoom;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.CommandArgs;
import uk.me.parabola.mkgmap.build.MapBuilder;
import uk.me.parabola.mkgmap.build.MapSplitter;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.MapLine;
import uk.me.parabola.mkgmap.general.MapPoint;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.reader.overview.OverviewMapDataSource;
import uk.me.parabola.mkgmap.srt.SrtTextReader;
import uk.me.parabola.util.EnhancedProperties;

/**
 * Build the overview map.  This is a low resolution map that covers the whole
 * of a map set.  It also contains polygons that correspond to the areas
 * covered by the individual map tiles.
 *
 * @author Steve Ratcliffe
 */
public class OverviewBuilder implements Combiner {
	Logger log = Logger.getLogger(OverviewBuilder.class);
	public static final String OVERVIEW_PREFIX = "ovm_";
	private OverviewMapDataSource overviewSource;
	private String areaName;
	private String overviewMapname;
	private String overviewMapnumber;
	private String outputDir;		
	private Integer codepage;
	private Integer encodingType;
	private List<String[]> copyrightMsgs = new ArrayList<>();
	private List<String[]> licenseInfos = new ArrayList<>();
	private LevelInfo[] wantedLevels;
	private Area bounds;
	private boolean hasBackground;
	private EnhancedProperties overviewProps = new EnhancedProperties();
	private int maxRes = 16; // we can write a 0x4a polygon for planet in res 16.

	public OverviewBuilder() {
		this.overviewSource = new OverviewMapDataSource();
	}

	public void init(CommandArgs args) {
		areaName = args.get("area-name", "Overview Map");
		overviewMapname = args.get("overview-mapname", "osmmap");
		overviewMapnumber = args.get("overview-mapnumber", "63240000");
		
		outputDir = args.getOutputDir();
		overviewProps = new EnhancedProperties(args.getProperties());
	}

	public void onMapEnd(FileInfo finfo) {
		if (!finfo.isImg())
			return;

		try {
			readFileIntoOverview(finfo);
		} catch (FileNotFoundException e) {
			throw new MapFailedException("Could not read detail map " + finfo.getFilename(), e);
		}
	}

	public void onFinish() {
		if (!hasBackground) {
			List<MapShape> shapes = overviewSource.getShapes();
			int inx = shapes.size();
			overviewSource.addBackground();
			// for --order-by-decreasing-area, need the background to be first:
			if (shapes.size() > inx) // something was added
				shapes.add(0, shapes.remove(inx));
		}
		calcLevels();
		writeOverviewMap();
		bounds = overviewSource.getBounds();
		overviewSource = null; // release memory
	}

	@Override
	public String getFilename() {
		return Utils.joinPath(outputDir, overviewMapname, "img");
	}

	private void calcLevels() {
		if (wantedLevels == null)
			setRes(maxRes);
		else {
			// make sure that the wanted levels for the overview map
			// can store the largest 0x4a polygon at level 0
			int n = wantedLevels.length-1;
			while (n > 0 && wantedLevels[n].getBits() > maxRes)
				n--;
			if (n > 0){
				int l = 0;
				while (n >= 0){
					wantedLevels[n] = new LevelInfo(l++, wantedLevels[n].getBits());
					n--;
				}
				wantedLevels = Arrays.copyOfRange(wantedLevels, 0, l);
				overviewSource.setMapLevels(wantedLevels);
			} else {
				setRes(maxRes);
			}
		}
	}

 	/**
	 * Adjust {@code maxRes} value.
	 * @param detailTileBounds tile bounds (of ovm_ file)
	 * @param tileName tile name
	 */
	private int checkFixRes(Area detailTileBounds, String tileName) {
		int newMaxRes = maxRes;
		int maxSize = MapSplitter.MAX_DIVISION_SIZE << (24 - newMaxRes);
		int maxDimPoly = detailTileBounds.getMaxDimension();
		if (maxDimPoly > maxSize) {
			int oldMaxRes = newMaxRes;
			while (maxDimPoly > maxSize) {
				newMaxRes--;
				maxSize = 0xffff << (24 - newMaxRes);
			}
			final String msg = "Tile selection (0x4a) polygon for";
			final String msg2;
			if (tileName != null)
				msg2 = "tile " + tileName;
			else
				msg2 = detailTileBounds.toString();
			log.error(msg, msg2, "cannot be written in level 0 resolution", oldMaxRes + ", using", newMaxRes, "instead");
		}
		return newMaxRes;
	}

	/**
	 * Write out the overview map.
	 */
	private void writeOverviewMap() {
		if (overviewSource.mapLevels() == null)
			return;
		
		MapBuilder mb = new MapBuilder(false, true);
		mb.setEnableLineCleanFilters(false);

		FileSystemParam params = new FileSystemParam();
		params.setMapDescription(areaName);
		mb.setCopyrights(creMsgList(copyrightMsgs));
		mb.setMapInfo(creMsgList(licenseInfos));
		
		
		try {
			if (codepage == null){
				codepage = 0; // should not happen
			}
			Sort sort = SrtTextReader.sortForCodepage(codepage);
			Map map = Map.createMap(overviewMapname, outputDir, params, overviewMapnumber, sort, true);
			map.config(overviewProps);
			mb.config(overviewProps);

			if (encodingType != null){
				map.getLblFile().setEncoder(encodingType, codepage);
			}
			mb.makeMap(map, overviewSource);
			map.close();
		} catch (FileExistsException e) {
			throw new ExitException("Could not create overview map", e);
		} catch (FileNotWritableException e) {
			throw new ExitException("Could not write to overview map", e);
		} catch (MapTooBigException e) {
			throw new MapTooBigException(e.getMaxAllowedSize(),
					"The overview map is too big.",
					"Try reducing the highest overview resolution or reducing the amount of information included in the overview.");
		}
	}

	/**
	 * Add an individual .img file to the overview map.
	 *
	 * @param finfo Information about an individual map.
	 */
	private void readFileIntoOverview(FileInfo finfo) throws FileNotFoundException {
		MapReader mapReader = null;
		String filename = finfo.getFilename();
		if (codepage == null){
			codepage = finfo.getCodePage();
		} 
		if (codepage != finfo.getCodePage()){
			Logger.defaultLogger.warn("Input file " + filename + " has different code page " + finfo.getCodePage());
		}

		try {
			mapReader = new MapReader(filename);

			if (encodingType == null){
				encodingType = mapReader.getEncodingType();
			} 
			if (encodingType != mapReader.getEncodingType()){
				Logger.defaultLogger.warn("Input file " + filename + " has different charset type " + encodingType);
			}

			String[] msgs = mapReader.getCopyrights();
			boolean found = false;
			for (String[] block : copyrightMsgs) {
				if (Arrays.deepEquals(block, msgs)){
					found = true;
					break;
				}
			}
			if (!found )
				copyrightMsgs.add(msgs);
			
			msgs = finfo.getLicenseInfo();
			found = false;
			for (String[] block : licenseInfos) {
				if (Arrays.deepEquals(block, msgs)){
					found = true;
					break;
				}
			}
			if (!found )
				licenseInfos.add(msgs);
			
			
			Zoom[] levels = mapReader.getLevels();
			if (wantedLevels == null){
				LevelInfo[] mapLevels;
				if (isOverviewImg(filename)){
					mapLevels = new LevelInfo[levels.length-1]; 
					for (int i = 1; i < levels.length; i++){
						mapLevels[i-1] = new LevelInfo(levels[i].getLevel(), levels[i].getResolution());
					}
				} else {
					mapLevels = new LevelInfo[1];
					mapLevels[0] = new LevelInfo(levels[1].getLevel(), levels[1].getResolution());
				}
				wantedLevels = mapLevels;
				maxRes = wantedLevels[wantedLevels.length-1].getBits();
			}
			addMapCoverageArea(finfo);
			if (isOverviewImg(filename)){
				readPoints(mapReader);
				readLines(mapReader);
				readShapes(mapReader);
			}
		} catch (FileNotFoundException e) {
			throw new ExitException("Could not open " + filename + " when creating overview file");
		} finally {
			Utils.closeFile(mapReader);
		}
	}

	/**
	 * Read the points from the .img file and add them to the overview map.
	 *
	 * @param mapReader Map reader on the detailed .img file.
	 */
	private void readPoints(MapReader mapReader) {
		Area sourceBounds = overviewSource.getBounds();
		Zoom[] levels = mapReader.getLevels();
		for (int l = 1; l < levels.length; l++){
			int min = levels[l].getLevel();
			int res = levels[l].getResolution();
			List<Point> pointList = mapReader.pointsForLevel(min, MapReader.WITH_EXT_TYPE_DATA);
			for (Point point: pointList) {
				if (log.isDebugEnabled())
					log.debug("got point", point);
				if (!sourceBounds.contains(point.getLocation())){
					if (log.isDebugEnabled())
						log.debug(point, "dropped, is outside of tile boundary");
					continue;
				}
					
				MapPoint mp = new MapPoint();
				mp.setType(point.getType());
				if (point.getLabel() != null) {
					mp.setName(point.getLabel().getText());
				}
				mp.setMaxResolution(res); 
				mp.setMinResolution(res);  
				mp.setLocation(point.getLocation());
				overviewSource.addPoint(mp);
			}
		}
	}

	/**
	 * Read the lines from the .img file and add them to the overview map.
	 *
	 * @param mapReader Map reader on the detailed .img file.
	 */
	private void readLines(MapReader mapReader) {
		Zoom[] levels = mapReader.getLevels();
		for (int l = 1; l < levels.length; l++){
			int min = levels[l].getLevel();
			int res = levels[l].getResolution();
			List<Polyline> lineList = mapReader.linesForLevel(min);
			for (Polyline line : lineList) {
				if (log.isDebugEnabled())
					log.debug("got line", line);
				MapLine ml = new MapLine();

				List<Coord> points = line.getPoints();
				if (log.isDebugEnabled())			
					log.debug("line point list", points);
				if (points.size() < 2)
					continue;

				ml.setType(line.getType());
				if ((line.getType() & 0x40) != 0)
					ml.setDirection(true);
				if (line.getLabel() != null)
					ml.setName(line.getLabel().getText());
				ml.setMaxResolution(res); 
				ml.setMinResolution(res);  
				ml.setPoints(points);

				overviewSource.addLine(ml);
			}
		}
	}

	/**
	 * Read the polygons from the .img file and add them to the overview map.
	 *
	 * @param mapReader Map reader on the detailed .img file.
	 */
	private void readShapes(MapReader mapReader) {
		Zoom[] levels = mapReader.getLevels();
		for (int l = 1; l < levels.length; l++){
			int min = levels[l].getLevel();
			int res = levels[l].getResolution();
			List<Polygon> list = mapReader.shapesForLevel(min, MapReader.WITH_EXT_TYPE_DATA);
			for (Polygon shape : list) {
				if (log.isDebugEnabled())
					log.debug("got polygon", shape);
				if (shape.getType() == 0x4b){
					hasBackground = true;
				}
				MapShape ms = new MapShape();

				List<Coord> points = shape.getPoints();
				if (log.isDebugEnabled())			
					log.debug("polygon point list", points);

				if (points.size() < 3)
					continue;

				ms.setType(shape.getType());
				if (shape.getLabel() != null)
					ms.setName(shape.getLabel().getText());
				ms.setMaxResolution(res); 
				ms.setMinResolution(res);  
				ms.setPoints(points);

				overviewSource.addShape(ms);
			}
		}
	}

	/**
	 * Add an area that shows the area covered by a detailed map.  This can
	 * be an arbitary shape, although at the current time we only support
	 * rectangles.
	 * Also build up full overview path (not ness. rectangle) for DEM
	 * and check/decrease resolution if necessary
	 *
	 * @param finfo Information about a detail map.
	 */
	private void addMapCoverageArea(FileInfo finfo) {
		Area bounds = finfo.getBounds();
		List<Coord> points = bounds.toCoords();
		points.forEach(overviewSource::addToBounds);
		overviewSource.addToTileAreaPath(points);
		maxRes = checkFixRes(bounds, finfo.getMapname());
		// Create the tile coverage rectangle
		MapShape bg = new MapShape();
		bg.setType(0x4a);
		bg.setPoints(points);
		bg.setMinResolution(0);
		bg.setName(finfo.getDescription() + '\u001d' + finfo.getMapname());
		
		overviewSource.addShape(bg); 
	}

	public Area getBounds() {
		if (bounds != null)
			return bounds;
		if (overviewSource != null)
			return overviewSource.getBounds();
		return new Area(1, 1, -1, -1); // return invalid bbox
	}

	/**
	 * Check if the the file name points to a partly overview img file  
	 * @param name full path or just a name 
	 * @return true if the name points to a partly overview img file
	 */
	public static boolean isOverviewImg (String name){
		return new File(name).getName().startsWith(OVERVIEW_PREFIX);
	}
	/**
	 * Add the prefix to the file name.
	 * @param name filename 
	 * @return filename of the corresponding overview img file (without a path)
	 */
	public static String getOverviewImgName (String name){
		File f = new File(name);
		return OverviewBuilder.OVERVIEW_PREFIX + f.getName();
	}

	public static String getMapName(String name) {
		String fname = new File(name).getName();
		if (fname.startsWith(OVERVIEW_PREFIX))
			return fname.substring(OVERVIEW_PREFIX.length());
		return name;
	}
	
	private static List<String> creMsgList(List<String[]> msgs){
		ArrayList< String> list = new ArrayList<>();
		for (int i = 0; i < msgs.size(); i++){
			String[] block = msgs.get(i);
			list.addAll(Arrays.asList(block));
			if (i < msgs.size() - 1) {
				// separate blocks
				list.add("");
			}
		}
		return list;
	}

	/**
	 * Set the highest resolution
	 * @param resolution
	 */
	private void setRes(int resolution) {
		LevelInfo[] mapLevels = new LevelInfo[1];
		mapLevels[0] = new LevelInfo(0, resolution);
		overviewSource.setMapLevels(mapLevels); 
	}

}
