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

import java.io.OutputStream;
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.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import de.hft.stuttgart.citydoctor2.CDVMessages;
import de.hft.stuttgart.citydoctor2.check.CheckConfiguration;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.ErrorType;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.checkresult.CheckReport;
import de.hft.stuttgart.citydoctor2.checkresult.Environment;
import de.hft.stuttgart.citydoctor2.checkresult.ErrorDetails;
import de.hft.stuttgart.citydoctor2.checkresult.ErrorReport;
import de.hft.stuttgart.citydoctor2.checkresult.ErrorStatistic;
import de.hft.stuttgart.citydoctor2.checkresult.ErrorStatus;
import de.hft.stuttgart.citydoctor2.checkresult.Feature;
import de.hft.stuttgart.citydoctor2.checkresult.FeatureReport;
import de.hft.stuttgart.citydoctor2.checkresult.GlobalErrorStatistics;
import de.hft.stuttgart.citydoctor2.checkresult.GlobalStatistics;
import de.hft.stuttgart.citydoctor2.checkresult.Header;
import de.hft.stuttgart.citydoctor2.checkresult.ModelStatistics;
import de.hft.stuttgart.citydoctor2.checkresult.PlanCheck;
import de.hft.stuttgart.citydoctor2.checkresult.PlanParameter;
import de.hft.stuttgart.citydoctor2.checkresult.Statistics;
import de.hft.stuttgart.citydoctor2.checkresult.ValidationPlan;
import de.hft.stuttgart.citydoctor2.checkresult.ValidationResults;
import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.BuildingPart;
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;

/**
 * Report for creating a XML report out of a stream of feature results
 * 
 * @author Matthias Betz
 *
 */
public class XmlStreamReporter implements StreamReporter {

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

	private OutputStream output;

	private CheckReport report;
	private Map<String, FeatureReport> reportMap;
	private ValidationConfiguration config;

	public XmlStreamReporter(OutputStream output, String fileName, ValidationConfiguration config) {
		this.output = output;
		this.config = config;
		reportMap = new HashMap<>();

		report = new CheckReport();

		report.setHeader(createHeader(fileName));
		report.setValidationPlan(createValidationPlan());
		report.setValidationResults(new ValidationResults());
	}

	private Header createHeader(String fileName) {
		Header header = new Header();
		header.setDate(ZonedDateTime.now());
		header.setFile(fileName);

		Environment env = new Environment();
		header.setEnvironment(env);
		env.setJavaVersion(System.getProperties().getProperty("java.vm.version"));
		env.setJavaVmVendor(System.getProperties().getProperty("java.vm.vendor"));
		env.setJavaVmVersion(System.getProperties().getProperty("java.version"));
		env.setOsArch(System.getProperties().getProperty("os.arch"));
		env.setOsName(System.getProperties().getProperty("os.name"));
		env.setValidationVersion(CDVMessages.getString(CDVMessages.VERSION));
		return header;
	}

	private ValidationPlan createValidationPlan() {
		ValidationPlan plan = new ValidationPlan();
		plan.setNumberOfRoundingPlaces(config.getNumberOfRoundingPlaces());
		List<PlanCheck> checkConfigs = new ArrayList<>();
		plan.setChecks(checkConfigs);
		for (Entry<CheckId, CheckConfiguration> e : config.getChecks().entrySet()) {
			PlanCheck checkConfig = new PlanCheck();
			checkConfigs.add(checkConfig);
			checkConfig.setName(e.getKey().toString());
			if (e.getValue().isEnabled()) {
				checkConfig.setActivated(true);
				Map<String, String> checkParams = e.getValue().getParameters();
				if (checkParams != null && !checkParams.isEmpty()) {
					List<PlanParameter> parameters = new ArrayList<>();
					checkConfig.setParameters(parameters);
					for (Entry<String, String> entry : checkParams.entrySet()) {
						PlanParameter param = new PlanParameter();
						param.setName(entry.getKey());
						param.setValue(entry.getValue());
						parameters.add(param);
					}
				}
			} else {
				checkConfig.setActivated(false);
			}
		}
		plan.setSchematronFile(config.getSchematronFilePath());
		return plan;
	}

