/*-
 *  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.File;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

import org.citygml4j.CityGMLContext;
import org.citygml4j.builder.jaxb.CityGMLBuilder;
import org.citygml4j.builder.jaxb.CityGMLBuilderException;
import org.citygml4j.factory.GMLGeometryFactory;
import org.citygml4j.model.citygml.ade.ADEException;
import org.citygml4j.model.citygml.ade.binding.ADEContext;
import org.citygml4j.model.citygml.core.CityModel;
import org.citygml4j.model.citygml.core.CityObjectMember;
import org.citygml4j.model.module.citygml.CityGMLVersion;
import org.citygml4j.xml.io.CityGMLOutputFactory;
import org.citygml4j.xml.io.writer.CityGMLWriteException;
import org.citygml4j.xml.io.writer.CityGMLWriter;

import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.ErrorId;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.Localization;
import de.hft.stuttgart.citydoctor2.utils.QualityADEUtils;
import de.hft.stuttgart.quality.QualityADEModule;
import de.hft.stuttgart.quality.model.Validation;
import de.hft.stuttgart.quality.model.jaxb.ErrorStatistics;
import de.hft.stuttgart.quality.model.jaxb.FeatureStatistics;
import de.hft.stuttgart.quality.model.jaxb.Statistics;
import de.hft.stuttgart.quality.model.jaxb.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<LandObject> 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;

	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 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 CityGMLBuilderException, CityGMLWriteException, ADEException {
		CityGMLContext gmlContext = CityGMLContext.getInstance();
		// setup energy ade stuff, so the parser doesn't crash on encountering this
		if (!gmlContext.hasADEContexts()) {
			for (ADEContext adeContext : ServiceLoader.load(ADEContext.class)) {
				gmlContext.registerADEContext(adeContext);
			}
		}
		CityGMLBuilder builder = gmlContext.createCityGMLBuilder();
		CityGMLOutputFactory factory = builder.createCityGMLOutputFactory();
		try (CityGMLWriter writer = factory.createCityGMLWriter(new File(file))) {
			if (isValidated) {
				writer.setPrefix("qual", QualityADEModule.NAMESPACE_URI);
				writer.setSchemaLocation(QualityADEModule.NAMESPACE_URI,
						"https://transfer.hft-stuttgart.de/pages/qualityade/0.1/qualityAde.xsd");
				// remove old model if available
				QualityADEUtils.removeValidation(cModel);
				
				// create new quality ade validation datastructures
				Validation val = createValidation();
				
				// add to city model
				cModel.addGenericApplicationPropertyOfCityModel(val);
			}
			writer.setIndentString("  ");
			writer.setPrefixes(CityGMLVersion.DEFAULT);
			writer.setSchemaLocations(CityGMLVersion.DEFAULT);
			GMLGeometryFactory gmlFactory = new GMLGeometryFactory();
			storeCityObjects(buildings, gmlFactory);
			storeCityObjects(vegetation, gmlFactory);
			storeCityObjects(bridges, gmlFactory);
			storeCityObjects(land, gmlFactory);
			storeCityObjects(roads, gmlFactory);
			storeCityObjects(water, gmlFactory);
			writer.write(cModel);
			cModel.unsetCityObjectMember();
		}
	}

	private void storeCityObjects(List<? extends CityObject> cos, GMLGeometryFactory gmlFactory) {
		for (CityObject co : cos) {
			QualityADEUtils.removeValidationResult(co);
			if (isValidated) {
				QualityADEUtils.writeQualityAde(co);
			}
			co.reCreateGeometries(gmlFactory, config);
			cModel.addCityObjectMember(new CityObjectMember(co.getGmlObject()));
		}
	}

	private Validation createValidation() {
		Validation val = new Validation();
		val.setValidationDate(ZonedDateTime.now());
		val.setValidationSoftware("CityDoctor " + Localization.getText(Localization.VERSION));
		Statistics statistics = new Statistics();
		Set<CheckError> errors = collectErrors();
		Map<ErrorId, AtomicInteger> errorCount = new HashMap<>();
		for (CheckError e : errors) {
			errorCount.compute(e.getErrorId(), (k, v) -> {
				if (v == null) {
					return new AtomicInteger(1);
				}
				v.incrementAndGet();
				return v;
			});
		}
		for (Entry<ErrorId, AtomicInteger> e : errorCount.entrySet()) {
			ErrorStatistics stats = new ErrorStatistics();
			stats.setAmount(e.getValue().get());
			stats.setName(QualityADEUtils.mapErrorIdToAdeId(e.getKey()));
			statistics.getErrorStatistics().add(stats);
		}
		statistics.setNumErrorBuildings(countValidatedCityObjects(buildings));
		statistics.setNumErrorBridgeObjects(countValidatedCityObjects(bridges));
		statistics.setNumErrorLandObjects(countValidatedCityObjects(land));
		statistics.setNumErrorTransportation(countValidatedCityObjects(roads));
		statistics.setNumErrorVegetation(countValidatedCityObjects(vegetation));
		statistics.setNumErrorWaterObjects(countValidatedCityObjects(water));
		val.setStatistics(statistics);
		val.setValidationPlan(plan);
		return val;
	}

	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);
		}
	}

	private FeatureStatistics countValidatedCityObjects(List<? extends CityObject> cos) {
		int numChecked = 0;
		int numError = 0;
		for (CityObject co : cos) {
			if (co.isValidated()) {
				numChecked++;
				if (co.containsAnyError()) {
					numError++;
				}
			}
		}
		return new FeatureStatistics(numChecked, numError);
	}

	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<LandObject> 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);
	}
}
