/**
 * Copyright © CENRA (2016)
 *
 * remi.clement@espaces-naturels.fr
 *
 * This software is a multi-platform and portable application based on
 * the scientifical toolbox to monitor wetlands. With this toolbox it’s
 * possible to calculate indicators «RhoMéO» to monitor quality,
 * fonctionalities and urban and agricultural pressures of wetlands.
 * Indicators are calculated using naturalist data entered and/or imported,
 * and geographic data downloaded from a geographic server.
 *
 * This software is governed by the CeCILL license under French law and
 * abiding by the rules of distribution of free software.  You can  use,
 * modify and/ or redistribute the software under the terms of the CeCILL
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * "http://www.cecill.info".
 *
 * As a counterpart to the access to the source code and  rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty  and the software's author,  the holder of the
 * economic rights,  and the successive licensors  have only  limited
 * liability.
 *
 * In this respect, the user's attention is drawn to the risks associated
 * with loading,  using,  modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean  that it is complicated to manipulate,  and  that  also
 * therefore means  that it is reserved for developers  and  experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or
 * data to be ensured and,  more generally, to use and operate it in the
 * same conditions as regards security.
 *
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.core.util;

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.impl.CoordinateArraySequence;
import fr.cenra.rhomeo.api.IdentifiedObject;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.list.HumidZoneType;
import fr.cenra.rhomeo.core.list.OdonateZoneBio;
import fr.cenra.rhomeo.core.list.OrthoptereZoneBio;
import fr.cenra.rhomeo.core.data.site.SiteImpl;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.zip.ZipOutputStream;
import javax.xml.stream.XMLStreamException;

import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.atom.AtomFactory;
import org.geotoolkit.atom.DefaultAtomFactory;
import org.geotoolkit.atom.xml.AtomConstants;
import org.geotoolkit.data.FeatureStoreUtilities;
import org.geotoolkit.data.geojson.GeoJSONFeatureStore;
import org.geotoolkit.data.kml.DefaultKmlFactory;
import org.geotoolkit.data.kml.KmlFactory;
import org.geotoolkit.data.kml.model.AbstractGeometry;
import org.geotoolkit.data.kml.model.Boundary;
import org.geotoolkit.data.kml.model.Kml;
import org.geotoolkit.data.kml.model.KmlException;
import org.geotoolkit.data.kml.model.LinearRing;
import org.geotoolkit.data.kml.xml.KmlConstants;
import org.geotoolkit.data.kml.xml.KmlWriter;
import org.geotoolkit.data.kml.xsd.SimpleTypeContainer;
import org.geotoolkit.data.shapefile.ShapefileFeatureStore;
import org.geotoolkit.feature.Feature;
import org.geotoolkit.feature.type.FeatureType;
import org.geotoolkit.geometry.jts.JTS;
import org.geotoolkit.referencing.CRS;
import org.geotoolkit.xal.xml.XalConstants;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import java.util.Set;
import javax.validation.ConstraintViolation;
import org.geotoolkit.feature.FeatureTypeBuilder;
import org.geotoolkit.feature.FeatureUtilities;
import org.geotoolkit.nio.ZipUtilities;
import static fr.cenra.rhomeo.core.data.site.SiteRepositoryImpl.toFeature;
import java.io.OutputStream;
import java.net.URI;
import java.util.Iterator;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import org.apache.sis.referencing.CommonCRS;
import org.geotoolkit.data.FeatureStore;
import org.geotoolkit.data.geojson.GeoJSONStreamWriter;
import org.geotoolkit.data.query.QueryBuilder;
import org.geotoolkit.feature.FeatureBuilder;
import org.geotoolkit.feature.GeometryAttribute;
import org.geotoolkit.nio.IOUtilities;
import org.opengis.feature.Property;
import org.opengis.util.GenericName;

/**
 * Export utilities.
 *
 * @author Samuel Andrés (Geomatys)
 * @author Cédric Briançon (Geomatys)
 */
public class ExportUtils {

    public static final String SITE_JSON = RhomeoCore.SITES.concat(".geojson");

    public static final String METADATA_JSON = "metadata.json";

    public static final String RHOMEO_PREFIX = "rhomeo";

    public static final KmlFactory KML_FACTORY = DefaultKmlFactory.getInstance();

    public static final AtomFactory ATOM_FACTORY = DefaultAtomFactory.getInstance();

