/*-
 *  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.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.citygml4j.factory.GMLGeometryFactory;
import org.citygml4j.model.citygml.core.CityModel;
import org.w3c.dom.Document;

import de.hft.stuttgart.citydoctor2.check.error.AttributeMissingError;
import de.hft.stuttgart.citydoctor2.check.error.AttributeValueWrongError;
import de.hft.stuttgart.citydoctor2.check.error.SchematronError;
import de.hft.stuttgart.citydoctor2.checkresult.utility.CheckReportWriteException;
import de.hft.stuttgart.citydoctor2.checks.CheckContainer;
import de.hft.stuttgart.citydoctor2.checks.CheckPrototype;
import de.hft.stuttgart.citydoctor2.checks.Checks;
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.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.FeatureType;
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.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.parser.ProgressListener;
import de.hft.stuttgart.citydoctor2.reporting.Reporter;
import de.hft.stuttgart.citydoctor2.reporting.StreamReporter;
import de.hft.stuttgart.citydoctor2.reporting.XmlStreamReporter;
import de.hft.stuttgart.citydoctor2.reporting.XmlValidationReporter;
import de.hft.stuttgart.citydoctor2.reporting.pdf.PdfReporter;
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.Validation;
import de.hft.stuttgart.quality.model.jaxb.Checking;
import de.hft.stuttgart.quality.model.jaxb.ErrorStatistics;
import de.hft.stuttgart.quality.model.jaxb.FeatureStatistics;
import de.hft.stuttgart.quality.model.jaxb.Parameter;
import de.hft.stuttgart.quality.model.jaxb.Requirement;
import de.hft.stuttgart.quality.model.jaxb.RequirementId;
import de.hft.stuttgart.quality.model.jaxb.Statistics;
import de.hft.stuttgart.quality.model.jaxb.TopLevelFeatureType;
import de.hft.stuttgart.quality.model.jaxb.ValidationPlan;
import net.sf.saxon.s9api.DOMDestination;
import net.sf.saxon.s9api.Destination;
import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.SAXDestination;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.XsltCompiler;
import net.sf.saxon.s9api.XsltExecutable;
import net.sf.saxon.s9api.XsltTransformer;

/**
 * The main container class for checking. It contains the logic for validation,
 * as well as contains the state of the checks performed.
 * 
 * @author Matthias Betz
 *
 */
public class Checker {

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

	private ValidationConfiguration config;
	private List<List<Check>> execLayers;

	private List<Filter> includeFilters;
	private List<Filter> excludeFilters;

	private Checks checkConfig;
	private CityDoctorModel model;

	public Checker(CityDoctorModel model) {
		this(ValidationConfiguration.loadStandardValidationConfig(), model);
	}

	public Checker(ValidationConfiguration config, CityDoctorModel model) {
		this.model = model;
		checkConfig = new Checks();
		setValidationConfig(config);
	}

	public Checks getChecks() {
		return checkConfig;
	}

	public CityDoctorModel getModel() {
		return model;
	}

