/*
 * Copyright (C) 2014.
 *
 * 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.reader.osm;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.GpxCreator;
import uk.me.parabola.util.QuadTree;

/**
 * A relation used by the sea generation code.
 * 
 * @author WanMil
 */
public class SeaPolygonRelation extends MultiPolygonRelation {
	private static final Logger log = Logger.getLogger(SeaPolygonRelation.class);

	private final QuadTree landCoords;
	private final QuadTree seaCoords;

	private boolean floodBlocker = true;
	private int floodBlockerGap = 40;
	private double floodBlockerRatio = 0.5d;
	private int floodBlockerThreshold = 20;
	private boolean debug;
	private final DecimalFormat format = new DecimalFormat("0.0000");
	private Rule floodBlockerRules;
	
	private final String[] landTag = { "natural", "land" };

	public SeaPolygonRelation(Relation other, Map<Long, Way> wayMap, Area bbox) {
		super(other, wayMap, bbox);
		this.landCoords = new QuadTree(bbox);
		this.seaCoords = new QuadTree(bbox);
		// set a special type because this is not the OSM common multipolygon
		// relation
		addTag("type", "mkgmap:seapolygon");
	}

	@Override
	protected boolean needsAreaSizeTag() {
		return false;
	}
	
	@Override
	protected void postProcessing() {
		if (isFloodBlocker()) {
			removeFloodedAreas();
		}
		super.postProcessing();
	}
	
	private void fillQuadTrees() {
		final AtomicBoolean isLand = new AtomicBoolean(false);
		final AtomicBoolean isSea = new AtomicBoolean(false);
		TypeResult fakedType = (el, type) -> {
			if (log.isDebugEnabled())
				log.debug(el.getId(), type);
			if (type.getType() == 0x01) {
				isLand.set(true);
			} else if (type.getType() == 0x02) {
				isSea.set(true);
			}
		};
		for (Way way : getTileWayMap().values()) {
			if (log.isDebugEnabled())
				log.debug("Check usage of way for floodblocker:", way.getId(), way.toTagString());
			floodBlockerRules.resolveType(way, fakedType);

			if (isLand.get()) {
				// save these coords to check if some sea polygons floods
				// the land
				log.debug("Way", way.getId(), "identified as land");
				landCoords.addAll(way.getPoints());
				isLand.set(false);

			} else if (isSea.get()) {
				// save these coords to check if some sea polygons floods the
				// land
				log.debug("Way", way.getId(), "identified as sea");
				seaCoords.addAll(way.getPoints());
				isSea.set(false);
			}
		}
	}

	private void removeFloodedAreas() {
		fillQuadTrees();

		// create a copy of all resulting ways - the tile way map contains only
		// polygons from
		// the sea generation
		ArrayList<Way> polygons = new ArrayList<>(getMpPolygons().values());

		log.info("Starting flood blocker. Polygons to check:", getMpPolygons().size());

		String baseName = GpxCreator.getGpxBaseName();
		if (debug) {
			GpxCreator.createAreaGpx(baseName + "bbox", getTileBounds());
		}

		// go through all polygons and check if it contains too many coords of
		// the other type
		for (Way p : polygons) {
			boolean sea = "sea".equals(p.getTag("natural"));

			QuadTree goodCoords = (sea ? seaCoords : landCoords);
			QuadTree badCoords = (sea ? landCoords : seaCoords);
			String polyType = (sea ? "sea" : "land");
			String otherType = (sea ? "land" : "sea");
			
			List<Coord> minusCoords = badCoords.get(p.getPoints(), getFloodBlockerGap());
			List<Coord> positiveCoords = goodCoords.get(p.getPoints());
			
			log.info(polyType,"polygon", p.getId(), "contains",
					minusCoords.size(), otherType,"coords and",
					positiveCoords.size(), polyType,"coords.");	

			if (!minusCoords.isEmpty()) {
				double area = MultiPolygonRelation.calcAreaSize(p.getPoints());
				double ratio = ((minusCoords.size() - positiveCoords.size()) * 100000.0d / area);
				String areaFMT = format.format(area);
				String ratioFMT = format.format(ratio);
				log.info("Flood blocker for", polyType, "polygon", p.getId());
				log.info("area",areaFMT);
				log.info(polyType, positiveCoords.size());
				log.info(otherType, minusCoords.size());
				log.info("ratio", ratioFMT);
				if (debug) {
					GpxCreator.createGpx(
							baseName + p.getId() + "_"+polyType+"_"
									+ minusCoords.size() + "_"
									+ positiveCoords.size() + "_" + ratioFMT,
							p.getPoints());
					GpxCreator.createGpx(
							baseName + p.getId() + "_con_"
									+ minusCoords.size() + "_"
									+ positiveCoords.size() + "_" + ratioFMT,
									null, minusCoords);

					if (!positiveCoords.isEmpty()) {
						GpxCreator.createGpx(
								baseName + p.getId() + "_pro_"
										+ minusCoords.size() + "_"
										+ positiveCoords.size() + "_"
										+ ratioFMT, null,
								positiveCoords);
					}
				}

				if (minusCoords.size() - positiveCoords.size() >= getFloodBlockerThreshold()
						&& ratio > getFloodBlockerRatio()) {
					log.warn("Polygon", p.getId(), "type",polyType,"seems to be wrong. Changing it to",otherType);
					if (sea) {
						p.deleteTag("natural");
						p.addTag(landTag[0], landTag[1]);
					} else {
						p.deleteTag(landTag[0]);
						p.addTag("natural", "sea");
					}
				} else {
					log.info("Polygon",p.getId(), "is not blocked");
				}

			}
		}
		log.info("Flood blocker finished. Resulting polygons:", getMpPolygons()
				.size());
		
		landCoords.clear();
		seaCoords.clear();
	}

	public boolean isFloodBlocker() {
		return floodBlocker;
	}

	public void setFloodBlocker(boolean floodBlocker) {
		this.floodBlocker = floodBlocker;
	}

	public int getFloodBlockerGap() {
		return floodBlockerGap;
	}

	public void setFloodBlockerGap(int floodBlockerGap) {
		this.floodBlockerGap = floodBlockerGap;
	}

	public double getFloodBlockerRatio() {
		return floodBlockerRatio;
	}

	public void setFloodBlockerRatio(double floodBlockerRatio) {
		this.floodBlockerRatio = floodBlockerRatio;
	}

	public int getFloodBlockerThreshold() {
		return floodBlockerThreshold;
	}

	public void setFloodBlockerThreshold(int floodBlockerThreshold) {
		this.floodBlockerThreshold = floodBlockerThreshold;
	}

	public void setDebug(boolean debug) {
		this.debug = debug;
	}

	public Rule getFloodBlockerRules() {
		return floodBlockerRules;
	}

	public void setFloodBlockerRules(Rule floodBlockerRules) {
		this.floodBlockerRules = floodBlockerRules;
	}

	public void setLandTag(String landTag, String landValue) {
		this.landTag[0] = landTag;
		this.landTag[1] = landValue;
	}

}
