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

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
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.check.CheckConfiguration;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ErrorId;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.check.error.SchematronError;
import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface;
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.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LandObject;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Opening;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
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.reporting.StreamReporter;
import de.hft.stuttgart.citydoctor2.utils.Localization;

/**
 * Reporter to create a PDF report out of a stream of feature results
 * 
 * @author Matthias Betz
 *
 */
public class PdfStreamReporter implements StreamReporter {

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

	private static final String ERROR_COLOR = "red";
	@SuppressWarnings("unused")
	private static final String WARNING_COLOR = "yellow";
	private static final String OK_COLOR = "green";

	private PdfReport report;
	private OutputStream outFile;

	private Map<ErrorId, AtomicInteger> errorStatistics;
	private ValidationConfiguration config;

	private Section statistics;
	private Section vr;

	private Section buildings;
	private int numErrorBuildings;
	private int numOkBuildings;

	private Section vegetation;
	private int numErrorVegetation;
	private int numOkVegetation;

	private Section trans;
	private int numErrorTrans;
	private int numOkTrans;

	private Section bridge;
	private int numErrorBridge;
	private int numOkBridge;

	private Section water;
	private int numErrorWater;
	private int numOkWater;

	private Section land;
	private int numErrorLand;
	private int numOkLand;

	private Section globalErrors;

	private Map<String, Section> sectionMap = new HashMap<>();

	public PdfStreamReporter(OutputStream pdfOutputFile, String fileName, ValidationConfiguration config,
			String logoPath) {
		this.config = config;
		errorStatistics = new EnumMap<>(ErrorId.class);
		outFile = pdfOutputFile;
		report = new PdfReport(logoPath);
		report.writeSourceFileName(fileName);

		writeEnvironment();

		statistics = report.createSection("Statistics", true);
		statistics.addTextElement("Object distribution:");

		writeValidationPlan();

		vr = report.createSection("Validation Results", true);
	}

	private void writeValidationPlan() {
		Section vp = report.createSection("Validation Plan", true);
		vp.addTextElement("numberOfRoundingPlaces = " + config.getNumberOfRoundingPlaces());
		vp.addTextElement("minVertexDistance = " + config.getMinVertexDistance());
		if (config.getSchematronFilePath() != null && !config.getSchematronFilePath().isEmpty()) {
			vp.addTextElement("Schematron file: " + config.getSchematronFilePath());
		}
		for (Entry<CheckId, CheckConfiguration> e : config.getChecks().entrySet()) {
			String text = e.getKey().toString();
			String color;
			if (e.getValue().isEnabled()) {
				text = text + " = enabled";
				color = OK_COLOR;
			} else {
				text = text + " = disabled";
				color = ERROR_COLOR;
			}
			vp.addTextElement(text, color);
			for (Entry<String, String> parameter : e.getValue().getParameters().entrySet()) {
				vp.add10PtTextElement(parameter.getKey() + " = " + parameter.getValue(), 10);
			}
		}
	}

	private void writeEnvironment() {
		Section env = report.createSection("Environment");
		env.addTextElement("The checks were executed under the following environment:");
		Table t = new Table(2);
		t.setTitle("Name", "Value");
		t.addRow("City Doctor Version", Localization.getText(Localization.VERSION));
		t.addRow("Java VM-Version", System.getProperties().getProperty("java.vm.version"));
		t.addRow("Java VM-Vendor", System.getProperties().getProperty("java.vm.vendor"));
		t.addRow("Java Version", System.getProperties().getProperty("java.version"));
		t.addRow("OS Name", System.getProperties().getProperty("os.name"));
		t.addRow("OS Architecture", System.getProperties().getProperty("os.arch"));
		env.addTable(t);
	}

	@Override
	public void report(CityObject co) {
		List<CheckError> errorList = new ArrayList<>();
		co.collectContainedErrors(errorList);
		Set<CheckError> errors = new HashSet<>(errorList);
		for (CheckError e : errors) {
			AtomicInteger errorCount = errorStatistics.computeIfAbsent(e.getErrorId(), k -> new AtomicInteger(0));
			errorCount.incrementAndGet();
		}
		boolean hasError = containsError(co);
		if (co instanceof Building) {
			reportBuilding(co, hasError);
		} else if (co instanceof Vegetation) {
			reportVegetation(co, hasError);
		} else if (co instanceof TransportationObject) {
			reportTrans(co, hasError);
		} else if (co instanceof BridgeObject) {
			reportBridge(co, hasError);
		} else if (co instanceof WaterObject) {
			reportWater(co, hasError);
		} else if (co instanceof LandObject) {
			reportLand(co, hasError);
		} else {
			throw new IllegalStateException("Unknown City Object found: " + co.getClass());
		}
	}