	/**
	 * Write the xml report for the given CityDoctorModel. If no report location is
	 * given or this checker has not validated anything, nothing is done.
	 * 
	 * @param xmlOutput the output file location for the XML report. Can be null.
	 * @param model     the model for which the report is written.
	 */
	public void writeXmlReport(String xmlOutput) {
		if (!model.isValidated() || xmlOutput == null) {
			return;
		}
		File xmlFile = new File(xmlOutput);
		if (xmlFile.getParentFile() == null) {
			xmlFile.getParentFile().mkdirs();
		}
		Reporter reporter = new XmlValidationReporter();
		try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(xmlFile.getAbsolutePath()))) {
			reporter.writeReport(checkConfig, bos, model, config);
		} catch (CheckReportWriteException | IOException e) {
			logger.error(Localization.getText("Checker.failXml"), e);
		}
	}

	public void writePdfReport(String pdfOutput) {
		if (!model.isValidated() || pdfOutput == null) {
			return;
		}
		File pdfFile = new File(pdfOutput);
		if (pdfFile.getParentFile() == null) {
			pdfFile.getParentFile().mkdirs();
		}
		Reporter reporter = new PdfReporter("assets/Logo.png");
		try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(pdfFile.getAbsolutePath()))) {
			reporter.writeReport(checkConfig, bos, model, config);
		} catch (IOException | CheckReportWriteException e) {
			logger.error(Localization.getText("Checker.failPdf"), e);
		}
	}

	public void runChecks() {
		runChecks((ProgressListener) null);
	}

	public void runChecks(String xmlOutput) {
		runChecks();
		writeXmlReport(xmlOutput);
	}

	public void runChecks(String xmlOutput, String pdfOutput) {
		runChecks();
		writeXmlReport(xmlOutput);
		writePdfReport(pdfOutput);
	}

	public void runChecks(String xmlOutput, String pdfOutput, ProgressListener l) {
		runChecks(l);
		writeXmlReport(xmlOutput);
		writePdfReport(pdfOutput);
	}

	public void runChecks(ProgressListener l) {
		if (config == null) {
			config = ValidationConfiguration.loadStandardValidationConfig();
		}
		checkCityModel(model, l);
		if (logger.isInfoEnabled()) {
			logger.info(Localization.getText("Checker.checksFinished"));
		}
		SvrlContentHandler handler = executeSchematronValidationIfAvailable(config, model.getFile());
		if (handler != null) {
			handleSchematronResults(handler);
		}
		model.setValidated(createValidationPlan());
	}

	private void handleSchematronResults(SvrlContentHandler handler) {
		model.addGlobalErrors(handler.getGeneralErrors());
		Map<String, CityObject> featureMap = new HashMap<>();
		model.createFeatureStream().forEach(f -> featureMap.put(f.getGmlId().getGmlString(), f));
		handler.getFeatureErrors().forEach((k, v) -> {
			if (k.trim().isEmpty()) {
				// missing gml id, ignore?
				return;
			}
			CityObject co = featureMap.get(k);
			if (co == null) {
				// gml id reported by schematron was not found, add to general errors
				for (SchematronError se : v) {
					model.addGlobalError(se);
				}
			} else {
				handleSchematronErrorsForCityObject(v, co);
			}
		});
	}

	private static void handleSchematronErrorsForCityObject(List<SchematronError> v, CityObject co) {
		int count = 0;
		for (SchematronError se : v) {
			CheckError err;
			if (AttributeMissingError.ID.getIdString().equals(se.getErrorIdString())) {
				err = new AttributeMissingError(co, se.getChildId(), se.getNameOfAttribute(), se.isGeneric());
			} else if (AttributeValueWrongError.ID.getIdString().equals(se.getErrorIdString())) {
				err = new AttributeValueWrongError(co, se.getChildId(), se.getNameOfAttribute(), se.isGeneric());
			} else {
				throw new IllegalStateException(
						"Unknown error ID was given in schematron file: " + se.getErrorIdString());
			}
			co.addCheckResult(new CheckResult(new CheckId("" + count), ResultStatus.ERROR, err));
			count++;
		}
	}

	private ValidationPlan createValidationPlan() {
		ValidationPlan plan = new ValidationPlan();
		List<Checking> filter = createFilter();

		for (Entry<CheckId, CheckConfiguration> e : config.getChecks().entrySet()) {
			RequirementId reqId = mapToRequirement(e.getKey());
			if (reqId == null) {
				continue;
			}
			Requirement req = new Requirement();
			req.setName(reqId);
			req.setEnabled(e.getValue().isEnabled());
			plan.getRequirements().add(req);
			CheckPrototype proto = Checks.getCheckPrototypeForId(e.getKey());
			Map<String, String> parameters = e.getValue().getParameters();
			if (parameters != null) {
				for (Entry<String, String> param : parameters.entrySet()) {
					Parameter p = new Parameter();
					DefaultParameter defaultP = getDefaultParameter(param.getKey(), proto);
					if (defaultP != null) {
						p.setUom(defaultP.getUnitType().getGmlRepresentation());
					}
					p.setName(param.getKey());
					p.setValue(param.getValue());
					req.getParameters().add(p);
				}
			}
		}

		Requirement missing = new Requirement();
		missing.setName(RequirementId.R_SE_ATTRIBUTES_EXISTING);
		Requirement correct = new Requirement();
		correct.setName(RequirementId.R_SE_ATTRIBUTES_CORRECT);
		missing.setEnabled(config.getSchematronFilePath() != null);
		correct.setEnabled(config.getSchematronFilePath() != null);
		plan.getRequirements().add(missing);
		plan.getRequirements().add(correct);

		plan.getFilter().addAll(filter);
		Parameter numRounding = new Parameter();
		numRounding.setName("numberOfRoundingPlaces");
		numRounding.setValue("" + config.getNumberOfRoundingPlaces());
		Parameter minVertexDistance = new Parameter();
		minVertexDistance.setName("minVertexDistance");
		minVertexDistance.setUom("m");
		minVertexDistance.setValue("" + config.getMinVertexDistance());
		Parameter schematronFile = new Parameter();
		schematronFile.setName("schematronFile");
		schematronFile.setValue(config.getSchematronFilePath());
		plan.getGlobalParameters().add(numRounding);
		plan.getGlobalParameters().add(minVertexDistance);
		plan.getGlobalParameters().add(schematronFile);
		return plan;
	}

	private DefaultParameter getDefaultParameter(String key, CheckPrototype proto) {
		for (DefaultParameter param : proto.getDefaultParameter()) {
			if (param.getName().equals(key)) {
				return param;
			}
		}
		return null;
	}

	private List<Checking> createFilter() {
		List<Checking> filter = new ArrayList<>();
		handleInputFilter(filter);
		if (excludeFilters != null) {
			for (Filter f : excludeFilters) {
				if (f instanceof TypeFilter) {
					TypeFilter tf = (TypeFilter) f;
					FeatureType type = tf.getType();
					TopLevelFeatureType tlft = mapToTopLevelFeatureType(type);
					if (tlft == null) {
						continue;
					}
					removeFilter(tlft, filter);
				}
			}
		}
		return filter;
	}

	private void handleInputFilter(List<Checking> filter) {
		if (includeFilters == null || includeFilters.isEmpty()) {
			// no filter means, use all
			addAllFilters(filter);
		} else {
			for (Filter f : includeFilters) {
				if (f instanceof TypeFilter) {
					TypeFilter tf = (TypeFilter) f;
					FeatureType type = tf.getType();
					TopLevelFeatureType tlft = mapToTopLevelFeatureType(type);
					if (tlft == null) {
						continue;
					}
					filter.add(new Checking(tlft));
				}
			}
			if (filter.isEmpty()) {
				// this happens if no type include filter was used
				// it is possible only single objects were tested then
				// so include everything
				addAllFilters(filter);
			}
		}
	}

	private void addAllFilters(List<Checking> filter) {
		filter.add(new Checking(TopLevelFeatureType.BUILDING));
		filter.add(new Checking(TopLevelFeatureType.BRIDGE));
		filter.add(new Checking(TopLevelFeatureType.LAND));
		filter.add(new Checking(TopLevelFeatureType.TRANSPORTATION));
		filter.add(new Checking(TopLevelFeatureType.VEGETATION));
		filter.add(new Checking(TopLevelFeatureType.WATER));
	}

	private void removeFilter(TopLevelFeatureType tlft, List<Checking> filter) {
		for (Checking c : filter) {
			if (c.getValue().equals(tlft)) {
				filter.remove(c);
				return;
			}
		}
	}

	private TopLevelFeatureType mapToTopLevelFeatureType(FeatureType type) {
		switch (type) {
		case BRIDGE:
			return TopLevelFeatureType.BRIDGE;
		case BUILDING:
			return TopLevelFeatureType.BUILDING;
		case LAND:
			return TopLevelFeatureType.LAND;
		case TRANSPORTATION:
			return TopLevelFeatureType.TRANSPORTATION;
		case VEGETATION:
			return TopLevelFeatureType.VEGETATION;
		case WATER:
			return TopLevelFeatureType.WATER;
		default:
			return null;
		}
	}

	private RequirementId mapToRequirement(CheckId key) {
		switch (key.getName()) {
		case "C_GE_R_TOO_FEW_POINTS":
			return RequirementId.R_GE_R_TOO_FEW_POINTS;
		case "C_GE_R_NOT_CLOSED":
			return RequirementId.R_GE_R_NOT_CLOSED;
		case "C_GE_R_DUPLICATE_POINT":
			return RequirementId.R_GE_R_CONSECUTIVE_POINTS_SAME;
		case "C_GE_R_SELF_INTERSECTION":
			return RequirementId.R_GE_R_SELF_INTERSECTION;
		case "C_GE_P_INTERIOR_DISCONNECTED":
			return RequirementId.R_GE_P_INTERIOR_DISCONNECTED;
		case "C_GE_P_INTERSECTING_RINGS":
			return RequirementId.R_GE_P_INTERSECTING_RINGS;
		case "C_GE_P_NON_PLANAR":
			return RequirementId.R_GE_P_NON_PLANAR;
		case "C_GE_S_TOO_FEW_POLYGONS":
			return RequirementId.R_GE_S_TOO_FEW_POLYGONS;
		case "C_GE_S_NON_MANIFOLD_EDGE":
			return RequirementId.R_GE_S_NON_MANIFOLD_EDGE;
		case "C_GE_S_POLYGON_WRONG_ORIENTATION":
			return RequirementId.R_GE_S_POLYGON_WRONG_ORIENTATION;
		case "C_GE_S_ALL_POLYGONS_WRONG_ORIENTATION":
			return RequirementId.R_GE_S_ALL_POLYGONS_WRONG_ORIENTATION;
		case "C_GE_S_NON_MANIFOLD_VERTEX":
			return RequirementId.R_GE_S_NON_MANIFOLD_VERTEX;
		case "C_GE_S_SELF_INTERSECTION":
			return RequirementId.R_GE_S_SELF_INTERSECTION;
		case "C_GE_P_HOLE_OUTSIDE":
			return RequirementId.R_GE_P_HOLE_OUTSIDE;
		case "C_GE_P_INNER_RINGS_NESTED":
			return RequirementId.R_GE_P_INNER_RINGS_NESTED;
		case "C_GE_S_NOT_CLOSED":
			return RequirementId.R_GE_S_NOT_CLOSED;
		case "C_GE_S_MULTIPLE_CONNECTED_COMPONENTS":
			return RequirementId.R_GE_S_MULTIPLE_CONNECTED_COMPONENTS;
		default:
			return null;
		}
	}

	public ValidationConfiguration getConfig() {
		return config;
	}

	public static SvrlContentHandler executeSchematronValidationIfAvailable(ValidationConfiguration config, File file) {
		if (config.getSchematronFilePath() != null && !config.getSchematronFilePath().isEmpty()) {
			if (logger.isInfoEnabled()) {
				logger.info(Localization.getText("Checker.schematronValidation"));
			}
			Processor processor = new Processor(false);
			XsltCompiler xsltCompiler = processor.newXsltCompiler();
			xsltCompiler.setURIResolver(new URIResolver() {

				@Override
				public Source resolve(String href, String base) throws TransformerException {
					return new StreamSource(Checker.class.getResourceAsStream(href));
				}

			});
			try {
				XsltExecutable includeExecutable = xsltCompiler
						.compile(new StreamSource(Checker.class.getResourceAsStream("iso_dsdl_include.xsl")));
				XsltTransformer includeTransformer = includeExecutable.load();
				includeTransformer.setSource(new StreamSource(new File(config.getSchematronFilePath())));

				XsltExecutable expandExecutable = xsltCompiler
						.compile(new StreamSource(Checker.class.getResourceAsStream("iso_abstract_expand.xsl")));
				XsltTransformer expandTransformer = expandExecutable.load();
				includeTransformer.setDestination(expandTransformer);

				XsltExecutable xslt2Executable = xsltCompiler
						.compile(new StreamSource(Checker.class.getResourceAsStream("iso_svrl_for_xslt2.xsl")));

				XsltTransformer xslt2Transformer = xslt2Executable.load();
				expandTransformer.setDestination(xslt2Transformer);

				DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
				factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
				Document doc = factory.newDocumentBuilder().newDocument();
				DOMDestination domDestination = new DOMDestination(doc);
				xslt2Transformer.setDestination(domDestination);
				includeTransformer.transform();

				XsltExecutable schematronExecutable = xsltCompiler.compile(new DOMSource(doc));
				XsltTransformer schematronTransformer = schematronExecutable.load();
				schematronTransformer.setSource(new StreamSource(file));
				SvrlContentHandler handler = new SvrlContentHandler();
				Destination dest = new SAXDestination(handler);
				schematronTransformer.setDestination(dest);
				schematronTransformer.transform();
				if (logger.isInfoEnabled()) {
					logger.info(Localization.getText("Checker.finishedSchematron"));
				}
				return handler;
			} catch (SaxonApiException | ParserConfigurationException e) {
				logger.catching(e);
			}
		}
		return null;
	}

	private void buildFilters() {
		FilterConfiguration filterConfig = config.getFilter();
		if (filterConfig == null) {
			includeFilters = Collections.emptyList();
			excludeFilters = Collections.emptyList();
			return;
		}
		excludeFilters = buildExcludeFilters(filterConfig);
		includeFilters = buildIncludeFilters(filterConfig);
	}

	private List<Filter> buildExcludeFilters(FilterConfiguration filterConfig) {
		if (filterConfig == null) {
			return Collections.emptyList();
		}
		ExcludeFilterConfiguration excludeConfig = filterConfig.getExclude();
		if (excludeConfig == null) {
			return Collections.emptyList();
		} else {
			List<Filter> filters = new ArrayList<>();
			if (excludeConfig.getTypes() != null) {
				for (FeatureType excludeType : excludeConfig.getTypes()) {
					filters.add(new TypeFilter(excludeType));
				}
			}
			if (excludeConfig.getIds() != null) {
				for (String excludePattern : excludeConfig.getIds()) {
					Filter f = new EqualsIgnoreCaseFilter(excludePattern);
					filters.add(f);
				}
			}
			return filters;
		}
	}

	private List<Filter> buildIncludeFilters(FilterConfiguration filterConfig) {
		if (filterConfig == null) {
			return Collections.emptyList();
		}
		IncludeFilterConfiguration includeConfig = filterConfig.getInclude();
		if (includeConfig == null) {
			return Collections.emptyList();
		} else {
			List<Filter> filters = new ArrayList<>();
			if (includeConfig.getTypes() != null) {
				for (FeatureType includeType : includeConfig.getTypes()) {
					filters.add(new TypeFilter(includeType));
				}
			}
			if (includeConfig.getIds() != null) {
				for (String includePattern : includeConfig.getIds()) {
					Filter f = new EqualsIgnoreCaseFilter(includePattern);
					filters.add(f);
				}
			}
			return filters;
		}
	}

	private void setValidationConfig(ValidationConfiguration config) {
		if (config == null) {
			throw new IllegalArgumentException("Validation configuration may not be null");
		}
		this.config = config;
		buildFilters();
		ParserConfiguration parserConfig = config.getParserConfiguration();
		List<Check> checks = collectEnabledChecksAndInit(parserConfig, config);
		execLayers = buildExecutionLayers(checks);
	}

	private List<Check> collectEnabledChecksAndInit(ParserConfiguration parserConfig, ValidationConfiguration config) {
		List<Check> checks = new ArrayList<>();
		for (Entry<CheckId, CheckConfiguration> e : config.getChecks().entrySet()) {
			if (e.getValue().isEnabled()) {
				Check c = checkConfig.getCheckForId(e.getKey());
				Map<String, String> parameters = new HashMap<>();
				parameters.putAll(e.getValue().getParameters());
				parameters.put("numberOfRoundingPlaces", "" + config.getNumberOfRoundingPlaces());
				parameters.put("minVertexDistance", "" + config.getMinVertexDistance());
				// initialize checks with parameters
				c.init(parameters, parserConfig);
				checks.add(c);
			}
		}
		return checks;
	}

	private void checkCityModel(CityDoctorModel model, ProgressListener l) {
		Stream<CityObject> features = model.createFeatureStream();
		float featureSum = model.getNumberOfFeatures();
		// stupid lamda with final variable restrictions
		int[] currentFeature = new int[1];
		features.forEach(co -> {
			if (config.getParserConfiguration().useLowMemoryConsumption()) {
				// no edges have been created yet, create them
				co.prepareForChecking();
			}
			// check every feature
			executeChecksForCityObject(co);

			if (config.getParserConfiguration().useLowMemoryConsumption()) {
				// low memory consumption, remove edges again
				co.clearMetaInformation();
			}
			if (l != null) {
				currentFeature[0]++;
				l.updateProgress(currentFeature[0] / featureSum);
			}
		});
	}

	private boolean filterObject(CityObject co) {
		return isObjectIncluded(co, includeFilters, excludeFilters);
	}

	private boolean isObjectIncluded(CityObject co, List<Filter> includeFilters, List<Filter> excludeFilters) {
		if (!includeFilters.isEmpty()) {
			boolean include = false;
			for (Filter f : includeFilters) {
				if (f.matches(co)) {
					include = true;
					break;
				}
			}
			if (!include) {
				// not included, ignore
				return false;
			}
		}
		// check if object is excluded
		for (Filter f : excludeFilters) {
			if (f.matches(co)) {
				// exclude object
				return false;
			}
		}
		return true;
	}

	/**
	 * Checks the city object if it has not been removed by the filters. The check
	 * result are stored into the city object itself.
	 * 
	 * @param co the city object that is going to be checked
	 */
	private void executeChecksForCityObject(CityObject co) {
		if (!filterObject(co)) {
			return;
		}
		executeChecksForCheckable(co);
	}

	/**
	 * Executes all checks for the checkable. This will bypass the filters.
	 * 
	 * @param co the checkable.
	 */
	public void executeChecksForCheckable(Checkable co) {
		// throw away old results
		co.clearAllContainedCheckResults();
		if (logger.isDebugEnabled()) {
			logger.debug(Localization.getText("Checker.checkFeature"), co);
		}
		for (int i = 0; i < execLayers.size(); i++) {
			for (Check check : execLayers.get(i)) {
				if (logger.isTraceEnabled()) {
					logger.trace(Localization.getText("Checker.executeCheck"), check.getCheckId());
				}
				co.accept(check);
			}
		}
	}

	public static List<List<Check>> buildExecutionLayers(List<Check> checks) {
		List<List<Check>> result = new ArrayList<>();

		Set<Check> availableChecks = new HashSet<>(checks);
		Set<CheckId> usedChecks = new HashSet<>();

		while (!availableChecks.isEmpty()) {
			List<Check> layer = new ArrayList<>();
			Iterator<Check> iterator = availableChecks.iterator();
			while (iterator.hasNext()) {
				Check c = iterator.next();
				boolean hasUnusedDependency = searchForUnusedDependency(usedChecks, c);
				if (!hasUnusedDependency) {
					iterator.remove();
					layer.add(new CheckContainer(c));
				}
			}
			if (layer.isEmpty()) {
				throw new IllegalStateException(
						"There are checks that have dependencies that are not executed or are unknown");
			}
			result.add(layer);
			for (Check c : layer) {
				usedChecks.add(c.getCheckId());
			}
		}
		return result;
	}

	private static boolean searchForUnusedDependency(Set<CheckId> usedChecks, Check c) {
		boolean hasUnusedDependency = false;
		for (CheckId id : c.getDependencies()) {
			if (!usedChecks.contains(id)) {
				hasUnusedDependency = true;
				break;
			}
		}
		return hasUnusedDependency;
	}

	public static void streamCheck(File inputFile, String xmlOutput, String pdfOutput, ValidationConfiguration config,
			String outputFile) throws IOException, CityGmlParseException {
		streamCheck(inputFile, xmlOutput, pdfOutput, config, "assets/Logo.png", null, outputFile);
	}

	public static void streamCheck(File inputFile, String xmlOutput, String pdfOutput, ValidationConfiguration config,
			String logoLocation, FeatureCheckedListener l, String outputFile)
			throws IOException, CityGmlParseException {
		try (BufferedOutputStream xmlBos = getXmlOutputMaybe(xmlOutput);
				BufferedOutputStream pdfBos = getPdfOutputMaybe(pdfOutput)) {
			Checker c = new Checker(config, null);
			String fileName = inputFile.getName();
			
			// create reporter if available
			XmlStreamReporter xmlReporter = getXmlReporter(config, xmlBos, fileName);
			PdfStreamReporter pdfReporter = getPdfReporter(config, logoLocation, pdfBos, fileName);
			
			// create quality ade structures
			Validation val = new Validation();
			val.setValidationDate(ZonedDateTime.now());
			val.setValidationSoftware("CityDoctor " + Localization.getText(Localization.VERSION));
			Statistics statistics = new Statistics();
			FeatureStatistics buildingStatistics = new FeatureStatistics();
			statistics.setNumErrorBuildings(buildingStatistics);
			FeatureStatistics bridgeStatistics = new FeatureStatistics();
			statistics.setNumErrorBridgeObjects(bridgeStatistics);
			FeatureStatistics transportationStatistics = new FeatureStatistics();
			statistics.setNumErrorTransportation(transportationStatistics);
			FeatureStatistics vegetationStatistics = new FeatureStatistics();
			statistics.setNumErrorVegetation(vegetationStatistics);
			FeatureStatistics landStatistics = new FeatureStatistics();
			statistics.setNumErrorLandObjects(landStatistics);
			FeatureStatistics waterStatistics = new FeatureStatistics();
			statistics.setNumErrorWaterObjects(waterStatistics);
			
			// map for counting individual error counts
			Map<ErrorId, AtomicInteger> errorCount = new HashMap<>();
			GMLGeometryFactory gmlFactory = new GMLGeometryFactory();
			
			// execute schematron first
			SvrlContentHandler handler = executeSchematronValidationIfAvailable(config, inputFile);
			
			CityGmlConsumer con = new CityGmlConsumer() {
				@Override
				public void accept(CityObject co) {
					c.checkFeature(xmlReporter, pdfReporter, co);
					
					if (handler != null) {
						List<SchematronError> errors = handler.getFeatureErrors().get(co.getGmlId().getGmlString());
						if (errors != null) {
							handleSchematronErrorsForCityObject(errors, co);
						}
					}
					
					// remove existing quality ade datastructure if existing
					QualityADEUtils.removeValidationResult(co);
					// store quality ade datastructures in cityobject
					QualityADEUtils.writeQualityAde(co);
					// 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) {
					QualityADEUtils.removeValidation(cm);
					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);
					}
					val.setStatistics(statistics);
					val.setValidationPlan(c.createValidationPlan());

					cm.addGenericApplicationPropertyOfCityModel(val);
				}
			};
			
			// parse and validate
			CityGmlParser.streamCityGml(inputFile, config.getParserConfiguration(), con, outputFile);
			
			// write reports if available
			writeReport(xmlReporter, handler);
			writeReport(pdfReporter, handler);
		} catch (CheckReportWriteException e) {
			logger.error(Localization.getText("Checker.failReports"), e);
		}
	}
	
	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);
		}
	}

	private static XmlStreamReporter getXmlReporter(ValidationConfiguration config, BufferedOutputStream xmlBos,
			String fileName) {
		XmlStreamReporter xmlReporter;
		if (xmlBos != null) {
			xmlReporter = new XmlStreamReporter(xmlBos, fileName, config);
		} else {
			xmlReporter = null;
		}
		return xmlReporter;
	}

	private static PdfStreamReporter getPdfReporter(ValidationConfiguration config, String logoLocation,
			BufferedOutputStream pdfBos, String fileName) {
		PdfStreamReporter pdfReporter;
		if (pdfBos != null) {
			pdfReporter = new PdfStreamReporter(pdfBos, fileName, config, logoLocation);
		} else {
			pdfReporter = null;
		}
		return pdfReporter;
	}

	public static void writeReport(StreamReporter reporter, SvrlContentHandler handler)
			throws CheckReportWriteException {
		if (reporter != null) {
			if (handler != null) {
				for (SchematronError err : handler.getGeneralErrors()) {
					reporter.reportGlobalError(err);
				}
				for (Entry<String, List<SchematronError>> e : handler.getFeatureErrors().entrySet()) {
					for (SchematronError se : e.getValue()) {
						reporter.addError(e.getKey(), se);
					}
				}
			}
			reporter.finishReport();
		}
	}

	public static BufferedOutputStream getPdfOutputMaybe(String pdfOutput) throws FileNotFoundException {
		return pdfOutput != null ? new BufferedOutputStream(new FileOutputStream(pdfOutput)) : null;
	}

	public static BufferedOutputStream getXmlOutputMaybe(String xmlOutput) throws FileNotFoundException {
		return xmlOutput != null ? new BufferedOutputStream(new FileOutputStream(xmlOutput)) : null;
	}

	public void checkFeature(XmlStreamReporter xmlReporter, PdfStreamReporter pdfReporter, CityObject co) {
		if (logger.isDebugEnabled()) {
			logger.debug(Localization.getText("Checker.checkFeature"), co);
		}
		executeChecksForCityObject(co);
		if (xmlReporter != null) {
			xmlReporter.report(co);
		}
		if (pdfReporter != null) {
			pdfReporter.report(co);
		}
	}
}
