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

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.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.core.AbstractFeatureProperty;
import org.citygml4j.core.model.core.CityModel;
import org.citygml4j.core.util.geometry.GeometryFactory;
import org.xmlobjects.gml.model.base.Reference;

import de.hft.stuttgart.citydoctor2.check.error.SchematronError;
import de.hft.stuttgart.citydoctor2.checks.SvrlContentHandler;
import de.hft.stuttgart.citydoctor2.checks.util.FeatureCheckedListener;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.LandObject;
import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject;
import de.hft.stuttgart.citydoctor2.datastructure.Vegetation;
import de.hft.stuttgart.citydoctor2.datastructure.WaterObject;
import de.hft.stuttgart.citydoctor2.parser.CityGmlConsumer;
import de.hft.stuttgart.citydoctor2.reporting.XmlStreamReporter;
import de.hft.stuttgart.citydoctor2.reporting.pdf.PdfStreamReporter;
import de.hft.stuttgart.citydoctor2.utils.Localization;
import de.hft.stuttgart.citydoctor2.utils.QualityADEUtils;
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;
import de.hft.stuttgart.quality.model.types.ValidationPlan;

public class StreamCityGmlConsumer implements CityGmlConsumer {

	private static final Logger logger = LogManager.getLogger(StreamCityGmlConsumer.class);

	private Checker c;
	private XmlStreamReporter xmlReporter;
	private PdfStreamReporter pdfReporter;
	private SvrlContentHandler handler;
	private Map<ErrorId, AtomicInteger> errorCount;
	private GeometryFactory gmlFactory;
	private ValidationConfiguration config;
	private Statistics statistics;
	private FeatureStatistics buildingStatistics;
	private FeatureStatistics bridgeStatistics;
	private FeatureStatistics transportationStatistics;
	private FeatureStatistics vegetationStatistics;
	private FeatureStatistics landStatistics;
	private FeatureStatistics waterStatistics;
	private Validation val;
	private FeatureCheckedListener l;

	public StreamCityGmlConsumer(Checker c, XmlStreamReporter xmlReporter, PdfStreamReporter pdfReporter,
			SvrlContentHandler handler, ValidationConfiguration config, FeatureCheckedListener l) {
		this.c = c;
		this.xmlReporter = xmlReporter;
		this.pdfReporter = pdfReporter;
		this.handler = handler;
		this.config = config;
		this.l = l;
		errorCount = new HashMap<>();
		gmlFactory = GeometryFactory.newInstance();

		val = new Validation();
		val.setId("CD" + UUID.randomUUID().toString());
		val.setValidationDate(ZonedDateTime.now());
		val.setValidationSoftware("CityDoctor " + Localization.getText(Localization.VERSION));
		statistics = new Statistics();
		buildingStatistics = new FeatureStatistics();
		statistics.setNumErrorBuildings(new FeatureStatisticsProperty(buildingStatistics));
		bridgeStatistics = new FeatureStatistics();
		statistics.setNumErrorBridgeObjects(new FeatureStatisticsProperty(bridgeStatistics));
		transportationStatistics = new FeatureStatistics();
		statistics.setNumErrorTransportation(new FeatureStatisticsProperty(transportationStatistics));
		vegetationStatistics = new FeatureStatistics();
		statistics.setNumErrorVegetation(new FeatureStatisticsProperty(vegetationStatistics));
		landStatistics = new FeatureStatistics();
		statistics.setNumErrorLandObjects(new FeatureStatisticsProperty(landStatistics));
		waterStatistics = new FeatureStatistics();
		statistics.setNumErrorWaterObjects(new FeatureStatisticsProperty(waterStatistics));

	}

	@Override
	public void accept(CityObject co) {
		if (logger.isDebugEnabled()) {
			logger.debug(Localization.getText("Checker.checkFeature"), co);
		}

		// check without reporters, nothing will be put into the reports like that
		// this will also clear old check results if any (though in stream mode there
		// won't be any)
		c.checkFeature(null, null, co);

		// now add the schematron errors
		if (handler != null) {
			List<SchematronError> errors = handler.getFeatureErrors().get(co.getGmlId().getGmlString());
			if (errors != null) {
				Checker.handleSchematronErrorsForCityObject(errors, co);
			}
		}

		// now add the city object to the reports so it includes the schematron errors
		// as well
		if (xmlReporter != null) {
			xmlReporter.report(co);
		}
		if (pdfReporter != null) {
			pdfReporter.report(co);
		}

		// store quality ade datastructures in cityobject
		QualityADEUtils.writeQualityAde(co, val);
		// recreate geometry
		co.reCreateGeometries(gmlFactory, config.getParserConfiguration());

		// store result in statistics
		applyToStatistics(buildingStatistics, bridgeStatistics, transportationStatistics, vegetationStatistics,
				landStatistics, waterStatistics, co);

		// add errors to statistics
		List<CheckError> errorList = new ArrayList<>();
		co.collectContainedErrors(errorList);
		Set<CheckError> errors = new HashSet<>(errorList);
		for (CheckError e : errors) {
			errorCount.compute(e.getErrorId(), (k, v) -> {
				if (v == null) {
					return new AtomicInteger(1);
				}
				v.incrementAndGet();
				return v;
			});
		}

		if (l != null) {
			l.featureChecked(co);
		}
	}

	@Override
	public void accept(CityModel cm) {
		for (Entry<ErrorId, AtomicInteger> e : errorCount.entrySet()) {
			var 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
				continue;
			}
			stats.setName(adeId);
			statistics.getErrors().add(new ErrorProperty(stats));
		}
		val.setStatistics(new StatisticsProperty(statistics));
		ValidationPlan validationPlan = c.createValidationPlan();
		validationPlan.setId("CD_PLAN_" + UUID.randomUUID().toString());
		
		cm.getFeatureMembers().add(new AbstractFeatureProperty(validationPlan));
		val.setValidationPlan(new Reference(validationPlan));

		cm.getFeatureMembers().add(new AbstractFeatureProperty(val));
	}

	private static void applyToStatistics(FeatureStatistics buildingStatistics, FeatureStatistics bridgeStatistics,
			FeatureStatistics transportationStatistics, FeatureStatistics vegetationStatistics,
			FeatureStatistics landStatistics, FeatureStatistics waterStatistics, CityObject co) {
		if (co.isValidated()) {
			if (co instanceof Building) {
				countForFeatureStatistics(buildingStatistics, co);
			} else if (co instanceof TransportationObject) {
				countForFeatureStatistics(transportationStatistics, co);
			} else if (co instanceof BridgeObject) {
				countForFeatureStatistics(bridgeStatistics, co);
			} else if (co instanceof WaterObject) {
				countForFeatureStatistics(waterStatistics, co);
			} else if (co instanceof LandObject) {
				countForFeatureStatistics(landStatistics, co);
			} else if (co instanceof Vegetation) {
				countForFeatureStatistics(vegetationStatistics, co);
			}
		}
	}

	private static void countForFeatureStatistics(FeatureStatistics featureStatistics, CityObject co) {
		featureStatistics.setNumChecked(featureStatistics.getNumChecked() + 1);
		if (co.containsAnyError()) {
			featureStatistics.setNumErrors(featureStatistics.getNumErrors() + 1);
		}
	}

}