    public static MathTransform TRANSFORM;
    static {
        try {
            // Set transform from site CRS to .kml CRS.
            TRANSFORM = CRS.findMathTransform(RhomeoCore.getSiteCRS(), CommonCRS.WGS84.normalizedGeographic(), true);
        } catch (FactoryException ex) {
            RhomeoCore.LOGGER.log(Level.WARNING, "Unable to build MathTransform to Kml export.", ex);
        }
    }

    public static enum ExportType {
        SHP("ESRI Shapefile", "shp"),
        KML("Google KML", "kml"),
        GJSON("GeoJSON", "json");

        public final String title;
        public final String extension;
        private ExportType(final String title, final String extension) {
            this.title = title;
            this.extension = extension;
        }

        @Override
        public String toString() {
            return title;
        }
    }

    /**
     * Builds a list of KML {@link AbstractGeometry} (WGS 84) from a {@link Site}
     * {@link MultiPolygon} (Lambert 93).
     *
     * @param multiPolygon The site Lambert 93 geometry.
     * @return A list of KML WGS 84 geometries mapping the site geometry.
     */
    static List<AbstractGeometry> toKmlGeometries(Geometry geom) throws TransformException {
        final List<AbstractGeometry> result;
        if (geom instanceof GeometryCollection) {
            final GeometryCollection col = (GeometryCollection) geom;
            result = new ArrayList<>();
            for (int i = 0 ; i < col.getNumGeometries() ; i++) {
                result.addAll(toKmlGeometries(col.getGeometryN(i)));
            }
        } else {
            AbstractGeometry converted = null;
            if (geom != null)
                geom = JTS.transform(geom, TRANSFORM);
            if (geom instanceof com.vividsolutions.jts.geom.Polygon) {
                final com.vividsolutions.jts.geom.Polygon currentPolygon = (com.vividsolutions.jts.geom.Polygon) geom;
                // Exterior ring
                final LinearRing extRing = KML_FACTORY.createLinearRing(new CoordinateArraySequence(currentPolygon.getExteriorRing().getCoordinates()));
                final Boundary extBoundary = KML_FACTORY.createBoundary(extRing, null, null);

                // Interior rings
                final List<Boundary> intBoundaries = new ArrayList<>();
                for (int j = 0; j < currentPolygon.getNumInteriorRing(); j++) {
                    final LinearRing intRing = KML_FACTORY.createLinearRing(new CoordinateArraySequence(currentPolygon.getInteriorRingN(j).getCoordinates()));
                    intBoundaries.add(KML_FACTORY.createBoundary(intRing, null, null));
                }
                converted = KML_FACTORY.createPolygon(extBoundary, intBoundaries);
            } else if (geom instanceof com.vividsolutions.jts.geom.LinearRing) {
                converted = KML_FACTORY.createLinearRing(((com.vividsolutions.jts.geom.LinearRing) geom).getCoordinateSequence());
            } else if (geom instanceof LineString) {
                converted = KML_FACTORY.createLineString(((com.vividsolutions.jts.geom.LineString) geom).getCoordinateSequence());
            } else if (geom instanceof Point) {
                converted = KML_FACTORY.createPoint(((Point) geom).getCoordinateSequence());
            }

            result = converted == null? Collections.EMPTY_LIST : Collections.singletonList(converted);
        }

        return result;
    }