	private void reportLand(CityObject co, boolean hasError) {
		if (land == null) {
			land = vr.createSubSection("Land Objects");
		}
		Section lSection = land.createSubSection(co.getGmlId().getGmlString());
		sectionMap.put(co.getGmlId().getGmlString(), lSection);
		if (hasError) {
			numErrorLand++;
			lSection.setHeadlineColor(ERROR_COLOR);
		} else {
			numOkLand++;
			lSection.setHeadlineColor(OK_COLOR);
		}
		writeErrorForCityObject(co, lSection);
	}

	private void reportWater(CityObject co, boolean hasError) {
		if (water == null) {
			water = vr.createSubSection("Water Objects");
		}
		Section wSection = water.createSubSection(co.getGmlId().getGmlString());
		sectionMap.put(co.getGmlId().getGmlString(), wSection);
		if (hasError) {
			numErrorWater++;
			wSection.setHeadlineColor(ERROR_COLOR);
		} else {
			numOkWater++;
			wSection.setHeadlineColor(OK_COLOR);
		}
		writeErrorForCityObject(co, wSection);
	}

	private void reportBridge(CityObject co, boolean hasError) {
		if (bridge == null) {
			bridge = vr.createSubSection("Bridge Objects");
		}
		Section bSection = bridge.createSubSection(co.getGmlId().getGmlString());
		sectionMap.put(co.getGmlId().getGmlString(), bSection);
		if (hasError) {
			numErrorBridge++;
			bSection.setHeadlineColor(ERROR_COLOR);
		} else {
			numOkBridge++;
			bSection.setHeadlineColor(OK_COLOR);
		}
		writeErrorForCityObject(co, bSection);
	}

	private void reportTrans(CityObject co, boolean hasError) {
		if (trans == null) {
			trans = vr.createSubSection("Transportation Objects");
		}
		Section tSection = trans.createSubSection(co.getGmlId().getGmlString());
		sectionMap.put(co.getGmlId().getGmlString(), tSection);
		if (hasError) {
			numErrorTrans++;
			tSection.setHeadlineColor(ERROR_COLOR);
		} else {
			numOkTrans++;
			tSection.setHeadlineColor(OK_COLOR);
		}
		writeErrorForCityObject(co, tSection);
		TransportationObject to = (TransportationObject) co;
		for (TransportationObject transO : to.getChildren()) {
			writeCheckResultForTransportationObject(transO, tSection);
		}
	}

	private void reportVegetation(CityObject co, boolean hasError) {
		if (vegetation == null) {
			vegetation = vr.createSubSection("Vegetation");
		}
		Section vSection = vegetation.createSubSection(co.getGmlId().getGmlString());
		sectionMap.put(co.getGmlId().getGmlString(), vSection);
		if (hasError) {
			numErrorVegetation++;
			vSection.setHeadlineColor(ERROR_COLOR);
		} else {
			numOkVegetation++;
			vSection.setHeadlineColor(OK_COLOR);
		}
		writeErrorForCityObject(co, vSection);
	}

	private void reportBuilding(CityObject co, boolean hasError) {
		if (buildings == null) {
			buildings = vr.createSubSection("Buildings");
		}
		Section bSection = buildings.createSubSection(co.getGmlId().getGmlString());
		sectionMap.put(co.getGmlId().getGmlString(), bSection);
		if (hasError) {
			bSection.setHeadlineColor(ERROR_COLOR);
		} else {
			bSection.setHeadlineColor(OK_COLOR);
		}
		writeErrorForCityObject(co, bSection);
		Building b = (Building) co;
		for (BuildingPart bp : b.getBuildingParts()) {
			sectionMap.put(bp.getGmlId().getGmlString(), bSection);
			writeCheckResultForBuildingPart(bp, bSection);
		}
		for (BoundarySurface bs : b.getBoundarySurfaces()) {
			writeCheckResultForBoundarySurface(bs, bSection);
		}
	}

	private void writeCheckResultForTransportationObject(TransportationObject to, Section root) {
		Map<CheckId, CheckResult> results = to.getAllCheckResults();
		writeCheckResults(results.values(), root);
		for (Geometry geom : to.getGeometries()) {
			writeCheckResultForGeometry(geom, root);
		}
	}

	private void writeCheckResultForBuildingPart(BuildingPart bp, Section root) {
		Map<CheckId, CheckResult> results = bp.getAllCheckResults();
		writeCheckResults(results.values(), root);
		for (Geometry geom : bp.getGeometries()) {
			writeCheckResultForGeometry(geom, root);
		}
		for (BoundarySurface bs : bp.getBoundarySurfaces()) {
			writeCheckResultForBoundarySurface(bs, root);
		}
	}

	private void writeErrorForCityObject(CityObject co, Section section) {
		Map<CheckId, CheckResult> results = co.getAllCheckResults();
		writeCheckResults(results.values(), section);
		for (Geometry geom : co.getGeometries()) {
			writeCheckResultForGeometry(geom, section);
		}
	}

	private void writeCheckResultForBoundarySurface(BoundarySurface bs, Section root) {
		Map<CheckId, CheckResult> results = bs.getAllCheckResults();
		writeCheckResults(results.values(), root);
		for (Opening o : bs.getOpenings()) {
			writeCheckResultForOpening(o, root);
		}
	}

