/* Copyright 2016 Applied Defense Solutions (ADS)
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * ADS licenses this file to You 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.orekit.files.ccsds.ndm.odm.ocm;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.orekit.errors.OrekitIllegalArgumentException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.ndm.odm.OdmHeader;
import org.orekit.files.ccsds.section.XmlStructureKey;
import org.orekit.files.ccsds.utils.FileFormat;
import org.orekit.files.ccsds.utils.generation.Generator;
import org.orekit.files.ccsds.utils.generation.KvnGenerator;
import org.orekit.files.ccsds.utils.generation.XmlGenerator;
import org.orekit.files.general.EphemerisFile;
import org.orekit.files.general.EphemerisFile.SatelliteEphemeris;
import org.orekit.files.general.EphemerisFileWriter;
import org.orekit.frames.Frame;
import org.orekit.utils.TimeStampedPVCoordinates;

/** An {@link EphemerisFileWriter} generating {@link Ocm OCM} files.
 * <p>
 * This writer is intended to write only trajectory state history blocks.
 * It does not writes physical properties, covariance data, maneuver data,
 * perturbations parameters, orbit determination or user-defined parameters.
 * If these blocks are needed, then {@link OcmWriter OcmWriter} must be
 * used as it handles all OCM data blocks.
 * </p>
 * <p>
 * The trajectory blocks metadata identifiers ({@code TRAJ_ID},
 * {@code TRAJ_PREV_ID}, {@code TRAJ_NEXT_ID}) are updated automatically
 * using {@link TrajectoryStateHistoryMetadata#incrementTrajID(String)},
 * so users should generally only set {@link TrajectoryStateHistoryMetadata#setTrajID(String)}
 * in the template.
 * </p>
 * @author Luc Maisonobe
 * @since 12.0
 * @see OcmWriter
 * @see StreamingOcmWriter
 */
public class EphemerisOcmWriter implements EphemerisFileWriter {

    /** Underlying writer. */
    private final OcmWriter writer;

    /** Header. */
    private final OdmHeader header;

    /** File metadata. */
    private final OcmMetadata metadata;

    /** Current trajectory metadata. */
    private final TrajectoryStateHistoryMetadata trajectoryMetadata;

    /** File format to use. */
    private final FileFormat fileFormat;

    /** Output name for error messages. */
    private final String outputName;

    /** Column number for aligning units. */
    private final int unitsColumn;

    /** Maximum offset for relative dates. */
    private final double maxRelativeOffset;