    /**
     * Builds a description from some {@link Site} fields.
     *
     * @param countyCode
     * @param zoneType
     * @param odonateType
     * @param orthoptereType
     * @return
     */
    static String description(final String countyCode, final String zoneType, final String odonateType, final String orthoptereType, final String remarks){
        final StringBuilder sb = new StringBuilder();

        if (countyCode != null && !countyCode.isEmpty()) {
                sb.append(InternationalResource.getFormatedResourceString(SiteImpl.class, "countyCode.kml", countyCode));
        }

        if (zoneType != null) {
            final HumidZoneType zone = HumidZoneType.getByCode(zoneType);
            if (zone != null) {
                if (sb.length() > 0)
                    sb.append(System.lineSeparator());

                sb.append(InternationalResource.getFormatedResourceString(SiteImpl.class, "zoneType.kml", zone.getValue(), zone.getCode()));
            }
        }
        if (odonateType != null) {
            final OdonateZoneBio zone = OdonateZoneBio.getByCode(odonateType);
            if (zone != null) {
                if (sb.length() > 0)
                    sb.append(System.lineSeparator());

                sb.append(InternationalResource.getFormatedResourceString(SiteImpl.class, "odonateType.kml", zone.getValue(), zone.getCode()));
            }
        }
        if (orthoptereType != null) {
            final OrthoptereZoneBio zone = OrthoptereZoneBio.getByCode(orthoptereType);
            if (zone != null) {
                if (sb.length() > 0)
                    sb.append(System.lineSeparator());

                sb.append(InternationalResource.getFormatedResourceString(SiteImpl.class, "orthoptereType.kml", zone.getValue(), zone.getCode()));
            }
        }
        if (remarks != null && !remarks.isEmpty()) {
            if (sb.length() > 0)
                sb.append(System.lineSeparator());
            sb.append(InternationalResource.getFormatedResourceString(SiteImpl.class, "remarks.kml", remarks));
        }
        return sb.toString();
    }

    /**
     * Writes the given {@link Site} to the given output as .kml v2.2 format.
     *
     * @param site
     * @param output
     * @throws XMLStreamException
     * @throws IOException
     * @throws KmlException
     * @throws TransformException
     * @throws DataStoreException
     */
    public static void writeKml(final Site site, final Object output) throws XMLStreamException, IOException, KmlException, TransformException, DataStoreException{
        ExportUtils.writeKml(toFeature(site), output);
    }

    /**
     * Writes the given {@link Site} {@link Feature} to the given output as .kml v2.2 format.
     *
     * @param site
     * @param output
     * @throws XMLStreamException
     * @throws IOException
     * @throws KmlException
     * @throws org.opengis.referencing.operation.TransformException
     */
    public static void writeKml(final Feature site, final Object output) throws XMLStreamException, IOException, KmlException, TransformException {

        // Retrieve site data incompatible with KML base semantics.
        final Property county = site.getProperty(RhomeoCore.FT_PROPERTIES.COUNTY.name());
        final Property type = site.getProperty(RhomeoCore.FT_PROPERTIES.TYPE.name());
        final Property odonate = site.getProperty(RhomeoCore.FT_PROPERTIES.ODONATE.name());
        final Property orthoptere = site.getProperty(RhomeoCore.FT_PROPERTIES.ORTHOPTERE.name());
        final Property remarks = site.getProperty(RhomeoCore.FT_PROPERTIES.REMARKS.name());

        // Build containers for rhomeo specific semantics.
        final List<SimpleTypeContainer> rhomeoExtensions = new ArrayList<>();
        if (county != null)
            rhomeoExtensions.add(KML_FACTORY.createSimpleTypeContainer(RhomeoKmlWriter.NAMESPACE, RhomeoKmlWriter.TAG_COUNTY, county.getValue()));
        if (type != null)
            rhomeoExtensions.add(KML_FACTORY.createSimpleTypeContainer(RhomeoKmlWriter.NAMESPACE, RhomeoKmlWriter.TAG_TYPE, type.getValue()));
        if (odonate != null)
            rhomeoExtensions.add(KML_FACTORY.createSimpleTypeContainer(RhomeoKmlWriter.NAMESPACE, RhomeoKmlWriter.TAG_ODONATE, odonate.getValue()));
        if (orthoptere != null)
            rhomeoExtensions.add(KML_FACTORY.createSimpleTypeContainer(RhomeoKmlWriter.NAMESPACE, RhomeoKmlWriter.TAG_ORTHOPTERE, orthoptere.getValue()));

        final Property org = site.getProperty(RhomeoCore.FT_PROPERTIES.ORG.name());

        final GeometryAttribute geomProp = site.getDefaultGeometryProperty();
        final List<AbstractGeometry> geoms = geomProp == null || geomProp.getValue() == null? null : toKmlGeometries((Geometry)geomProp.getValue());

        final Property nameProp =  site.getProperty(RhomeoCore.FT_PROPERTIES.NAME.name());
        final String name;
        if (nameProp == null || nameProp.getValue() == null)
            name = null;
        else
            name = nameProp.getValue().toString();

        final String description;
        if (county != null || type != null || odonate != null || orthoptere != null || remarks != null)
            description = description(
                    county == null? null : String.valueOf(county.getValue()),
                    type == null? null : String.valueOf(type.getValue()),
                    odonate == null? null : String.valueOf(odonate.getValue()),
                    orthoptere == null? null : String.valueOf(orthoptere.getValue()),
                    remarks == null? null : String.valueOf(remarks.getValue()));
        else description = null;

        final Property referent = site.getProperty(RhomeoCore.FT_PROPERTIES.REFERENT.name());

        // Build placemark mapping site data.
        final Feature placemark = KML_FACTORY.createPlacemark(null,
                null,
                name,
                true,
                true,
                 name==null ? null : ATOM_FACTORY.createAtomPersonConstruct(Collections.singletonList(name)),
                null,
                name,
                null,
                null,
                null,
                description == null? null : description,
                null,
                null,
                null,
                new ArrayList<>(),
                null,
                null,
                null,
                null,
                KML_FACTORY.createMultiGeometry(null, null, null, null, geoms, null, null),
                rhomeoExtensions,
                null);

        final Kml kml = KML_FACTORY.createKml();

        // Set KML version
        kml.setVersion(KmlConstants.URI_KML_2_2);

        // Set prefixes for URIs.
        final Map<String, String> extensions = new HashMap<>();
        extensions.put(AtomConstants.URI_ATOM, KmlConstants.PREFIX_ATOM);
        extensions.put(XalConstants.URI_XAL, KmlConstants.PREFIX_XAL);
        extensions.put(RhomeoKmlWriter.NAMESPACE, RHOMEO_PREFIX);
        kml.setExtensionsUris(extensions);

        // Set site placemark
        kml.setAbstractFeature(placemark);

        final KmlWriter writer = new KmlWriter();
        try{
            writer.setOutput(output);
            writer.addExtensionWriter(RhomeoKmlWriter.NAMESPACE, new RhomeoKmlWriter());
            writer.write(kml);
        }
        finally{
            writer.dispose();
        }
    }