	private void writeCheckResultForOpening(Opening o, Section root) {
		Map<CheckId, CheckResult> results = o.getAllCheckResults();
		writeCheckResults(results.values(), root);
	}

	private void writeCheckResultForGeometry(Geometry geom, Section root) {
		Map<CheckId, CheckResult> results = geom.getAllCheckResults();
		writeCheckResults(results.values(), root);
		for (Polygon poly : geom.getPolygons()) {
			writeCheckResultForPolygon(poly, root);
		}
	}

	private void writeCheckResultForPolygon(Polygon poly, Section root) {
		Map<CheckId, CheckResult> results = poly.getAllCheckResults();
		writeCheckResults(results.values(), root);
		writeCheckResultForLinearRing(poly.getExteriorRing(), root);
		for (LinearRing ring : poly.getInnerRings()) {
			writeCheckResultForLinearRing(ring, root);
		}
	}

	private void writeCheckResultForLinearRing(LinearRing ring, Section root) {
		Map<CheckId, CheckResult> results = ring.getAllCheckResults();
		writeCheckResults(results.values(), root);
	}

	private void writeCheckResults(Collection<CheckResult> results, Section root) {
		for (CheckResult cr : results) {
			if (cr.getResultStatus() == ResultStatus.ERROR) {
				writeError(root, cr.getError());
			}
		}
	}

	private void writeError(Section root, CheckError error) {
		String text = "Error " + error.getErrorId().toString() + ":";
		root.add12PtTextElement(text, 0);
		PdfErrorHandler handler = new PdfErrorHandler(root, config.getParserConfiguration());
		error.report(handler);
		root.addError(error);
	}

	private boolean containsError(CityObject co) {
		return co.containsAnyError();
	}

	@Override
	public void finishReport() throws CheckReportWriteException {
		for (Section s : buildings.getSubSections()) {
			if (!s.hasErrors()) {
				numOkBuildings++;
				// building has no errors, no table
				continue;
			}
			numErrorBuildings++;
			Table t = new Table(2);
			t.setTableColumnWidth(75, 25);
			t.setTitle("Error", "Count");
			for (Entry<String, AtomicInteger> e : s.getStats().getErrorCounts().entrySet()) {
				t.addRow(e.getKey(), e.getValue().toString());
			}
			s.addTable(1, t);
		}
		int numBuildings = numErrorBuildings + numOkBuildings;
		if (numBuildings > 0) {
			statistics.addDistributionBar("Buildings", numErrorBuildings, numOkBuildings);
		}

		int numVegetation = numErrorVegetation + numOkVegetation;
		if (numVegetation > 0) {
			statistics.addDistributionBar("Vegetation", numErrorVegetation, numOkVegetation);
		}

		int numtr = numErrorTrans + numOkTrans;
		if (numtr > 0) {
			statistics.addDistributionBar("Transportation Objects", numErrorTrans, numOkTrans);
		}

		int numbri = numErrorBridge + numOkBridge;
		if (numbri > 0) {
			statistics.addDistributionBar("Bridge Objects", numErrorBridge, numOkBridge);
		}

		int numWater = numErrorWater + numOkWater;
		if (numWater > 0) {
			statistics.addDistributionBar("Water Objects", numErrorWater, numOkWater);
		}

		int numLand = numErrorLand + numOkLand;
		if (numLand > 0) {
			statistics.addDistributionBar("Land Objects", numErrorLand, numOkLand);
		}

		statistics.addTextElement("Error Statistics:");
		for (Entry<ErrorId, AtomicInteger> e : errorStatistics.entrySet()) {
			statistics.add10PtTextElement(e.getKey().toString() + ": " + e.getValue().intValue(), 10);
		}
		report.save(outFile);
	}

	@Override
	public void reportGlobalError(CheckError err) {
		AtomicInteger errorCount = errorStatistics.computeIfAbsent(err.getErrorId(), k -> new AtomicInteger(0));
		errorCount.incrementAndGet();

		if (globalErrors == null) {
			globalErrors = vr.createSubSection("Global Errors");
		}

		String text = "Global error " + err.getErrorId();
		globalErrors.add10PtTextElement(text, 10);
		if (err instanceof SchematronError) {
			text = ((SchematronError) err).getText();
			globalErrors.add10PtTextElement(text, 20);
		}
	}

	@Override
	public void addError(String gmlId, CheckError err) {
		AtomicInteger errorCount = errorStatistics.computeIfAbsent(err.getErrorId(), k -> new AtomicInteger(0));
		errorCount.incrementAndGet();
		Section section = sectionMap.get(gmlId);
		if (section == null) {
			logger.warn("No section found for gml id: {}, adding to global errors instead", gmlId);
			reportGlobalError(err);
		} else {
			section.setHeadlineColor(ERROR_COLOR);
			writeError(section, err);
			section.addError(err);
		}
	}

}
