/*
 * citygml-tools - Collection of tools for processing CityGML files
 * https://github.com/citygml4j/citygml-tools
 *
 * citygml-tools is part of the citygml4j project
 *
 * Copyright 2018-2023 Claus Nagel <claus.nagel@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.citygml4j.tools.command;

import org.citygml4j.cityjson.adapter.appearance.serializer.AppearanceSerializer;
import org.citygml4j.cityjson.adapter.geometry.serializer.GeometrySerializer;
import org.citygml4j.cityjson.model.metadata.Metadata;
import org.citygml4j.cityjson.model.metadata.ReferenceSystem;
import org.citygml4j.cityjson.writer.AbstractCityJSONWriter;
import org.citygml4j.cityjson.writer.CityJSONOutputFactory;
import org.citygml4j.cityjson.writer.CityJSONWriteException;
import org.citygml4j.tools.ExecutionException;
import org.citygml4j.tools.option.CityJSONOutputOptions;
import org.citygml4j.tools.option.InputOptions;
import org.citygml4j.tools.util.GlobalObjects;
import org.citygml4j.tools.util.GlobalObjectsReader;
import org.citygml4j.tools.util.InputFiles;
import org.citygml4j.xml.reader.*;
import org.xmlobjects.gml.model.geometry.Envelope;
import picocli.CommandLine;

import java.nio.file.Path;
import java.util.List;

@CommandLine.Command(name = "to-cityjson",
        description = "Converts CityGML files into CityJSON.")
public class ToCityJSONCommand extends CityGMLTool {
    @CommandLine.Option(names = {"-e", "--epsg"}, paramLabel = "<code>",
            description = "EPSG code to use as CRS in the metadata.")
    private int epsg;

    @CommandLine.Option(names = {"-c", "--compute-extent"},
            description = "Compute city model extent to use in the metadata.")
    private boolean computeExtent;

    @CommandLine.Option(names = "--vertex-precision", paramLabel = "<digits>",
            description = "Number of decimal places to keep for geometry vertices (default: ${DEFAULT-VALUE}).")
    private int vertexPrecision = GeometrySerializer.DEFAULT_VERTEX_PRECISION;

    @CommandLine.Option(names = "--template-precision", paramLabel = "<digits>",
            description = "Number of decimal places to keep for template vertices (default: ${DEFAULT-VALUE}).")
    private int templatePrecision = GeometrySerializer.DEFAULT_TEMPLATE_PRECISION;

    @CommandLine.Option(names = "--texture-vertex-precision", paramLabel = "<digits>",
            description = "Number of decimal places to keep for texture vertices (default: ${DEFAULT-VALUE}).")
    private int textureVertexPrecision = AppearanceSerializer.DEFAULT_TEXTURE_VERTEX_PRECISION;

    @CommandLine.Option(names = {"-t", "--transform-coordinates"},
            description = "Transform the coordinates of vertices to integers to reduce the file size. The " +
                    "transformation is always applied for CityJSON 1.1.")
    private boolean transformCoordinates;

    @CommandLine.Option(names = {"-r", "--replace-implicit-geometries"},
            description = "Replace implicit geometries with their absolute coordinates.")
    private boolean replaceImplicitGeometries;

    @CommandLine.Option(names = "--no-material-defaults", negatable = true, defaultValue = "true",
            description = "Use CityGML default values for material properties (default: ${DEFAULT-VALUE}).")
    private boolean useMaterialDefaults;

    @CommandLine.Option(names = "--fallback-theme", paramLabel = "<theme>",
            description = "Theme to use for materials and textures if not defined in the input file(s) " +
                    "(default: ${DEFAULT-VALUE}).")
    private String fallbackTheme = AppearanceSerializer.FALLBACK_THEME;

    @CommandLine.Option(names = "--add-generic-attribute-types",
            description = "Add data types of generic attributes as extension property.")
    private boolean addGenericAttributeTypes;

    @CommandLine.Mixin
    private CityJSONOutputOptions outputOptions;

    @CommandLine.Mixin
    private InputOptions inputOptions;

    @Override
    public Integer call() throws ExecutionException {
        log.debug("Searching for CityGML input files.");
        List<Path> inputFiles = InputFiles.of(inputOptions.getFiles()).find();

        if (inputFiles.isEmpty()) {
            log.warn("No files found at " + inputOptions.joinFiles() + ".");
            return CommandLine.ExitCode.OK;
        }

        log.info("Found " + inputFiles.size() + " file(s) at " + inputOptions.joinFiles() + ".");
        log.debug("Using CityJSON " + outputOptions.getVersion() + " for the output file(s).");

        CityGMLInputFactory in = createCityGMLInputFactory().withChunking(ChunkOptions.defaults());
        CityJSONOutputFactory out = createCityJSONOutputFactory(outputOptions.getVersion())
                .computeCityModelExtent(computeExtent)
                .withVertexPrecision(vertexPrecision)
                .withTemplatePrecision(templatePrecision)
                .withTextureVertexPrecision(textureVertexPrecision)
                .applyTransformation(transformCoordinates)
                .transformTemplateGeometries(replaceImplicitGeometries)
                .useMaterialDefaults(useMaterialDefaults)
                .withFallbackTheme(fallbackTheme)
                .writeGenericAttributeTypes(addGenericAttributeTypes);

        for (int i = 0; i < inputFiles.size(); i++) {
            Path inputFile = inputFiles.get(i);
            Path outputFile = inputFile.resolveSibling(replaceFileExtension(inputFile, "json"));

            log.info("[" + (i + 1) + "|" + inputFiles.size() + "] Processing file " + inputFile.toAbsolutePath() + ".");

            log.debug("Reading global appearances, groups and implicit geometries from input file.");
            GlobalObjects globalObjects = GlobalObjectsReader.defaults()
                    .read(inputFile, getCityGMLContext());

            try (CityGMLReader reader = createSkippingCityGMLReader(in, inputFile, inputOptions,
                    "CityObjectGroup", "Appearance")) {
                Metadata metadata = new Metadata();
                if (reader.hasNext()) {
                    populateMetadata(metadata, reader.getParentInfo());
                }

                log.info("Writing output to file " + outputFile.toAbsolutePath() + ".");

                try (AbstractCityJSONWriter<?> writer = createCityJSONWriter(out, outputFile, outputOptions)
                        .withMetadata(metadata)) {
                    log.debug("Reading city objects and converting them into CityJSON " + outputOptions.getVersion() + ".");
                    globalObjects.getAppearances().forEach(writer::withGlobalAppearance);
                    globalObjects.getCityObjectGroups().forEach(writer::withGlobalCityObjectGroup);
                    globalObjects.getTemplateGeometries().values().forEach(writer::withGlobalTemplateGeometry);

                    while (reader.hasNext()) {
                        writer.writeCityObject(reader.next());
                    }
                }
            } catch (CityGMLReadException e) {
                throw new ExecutionException("Failed to read file " + inputFile.toAbsolutePath() + ".", e);
            } catch (CityJSONWriteException e) {
                throw new ExecutionException("Failed to write file " + outputFile.toAbsolutePath() + ".", e);
            }
        }

        return CommandLine.ExitCode.OK;
    }

    private void populateMetadata(Metadata metadata, FeatureInfo info) {
        if (info != null) {
            if (info.getId() != null) {
                metadata.setIdentifier(info.getId());
            }

            if (info.getDescription() != null) {
                metadata.setTitle(info.getDescription().getValue());
            }

            if (info.getBoundedBy() != null
                    && info.getBoundedBy().getEnvelope() != null) {
                Envelope envelope = info.getBoundedBy().getEnvelope();
                metadata.setGeographicalExtent(envelope);
                metadata.setReferenceSystem(epsg > 0 ?
                        new ReferenceSystem(epsg) :
                        ReferenceSystem.parse(envelope.getSrsName()));
            }
        }
    }
}