    /**
     * Writes the given {@Site} as a shapefile to the given output file.
     *
     * @param site
     * @param outputFile
     * @throws DataStoreException
     * @throws MalformedURLException
     */
    public static void writeShp(final Site site, final File outputFile) throws DataStoreException, MalformedURLException, IOException {
        ArgumentChecks.ensureNonNull("Output zip file", outputFile);
        final Feature toAdd = toFeature(site);
        write(toAdd, outputFile.toPath(), ExportType.SHP);
    }

    /**
     * Writes the selected site into a geojson file.
     *
     * @param site Selected site to serialize.
     * @param outputFolder Geojson in which to write the site.
     * @return The json path created.
     * @throws IOException
     * @throws DataStoreException
     */
    public static Path writeGeoJson(final Site site, final Path outputFolder) throws IOException, DataStoreException {
        final Path outputJson = outputFolder.resolve(SITE_JSON);
        Files.deleteIfExists(outputJson);
        final Path outputTypeJson = outputFolder.resolve(RhomeoCore.SITES +"_Type.json");
        Files.deleteIfExists(outputTypeJson);
        final Feature toAdd = toFeature(site);


        write(toAdd, outputJson, ExportType.GJSON);
        return outputJson;
    }

    public static void write(final Feature feature, final Path outputFile, final ExportType format) throws IOException, DataStoreException {
        switch (format) {
            case KML:
                try {
                    writeKml(feature, outputFile);
                    break;
                } catch (Exception e) {
                    throw new IOException(e);
                }
            case GJSON:
                // HACK : Force long/lat coordinates, because our writer uses
                // comas in big numbers (Ex: 10000 --> 10,000), which breaks format.
                FeatureStoreUtilities.collection(feature).subCollection(QueryBuilder.reprojected(feature.getType().getName(), CommonCRS.defaultGeographic()));
                try (final OutputStream out = Files.newOutputStream(outputFile);
                        final GeoJSONStreamWriter writer = new GeoJSONStreamWriter(out, feature.getType(), JsonEncoding.UTF8, 4)) {
                    FeatureUtilities.copy(
                            FeatureStoreUtilities.collection(feature)
                                    .subCollection(QueryBuilder.reprojected(feature.getType().getName(), CommonCRS.defaultGeographic()))
                                    .iterator().next(),
                            writer.next(), false);
                    writer.write();
                }
                break;

            default:
                final GenericName name = feature.getType().getName();
                final FeatureStore store = create(outputFile.toUri(), String.valueOf(name.scope()), format);
                try {
                    store.createFeatureType(name, feature.getType());
                    store.addFeatures(name, FeatureStoreUtilities.collection(feature));
                } finally {
                    // TODO : replace with try/w/r once FeatureStore implements auto-closeable.
                    if (store != null)
                        store.close();
                }
        }
    }