	@Override
	public void report(CityObject co) {
		if (co instanceof Building) {
			reportBuilding((Building) co);
		} else if (co instanceof Vegetation) {
			reportVegetation((Vegetation) co);
		} else if (co instanceof TransportationObject) {
			reportTrans((TransportationObject) co);
		} else if (co instanceof BridgeObject) {
			reportBridge((BridgeObject) co);
		} else if (co instanceof WaterObject) {
			reportWater((WaterObject) co);
		} else if (co instanceof LandObject) {
			reportLand((LandObject) co);
		} else {
			throw new IllegalStateException("Not reportable CityObject found: " + co.getClass().getSimpleName());
		}
	}

	@Override
	public void reportGlobalError(CheckError err) {
		report.getValidationResults().getGlobalErrors().add(createErrorReport(err));
	}

	private void reportLand(LandObject lo) {
		FeatureReport fr = createCityObjectReportNode(lo);
		report.getValidationResults().getLandReports().add(fr);
	}

	private void reportWater(WaterObject wo) {
		FeatureReport fr = createCityObjectReportNode(wo);
		report.getValidationResults().getWaterReports().add(fr);
	}

	private void reportBridge(BridgeObject bo) {
		FeatureReport fr = createCityObjectReportNode(bo);
		report.getValidationResults().getBridgeReports().add(fr);
	}

	private void reportTrans(TransportationObject to) {
		FeatureReport fr = createCityObjectReportNode(to);
		report.getValidationResults().getTransportationReports().add(fr);
	}

	private void reportVegetation(Vegetation v) {
		FeatureReport fr = createCityObjectReportNode(v);
		report.getValidationResults().getVegetationReports().add(fr);
	}

	private void reportBuilding(Building co) {
		FeatureReport fr = createCityObjectReportNode(co);
		for (BuildingPart bp : co.getBuildingParts()) {
			reportMap.put(bp.getGmlId().getGmlString(), fr);
		}
		report.getValidationResults().getBuildingReports().add(fr);
	}

	private FeatureReport createCityObjectReportNode(CityObject co) {
		FeatureReport fr = new FeatureReport();
		reportMap.put(co.getGmlId().getGmlString(), fr);
		fr.setGmlId(co.getGmlId().getGmlString());

		List<CheckError> errors = new ArrayList<>();
		co.collectContainedErrors(errors);
		// Filter out duplicate errors (from linked polygons)
		Set<CheckError> errorSet = new HashSet<>(errors);
		for (CheckError err : errorSet) {
			ErrorReport errReport = createErrorReport(err);
			fr.getErrors().add(errReport);
		}

		return fr;
	}

	private ErrorReport createErrorReport(CheckError err) {
		ErrorReport errReport = new ErrorReport();
		errReport.setId(err.getErrorId().toString());
		if (err.getType() == ErrorType.WARNING) {
			errReport.setStatus(ErrorStatus.WARNING);
		} else if (err.getType() == ErrorType.ERROR) {
			errReport.setStatus(ErrorStatus.ERROR);
		} else {
			throw new IllegalStateException("Unknown error type: " + err.getType());
		}
		if (err.getFeature() != null) {
			Feature feature = new Feature();
			feature.setGmlId(err.getFeature().getGmlId().getGmlString());
			feature.setType(err.getFeature().getCheckClass().getSimpleName());
			errReport.setFeature(feature);
		}

		XmlStreamErrorHandler handler = new XmlStreamErrorHandler(config.getParserConfiguration());
		err.report(handler);
		ErrorDetails details = handler.getDetails();
		errReport.setErrorDetails(details);
		return errReport;
	}

