/*-
 *  Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
 * 
 *  This file is part of CityDoctor2.
 *
 *  CityDoctor2 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  CityDoctor2 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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with CityDoctor2.  If not, see <https://www.gnu.org/licenses/>.
 */
package de.hft.stuttgart.citydoctor2.datastructure;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.core.CityModel;

import de.hft.stuttgart.citydoctor2.check.AbstractCheck;
import de.hft.stuttgart.citydoctor2.check.Check;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.writer.CityGMLWriterUtils;
import de.hft.stuttgart.quality.model.types.ValidationPlan;

/**
 * The complete CityGML model containing all features that are used in
 * CityDoctor
 * 
 * @author Matthias Betz
 *
 */
public class CityDoctorModel {

	private static final String COULD_NOT_FIND_FEATURE = "Could not find feature: ";
	private List<Building> buildings;
	private List<Vegetation> vegetation;
	private List<BridgeObject> bridges;
	private List<CityObject> land;
	private List<TransportationObject> roads;
	private List<WaterObject> water;
	private CityModel cModel;
	private ParserConfiguration config;
	private String fileName;
	private File file;
	private List<CheckError> globalErrors;
	private boolean isValidated = false;
	private ValidationPlan plan;
	private CityGMLVersion cityGMLVersion;

	public CityDoctorModel(ParserConfiguration config, File file) {
		if (config == null) {
			throw new IllegalArgumentException("Parser configuration may not be null");
		}
		this.fileName = file.getName();
		this.config = config;
		this.file = file;
		buildings = new ArrayList<>();
		vegetation = new ArrayList<>();
		bridges = new ArrayList<>();
		land = new ArrayList<>();
		roads = new ArrayList<>();
		water = new ArrayList<>();
		globalErrors = new ArrayList<>();
	}

	public boolean isValidated() {
		return isValidated;
	}

	public void setValidated(ValidationPlan plan) {
		this.plan = plan;
		this.isValidated = true;
	}
	
	public ValidationPlan getValidationPlan() {
		return plan;
	}

	public void addGlobalError(CheckError err) {
		globalErrors.add(err);
	}

	public void addGlobalErrors(List<? extends CheckError> generalErrors) {
		globalErrors.addAll(generalErrors);
	}

	public List<CheckError> getGlobalErrors() {
		return globalErrors;
	}

	public File getFile() {
		return file;
	}

	public Stream<CityObject> createFeatureStream() {
		return Stream.of(buildings.stream(), vegetation.stream(), bridges.stream(), land.stream(), roads.stream(),
				water.stream()).flatMap(co -> co);
	}

	public void saveAs(String file) throws CityDoctorWriteException {
		if (file.endsWith(".off")) {
			exportAsOff(file);
		} else {
			exportAsGML(file);
		}
	}

	private void exportAsGML(String file) throws CityDoctorWriteException {
		CityGMLWriterUtils.writeCityModel(file, this);
	}

	private void exportAsOff(String file) {
		file = file.substring(0, file.lastIndexOf('.'));
		File folder = new File(file);
		folder.mkdirs();
		createFeatureStream().forEach(co -> {
			Map<Lod, Set<Polygon>> polygonMap = new EnumMap<>(Lod.class);
			Check c = new AbstractCheck() {
				
				@Override
				public void check(Geometry geom) {
					polygonMap.compute(geom.getLod(), (lod, polys) -> {
						if (polys == null) {
							polys = new HashSet<>();
						}
						for (Polygon poly : geom.getPolygons()) {
							polys.add(poly.getOriginal());
						}
						return polys;
					});
				}
			};
			co.accept(c);
			
			Set<Polygon> highestLod = polygonMap.get(Lod.LOD2);
			if (highestLod == null) {
				highestLod = polygonMap.get(Lod.LOD1);
			}
			if (highestLod == null) {
				return;
			}
			Map<Vertex, Integer> idMap = new LinkedHashMap<>();
			int counter = 0;
			for (Polygon poly : highestLod) {
				counter = addRing(poly.getExteriorRing(), idMap, counter);
			}
			
			try (BufferedWriter writer = Files.newBufferedWriter(folder.toPath().resolve(co.getGmlId() + ".off"))) {
				writer.write("OFF\n");
				writer.write(Integer.toString(idMap.size()));
				writer.write(' ');
				writer.write(Integer.toString(highestLod.size()));
				writer.write(" 0\n");
				for (Vertex v : idMap.keySet()) {
					writer.write(Double.toString(v.getX()));
					writer.write(' ');
					writer.write(Double.toString(v.getY()));
					writer.write(' ');
					writer.write(Double.toString(v.getZ()));
					writer.write('\n');
				}
				for (Polygon poly : highestLod) {
					writer.write(Integer.toString(poly.getExteriorRing().getVertices().size() - 1));
					writer.write(' ');
					for (int i = 0; i < poly.getExteriorRing().getVertices().size() - 1; i++) {
						Vertex v = poly.getExteriorRing().getVertices().get(i);
						writer.write(Integer.toString(idMap.get(v)));
						writer.write(' ');
					}
					writer.write('\n');
				}
			} catch (IOException e) {
				throw new UncheckedIOException(e);
			} 
			
		});
	}

	private int addRing(LinearRing ring, Map<Vertex, Integer> idMap, int counter) {
		for (Vertex v : ring.getVertices()) {
			if (!idMap.containsKey(v)) {
				idMap.put(v, counter);
				counter++;
			}
		}
		return counter;
	}