    /**
     * Constructor used to create a new OCM writer configured with the necessary parameters
     * to successfully fill in all required fields that aren't part of a standard object.
     * <p>
     * If the mandatory header entries are not present (or if header is null),
     * built-in defaults will be used
     * </p>
     * <p>
     * The writer is built from the complete header and partial metadata. The template
     * metadata is used to initialize and independent local copy, that will be updated
     * as new segments are written (with at least the segment start and stop will change,
     * but some other parts may change too). The {@code template} argument itself is not
     * changed.
     * </p>
     * @param writer underlying writer
     * @param header file header (may be null)
     * @param metadata  file metadata
     * @param template  template for trajectory metadata
     * @param fileFormat file format to use
     * @param outputName output name for error messages
     * @param maxRelativeOffset maximum offset in seconds to use relative dates
     * (if a date is too far from reference, it will be displayed as calendar elements)
     * @param unitsColumn columns number for aligning units (if negative or zero, units are not output)
     */
    public EphemerisOcmWriter(final OcmWriter writer,
                              final OdmHeader header, final OcmMetadata metadata,
                              final TrajectoryStateHistoryMetadata template,
                              final FileFormat fileFormat, final String outputName,
                              final double maxRelativeOffset, final int unitsColumn) {
        this.writer             = writer;
        this.header             = header;
        this.metadata           = metadata.copy(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
        this.trajectoryMetadata = template.copy(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
        this.fileFormat         = fileFormat;
        this.outputName         = outputName;
        this.maxRelativeOffset  = maxRelativeOffset;
        this.unitsColumn        = unitsColumn;
    }

    /** {@inheritDoc}
     * <p>
     * As {@code EphemerisFile.SatelliteEphemeris} does not have all the entries
     * from {@link OcmMetadata}, the only values that will be extracted from the
     * {@code ephemerisFile} will be the start time, stop time, reference frame, interpolation
     * method and interpolation degree. The missing values (like object name, local spacecraft
     * body frame...) will be inherited from the template  metadata set at writer
     * {@link #EphemerisOcmWriter(OcmWriter, OdmHeader, OcmMetadata, TrajectoryStateHistoryMetadata,
     * FileFormat, String, double, int) construction}.
     * </p>
     */
    @Override
    public <C extends TimeStampedPVCoordinates, S extends EphemerisFile.EphemerisSegment<C>>
        void write(final Appendable appendable, final EphemerisFile<C, S> ephemerisFile)
        throws IOException {

        if (appendable == null) {
            throw new OrekitIllegalArgumentException(OrekitMessages.NULL_ARGUMENT, "writer");
        }

        if (ephemerisFile == null) {
            return;
        }

        final String name;
        if (metadata.getObjectName() != null) {
            name = metadata.getObjectName();
        } else if (metadata.getInternationalDesignator() != null) {
            name = metadata.getInternationalDesignator();
        } else if (metadata.getObjectDesignator() != null) {
            name = metadata.getObjectDesignator();
        } else {
            name = Ocm.UNKNOWN_OBJECT;
        }
        final SatelliteEphemeris<C, S> satEphem = ephemerisFile.getSatellites().get(name);
        if (satEphem == null) {
            throw new OrekitIllegalArgumentException(OrekitMessages.VALUE_NOT_FOUND,
                                                     name, "ephemerisFile");
        }

        // Get trajectory blocks to output.
        final List<S> blocks = satEphem.getSegments();
        if (blocks.isEmpty()) {
            // No data -> No output
            return;
        }

        try (Generator generator = fileFormat == FileFormat.KVN ?
                                   new KvnGenerator(appendable, OcmWriter.KVN_PADDING_WIDTH, outputName,
                                                    maxRelativeOffset, unitsColumn) :
                                   new XmlGenerator(appendable, XmlGenerator.DEFAULT_INDENT, outputName,
                                                    maxRelativeOffset, unitsColumn > 0, null)) {

            writer.writeHeader(generator, header);

            if (generator.getFormat() == FileFormat.XML) {
                generator.enterSection(XmlStructureKey.segment.name());
            }

            // write single segment metadata
            metadata.setStartTime(blocks.get(0).getStart());
            metadata.setStopTime(blocks.get(blocks.size() - 1).getStop());
            new OcmMetadataWriter(metadata, writer.getTimeConverter()).write(generator);

            if (generator.getFormat() == FileFormat.XML) {
                generator.enterSection(XmlStructureKey.data.name());
            }

            // Loop on trajectory blocks
            double lastZ = Double.NaN;
            for (final S block : blocks) {

                // prepare metadata
                trajectoryMetadata.setTrajNextID(TrajectoryStateHistoryMetadata.incrementTrajID(trajectoryMetadata.getTrajID()));
                trajectoryMetadata.setUseableStartTime(block.getStart());
                trajectoryMetadata.setUseableStopTime(block.getStop());
                trajectoryMetadata.setInterpolationDegree(block.getInterpolationSamples() - 1);

                // prepare data
                final OrbitElementsType type      = trajectoryMetadata.getTrajType();
                final Frame             frame     = trajectoryMetadata.getTrajReferenceFrame().asFrame();
                int                     crossings = 0;
                final List<TrajectoryState> states = new ArrayList<>(block.getCoordinates().size());
                for (final C pv : block.getCoordinates()) {
                    if (lastZ < 0.0 && pv.getPosition().getZ() >= 0.0) {
                        // we crossed ascending node
                        ++crossings;
                    }
                    lastZ = pv.getPosition().getZ();
                    states.add(new TrajectoryState(type, pv.getDate(), type.toRawElements(pv, frame, block.getMu())));
                }
                final TrajectoryStateHistory history = new TrajectoryStateHistory(trajectoryMetadata, states, block.getMu());

                // write trajectory block
                final TrajectoryStateHistoryWriter trajectoryWriter =
                                new TrajectoryStateHistoryWriter(history, writer.getTimeConverter());
                trajectoryWriter.write(generator);

                // update the trajectory IDs
                trajectoryMetadata.setTrajPrevID(trajectoryMetadata.getTrajID());
                trajectoryMetadata.setTrajID(trajectoryMetadata.getTrajNextID());

                if (trajectoryMetadata.getOrbRevNum() >= 0) {
                    // update the orbits revolution number
                    trajectoryMetadata.setOrbRevNum(trajectoryMetadata.getOrbRevNum() + crossings);
                }

            }

            if (generator.getFormat() == FileFormat.XML) {
                generator.exitSection(); // exit data
                generator.exitSection(); // exit segment
            }

            writer.writeFooter(generator);

        }

    }

}