    public static FeatureStore create(final URI dataPath, final String namespace, final ExportType format) throws DataStoreException, MalformedURLException {
        ArgumentChecks.ensureNonNull("Output path", dataPath);
        switch (format) {
            case SHP: return new ShapefileFeatureStore(dataPath, namespace);
            case KML: throw new IllegalArgumentException("No feature store available for KML data");
            default:
                return new GeoJSONFeatureStore(dataPath, namespace, null);
        }
    }

    /**
     * Writes the given {@Site} as a zipped shapefile to the given output which
     * is assumed end with ".zip" extension.
     *
     * @param site
     * @param outputFile
     * @throws java.io.IOException
     * @throws org.apache.sis.storage.DataStoreException
     * @throws IllegalArgumentException if outputPath does not end with ".zip" extension.
     */
    public static void writeZip(final Site site, final File outputFile) throws IOException, DataStoreException{

        ArgumentChecks.ensureNonNull("Output zip file", outputFile);

        if(!outputFile.getName().endsWith(".zip")) throw new IllegalArgumentException("Output file name must end with .zip extension.");

        final Path tmpDirectory = Files.createTempDirectory("site");
        tmpDirectory.toFile().deleteOnExit();
        final File shpFile = new File(tmpDirectory.toFile(), outputFile.getName().replaceFirst(".zip$", ".shp"));

        writeShp(site, shpFile);

        final Collection<Path> resources = new ArrayList<>();

        Files.walkFileTree(tmpDirectory, new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                final File toAdd = file.toFile();
                toAdd.deleteOnExit();
                resources.add(file);
                return FileVisitResult.CONTINUE;
            }
        });

        ZipUtilities.zip(outputFile.toPath(), ZipOutputStream.DEFLATED, 9, null, resources.toArray(new Path[0]));
    }

    /**
     * Write metadata in a JSON file using information stored in the {@linkplain SerializableDataContext context}.
     *
     * @param context Data to serialize.
     * @param outputFolder Folder which will contain the json file.
     * @return Path of the generated metadata json.
     * @throws IOException
     */
    public static Path writeMetadata(final SerializableDataContext context, final Path outputFolder) throws IOException {
        final Path metadataJsonFile = outputFolder.resolve(METADATA_JSON);
        Files.deleteIfExists(metadataJsonFile);
        try (final BufferedWriter writer = Files.newBufferedWriter(metadataJsonFile, StandardCharsets.UTF_8)) {
            final ObjectMapper jsonMapper = new ObjectMapper();
            jsonMapper.writeValue(writer, context);
        }
        return metadataJsonFile;
    }

    /**
     * Export Site and its buffer in a given zip archive.
     *
     * @param outputFile The ZIP file in which we must insert data. Must not be null
     * @param site The site to export. Must not be null.
     * @param buffer The computed buffer for input site. Must not be null
     * @param territory Geometry representing hydrographic zones which intersects the buffer. Optional.
     * @param type File format to use for data. Must not be null
     *
     */
    public static void exportSiteAndBuffer(File outputFile, Site site, Geometry buffer, Optional<Geometry> territory, ExportType type) throws Exception {
        if (outputFile == null || type == null)
            return;

        final Path outputZipFile = outputFile.toPath();
        final Path tmpDir = Files.createTempDirectory("export");
        try {
            // issue #40 : write concatenation of site + buffer
            Feature siteFeat = toFeature(site);
            final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
            ftb.copy(siteFeat.getType());
            ftb.setName("site_buffer");
            ftb.add("type_contour", String.class);
            final FeatureType siteBufType = ftb.buildFeatureType();
            siteFeat.getDefaultGeometryProperty().setValue(buffer.difference(site.getGeometry()));
            FeatureBuilder fb = new FeatureBuilder(siteBufType);
            for (final Property p : siteFeat.getProperties()) {
                fb.setPropertyValue(p.getName(), p.getValue());
            }
            fb.setPropertyValue("type_contour", "site+buffer");
            // write site
            write(fb.buildFeature(site.getName()), tmpDir.resolve("site_buffer.".concat(type.extension)), type);

            // issue #40 : write intersecting hydrologic zones

            if (territory.isPresent()) {
                final Geometry territoryGeom = territory.get();
                ftb.reset();
                ftb.setName("territoire");
                ftb.add("the_geom", territoryGeom.getClass(), JTS.findCoordinateReferenceSystem(territoryGeom));
                ftb.add("type_contour", String.class);
                final FeatureType featureType = ftb.buildFeatureType();
                final Feature feat = FeatureUtilities.defaultFeature(featureType, "territoire");
                feat.getDefaultGeometryProperty().setValue(territoryGeom);
                feat.setPropertyValue("type_contour", "territoire");
                write(feat, tmpDir.resolve("territoire.".concat(type.extension)), type);
            }
            // Zip all data
            ZipUtilities.zip(outputZipFile, ZipOutputStream.DEFLATED, 9, null, Files.list(tmpDir).collect(Collectors.toList()).toArray(new Path[0]));
        } finally {
            IOUtilities.deleteRecursively(tmpDir);
        }
    }

    /**
     * Write result metadata file from input parameters. The output format is
     * simple text.
     * @param output File to write into.
     * @param site Site to write.
     * @param protocol Protocol to write.
     * @param referenceVersions Reference versions used for computing. (Key = reference  or reference name, value = version)
     * @param userData
     * @param indicators computed indicators
     * @param errors Errors which occurred while processing results.
     * @throws IOException
     */
    public static void writeMetadataText(
            final Path output,
            final Site site,
            final Protocol protocol,
            final Map<?, ?> referenceVersions,
            final Collection<Indicator> indicators,
            final Map<String, Object> userData,
            final Set<ConstraintViolation<Dataset>> errors) throws IOException {
        try (final BufferedWriter writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {
            writer.write("---- Site ----");
            writer.newLine();
            writer.write("Nom : ");
            writer.write(site.getName());
            writer.newLine();
            writer.write("Referent : ");
            writer.write(site.getReferent() == null? "Non renseigné" : site.getReferent());
            writer.newLine();
            writer.write("Organisation : ");
            writer.write(site.getOrganization() == null? "Non renseigné" : site.getOrganization());
            writer.newLine();
            writer.write("Département : ");
            writer.write(site.getCountyCode());
            writer.newLine();
            writer.newLine();

            writer.write("---- Protocole ----");
            writer.newLine();
            writer.write(protocol.getTitle());
            writer.newLine();
            writer.newLine();

            if (!referenceVersions.isEmpty()) {
                writer.write("---- Versions des référentiels ----");
                for (final Map.Entry ref : referenceVersions.entrySet()) {
                    writer.newLine();
                    writer.write(toString(ref.getKey()));
                    writer.write(" : ");
                    writer.write(toString(ref.getValue()));
                }
                writer.newLine();
                writer.newLine();
            }

            writer.write("---- Indicateurs calculés ----");
            for (final Indicator i : indicators) {
                writer.newLine();
                writer.write(i.getTitle());
            }

            if (userData != null && !userData.isEmpty()) {
                for (final Map.Entry<String, Object> data : userData.entrySet()) {
                    writer.newLine();
                    writer.newLine();
                    writer.write("---- " + data.getKey() + " ----");
                    writer.newLine();
                    writer.write(toString(data.getValue()));
                }
            }

            if (!errors.isEmpty()) {
                writer.newLine();
                writer.newLine();
                writer.write("---- Erreurs sur le lot de données ----");
                for (final ConstraintViolation v : errors) {
                    writer.newLine();
                    writer.write(v.getMessage());
                }
            }
        }
    }

    private static String toString(final Object o) {
        if (o == null)
            return "N/A";
        else if (o instanceof Collection) {
            final StringJoiner joiner = new StringJoiner(", ", "[", "]");
            final Iterator it = ((Collection)o).iterator();
            while (it.hasNext())
                joiner.add(toString(it.next()));
            return joiner.toString();
        } else if (o instanceof IdentifiedObject) {
            return ((IdentifiedObject)o).getTitle();
        } else {
            final String value = o.toString().trim();
            if (value.isEmpty())
                return "N/A";
            return value;
        }
    }
}