	public Set<CheckError> collectErrors() {
		List<CheckError> errors = new ArrayList<>();
		collectErrorsFromList(errors, buildings);
		collectErrorsFromList(errors, vegetation);
		collectErrorsFromList(errors, bridges);
		collectErrorsFromList(errors, land);
		collectErrorsFromList(errors, roads);
		collectErrorsFromList(errors, water);
		return new HashSet<>(errors);
	}

	private void collectErrorsFromList(List<CheckError> errors, List<? extends CityObject> cos) {
		for (CityObject co : cos) {
			co.collectContainedErrors(errors);
		}
	}

	public String getFileName() {
		return fileName;
	}

	public void addBridge(BridgeObject coBridge) {
		bridges.add(coBridge);
	}

	public void addBuilding(Building coBuilding) {
		buildings.add(coBuilding);
	}

	public List<Building> getBuildings() {
		return buildings;
	}

	public List<BridgeObject> getBridges() {
		return bridges;
	}

	public void setCityModel(CityModel cModel) {
		this.cModel = cModel;
	}

	public CityModel getCityModel() {
		return cModel;
	}

	public List<TransportationObject> getTransportation() {
		return roads;
	}

	public List<WaterObject> getWater() {
		return water;
	}

	public List<CityObject> getLand() {
		return land;
	}

	public List<Vegetation> getVegetation() {
		return vegetation;
	}

	public void addWater(WaterObject wo) {
		water.add(wo);
	}

	public void addLand(LandObject lo) {
		land.add(lo);
	}

	public void addVegetation(Vegetation veg) {
		vegetation.add(veg);
	}

	public void addTransportation(TransportationObject to) {
		roads.add(to);
	}

	public int getNumberOfFeatures() {
		return buildings.size() + vegetation.size() + bridges.size() + land.size() + roads.size() + water.size();
	}

	public ParserConfiguration getParserConfig() {
		return config;
	}

	public void replaceFeature(CityObject currentFeature, CityObject nextFeature) {
		if (nextFeature instanceof Building) {
			replaceBuilding(currentFeature, nextFeature);
		} else if (nextFeature instanceof BridgeObject) {
			replaceBridge(currentFeature, nextFeature);
		} else if (nextFeature instanceof TransportationObject) {
			replaceTransportationObject(currentFeature, nextFeature);
		} else if (nextFeature instanceof Vegetation) {
			replaceVegetation(currentFeature, nextFeature);
		} else if (nextFeature instanceof LandObject) {
			replaceLandObject(currentFeature, nextFeature);
		} else if (nextFeature instanceof WaterObject) {
			replaceWaterObject(currentFeature, nextFeature);
		}
	}

	private void replaceWaterObject(CityObject currentFeature, CityObject nextFeature) {
		int index = water.indexOf(currentFeature);
		if (index == -1) {
			throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in water objects");
		}
		water.set(index, (WaterObject) nextFeature);
	}

	private void replaceLandObject(CityObject currentFeature, CityObject nextFeature) {
		int index = land.indexOf(currentFeature);
		if (index == -1) {
			throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in land objects");
		}
		land.set(index, (LandObject) nextFeature);
	}

	private void replaceVegetation(CityObject currentFeature, CityObject nextFeature) {
		int index = vegetation.indexOf(currentFeature);
		if (index == -1) {
			throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in vegetation");
		}
		vegetation.set(index, (Vegetation) nextFeature);
	}

	private void replaceTransportationObject(CityObject currentFeature, CityObject nextFeature) {
		int index = roads.indexOf(currentFeature);
		if (index == -1) {
			throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in transportation objects");
		}
		roads.set(index, (TransportationObject) nextFeature);
	}

	private void replaceBridge(CityObject currentFeature, CityObject nextFeature) {
		int index = bridges.indexOf(currentFeature);
		if (index == -1) {
			throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in bridges");
		}
		bridges.set(index, (BridgeObject) nextFeature);
	}

	private void replaceBuilding(CityObject currentFeature, CityObject nextFeature) {
		int index = buildings.indexOf(currentFeature);
		if (index == -1) {
			throw new IllegalStateException(COULD_NOT_FIND_FEATURE + currentFeature + " in buildings");
		}
		buildings.set(index, (Building) nextFeature);
	}

	public void addCityObject(CityObject co) {
		if (co instanceof Building b) {
			buildings.add(b);
		} else if (co instanceof BridgeObject bo) {
			bridges.add(bo);
		} else if (co instanceof TransportationObject to) {
			roads.add(to);
		} else if (co instanceof Vegetation veg) {
			vegetation.add(veg);
		} else if (co instanceof LandObject lo) {
			land.add(lo);
		} else if (co instanceof WaterObject wo) {
			water.add(wo);
		} else if (co instanceof ReliefObject) {
			land.add(co);
		} else if (co instanceof TinObject) {
			land.add(co);
		}
	}

	public void setParsedCityGMLVersion(CityGMLVersion cityGMLVersion) {
		this.cityGMLVersion = cityGMLVersion;
	}
	
	public CityGMLVersion getCityGMLVersion() {
		return cityGMLVersion;
	}

	public void addRelief(ReliefObject relief) {
		addCityObject(relief);
	}

	public void addTin(TinObject tin) {
		addCityObject(tin);
	}
}