	@Override
	public void finishReport() throws CheckReportWriteException {
		GlobalStatistics stats = new GlobalStatistics();
		GlobalErrorStatistics globErrStats = new GlobalErrorStatistics();
		globErrStats
				.setNumErrorBridgeObjects(getNumberOfErrorFeatures(report.getValidationResults().getBridgeReports()));
		globErrStats.setNumErrorBuildings(getNumberOfErrorFeatures(report.getValidationResults().getBuildingReports()));
		globErrStats.setNumErrorLandObjects(getNumberOfErrorFeatures(report.getValidationResults().getLandReports()));
		globErrStats.setNumErrorTransportation(
				getNumberOfErrorFeatures(report.getValidationResults().getTransportationReports()));
		globErrStats
				.setNumErrorVegetation(getNumberOfErrorFeatures(report.getValidationResults().getVegetationReports()));
		globErrStats.setNumErrorWaterObjects(getNumberOfErrorFeatures(report.getValidationResults().getWaterReports()));
		stats.setGlobalErrorStats(globErrStats);

		ModelStatistics modelStats = new ModelStatistics();
		modelStats.setNumBridgeObjects(report.getValidationResults().getBridgeReports().size());
		modelStats.setNumBuildings(report.getValidationResults().getBuildingReports().size());
		modelStats.setNumLandObjects(report.getValidationResults().getLandReports().size());
		modelStats.setNumTransportation(report.getValidationResults().getTransportationReports().size());
		modelStats.setNumVegetation(report.getValidationResults().getVegetationReports().size());
		modelStats.setNumWaterObjects(report.getValidationResults().getWaterReports().size());
		stats.setModelStats(modelStats);

		ErrorStatisticsCollector globalErrorCount = new ErrorStatisticsCollector();

		for (ErrorReport errReport : report.getValidationResults().getGlobalErrors()) {
			globalErrorCount.addError(errReport.getId());
		}

		createStatistics(globalErrorCount, report.getValidationResults().getBridgeReports());
		createStatistics(globalErrorCount, report.getValidationResults().getBuildingReports());
		createStatistics(globalErrorCount, report.getValidationResults().getLandReports());
		createStatistics(globalErrorCount, report.getValidationResults().getTransportationReports());
		createStatistics(globalErrorCount, report.getValidationResults().getVegetationReports());
		createStatistics(globalErrorCount, report.getValidationResults().getWaterReports());

		addStatisticsObjects(stats.getErrorStats(), globalErrorCount);
		report.setGlobalStatistics(stats);
		report.saveAs(output);
	}

	private void createStatistics(ErrorStatisticsCollector globalErrorCount, List<FeatureReport> reports) {
		for (FeatureReport fReport : reports) {
			if (fReport.getErrors().isEmpty()) {
				fReport.setErrors(null);
				continue;
			}
			ErrorStatisticsCollector collector = new ErrorStatisticsCollector();
			for (ErrorReport errReport : fReport.getErrors()) {
				collector.addError(errReport.getId());
				globalErrorCount.addError(errReport.getId());
			}
			Statistics reportStats = new Statistics();
			addStatisticsObjects(reportStats.getErrorStats(), collector);
			fReport.setStatistics(reportStats);
		}
	}

	private void addStatisticsObjects(List<ErrorStatistic> stats, ErrorStatisticsCollector collector) {
		for (Entry<String, AtomicInteger> er : collector.getErrorCounts().entrySet()) {
			if (er.getValue().get() > 0) {
				ErrorStatistic errStats = new ErrorStatistic();
				errStats.setName(er.getKey());
				errStats.setCount(er.getValue().get());
				stats.add(errStats);
			}
		}
	}

	private int getNumberOfErrorFeatures(List<FeatureReport> reports) {
		int errorFeatures = 0;
		for (FeatureReport fReport : reports) {
			boolean hasError = hasError(fReport);
			if (hasError) {
				errorFeatures++;
			}
		}
		return errorFeatures;
	}

	private boolean hasError(FeatureReport fReport) {
		return !fReport.getErrors().isEmpty();
	}

	@Override
	public void addError(String gmlId, CheckError err) {
		FeatureReport fReport = reportMap.get(gmlId);
		if (fReport == null) {
			logger.warn("Error reported for unknown gml id: {}, adding to global errors instead", gmlId);
			reportGlobalError(err);
		} else {
			fReport.getErrors().add(createErrorReport(err));
		}
	}
}
