/*-
 *  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.writer;

import java.io.File;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.core.AbstractCityObjectProperty;
import org.citygml4j.core.model.core.AbstractFeatureProperty;
import org.citygml4j.core.model.core.CityModel;
import org.citygml4j.core.util.geometry.GeometryFactory;
import org.citygml4j.xml.CityGMLContext;
import org.citygml4j.xml.writer.CityGMLOutputFactory;
import org.citygml4j.xml.writer.CityGMLWriteException;
import org.citygml4j.xml.writer.CityGMLWriter;
import org.xmlobjects.gml.model.base.Reference;

import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.ErrorId;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
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.properties.ErrorProperty;
import de.hft.stuttgart.quality.model.properties.FeatureStatisticsProperty;
import de.hft.stuttgart.quality.model.properties.StatisticsProperty;
import de.hft.stuttgart.quality.model.types.FeatureStatistics;
import de.hft.stuttgart.quality.model.types.Statistics;
import de.hft.stuttgart.quality.model.types.Validation;

public class CityGMLWriterUtils {
	
	private static final Logger logger = LogManager.getLogger(CityGMLWriterUtils.class);
	
	private CityGMLWriterUtils() {
	}
	
	public static void writeCityModel(String file, CityDoctorModel model) throws CityDoctorWriteException {
		CityGMLContext gmlContext = CityGmlParser.getContext();
		CityModel cModel = model.getCityModel();
		CityGMLOutputFactory factory = gmlContext.createCityGMLOutputFactory(CityGMLVersion.v2_0);
		try (CityGMLWriter writer = factory.createCityGMLWriter(new File(file))) {
			writer.withIndent("  ");
			writer.withDefaultPrefixes();
			writer.withDefaultSchemaLocations();
			Validation val = null;
			if (model.isValidated()) {
				writer.withPrefix("qual", QualityADEModule.NAMESPACE_URI);
				writer.withSchemaLocation(QualityADEModule.NAMESPACE_URI,
						QualityADEModule.NAMESPACE_URI + "/qualityAde.xsd");
				// create new quality ade validation datastructures
				val = createValidation(model);

				// add to city model
				cModel.getFeatureMembers().add(new AbstractFeatureProperty(val));
				cModel.getFeatureMembers().add(new AbstractFeatureProperty(model.getValidationPlan()));
			}
			GeometryFactory gmlFactory = GeometryFactory.newInstance();
			storeCityObjects(model.getBuildings(), gmlFactory, model, cModel, val);
			storeCityObjects(model.getVegetation(), gmlFactory, model, cModel, val);
			storeCityObjects(model.getBridges(), gmlFactory, model, cModel, val);
			storeCityObjects(model.getLand(), gmlFactory, model, cModel, val);
			storeCityObjects(model.getTransportation(), gmlFactory, model, cModel, val);
			storeCityObjects(model.getWater(), gmlFactory, model, cModel, val);

			writer.write(cModel);
			cModel.getCityObjectMembers().clear();
			if (model.isValidated()) {
				// remove validation plan again to not re-add it on recurring save
				Iterator<AbstractFeatureProperty> it = cModel.getFeatureMembers().iterator();
				while (it.hasNext()) {
					AbstractFeatureProperty afp = it.next();
					if (afp.getObject() == val || afp.getObject() == model.getValidationPlan()) {
						it.remove();
					}
				}
			}
		} catch (CityGMLWriteException e) {
			throw new CityDoctorWriteException(e);
		}
	}
	
	private static Validation createValidation(CityDoctorModel model) {
		Validation val = new Validation();
		val.setId("CD" + UUID.randomUUID().toString());
		val.setValidationDate(ZonedDateTime.now());
		val.setValidationSoftware("CityDoctor " + Localization.getText(Localization.VERSION));
		Statistics statistics = new Statistics();
		Set<CheckError> errors = model.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()) {
			de.hft.stuttgart.quality.model.types.Error stats = new de.hft.stuttgart.quality.model.types.Error();
			stats.setOccurrences(e.getValue().get());
			de.hft.stuttgart.quality.model.enums.ErrorId adeId = QualityADEUtils.mapErrorIdToAdeId(e.getKey());
			if (adeId == null) {
				// error that is not part of the ade standard
				logger.warn("Found error {} that cannot be converted to ADE error. It will not be written", e.getKey());
				continue;
			}
			stats.setName(adeId);
			statistics.getErrors().add(new ErrorProperty(stats));
		}
		statistics.setNumErrorBuildings(new FeatureStatisticsProperty(countValidatedCityObjects(model.getBuildings())));
		statistics.setNumErrorBridgeObjects(new FeatureStatisticsProperty(countValidatedCityObjects(model.getBridges())));
		statistics.setNumErrorLandObjects(new FeatureStatisticsProperty(countValidatedCityObjects(model.getLand())));
		statistics.setNumErrorTransportation(new FeatureStatisticsProperty(countValidatedCityObjects(model.getTransportation())));
		statistics.setNumErrorVegetation(new FeatureStatisticsProperty(countValidatedCityObjects(model.getVegetation())));
		statistics.setNumErrorWaterObjects(new FeatureStatisticsProperty(countValidatedCityObjects(model.getWater())));
		val.setStatistics(new StatisticsProperty(statistics));
		val.setValidationPlan(new Reference(model.getValidationPlan()));
		return val;
	}
	
	private static 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);
	}
	
	private static void storeCityObjects(List<? extends CityObject> cos, GeometryFactory gmlFactory, CityDoctorModel model, CityModel cModel, Validation val) {
		for (CityObject co : cos) {
			if (model.isValidated() && val != null) {
				QualityADEUtils.writeQualityAde(co, val);
			}
			co.reCreateGeometries(gmlFactory, model.getParserConfig());
			cModel.getCityObjectMembers().add(new AbstractCityObjectProperty(co.getGmlObject()));
		}
	}

}
