<gh_stars>0
/* Copyright 2002-2018 CS Systèmes d'Information
 * Licensed to CS Systèmes d'Information (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS 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 fr.cs.examples.bodies;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.hipparchus.util.FastMath;
import org.orekit.data.DataProvidersManager;
import org.orekit.data.DirectoryCrawler;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScalesFactory;
import org.orekit.utils.Constants;

public class DEFile {

    private static final int INPOP_DE_NUMBER                = 100;
    private static final int CONSTANTS_MAX_NUMBER           = 400;

    private static final int HEADER_LABEL_SIZE              = 84;
    private static final int HEADER_LABEL_1_OFFSET          = 0;
    private static final int HEADER_LABEL_2_OFFSET          = HEADER_LABEL_1_OFFSET + HEADER_LABEL_SIZE;
    private static final int HEADER_LABEL_3_OFFSET          = HEADER_LABEL_2_OFFSET + HEADER_LABEL_SIZE;
    private static final int HEADER_EPHEMERIS_TYPE_OFFSET   = 2840;
    private static final int HEADER_RECORD_SIZE_OFFSET      = 2856;
    private static final int HEADER_START_EPOCH_OFFSET      = 2652;
    private static final int HEADER_END_EPOCH_OFFSET        = 2660;
    private static final int HEADER_CONSTANTS_NAMES_OFFSET  = 252;
    private static final int HEADER_CONSTANTS_VALUES_OFFSET = 0;
    private static final int DATA_START_RANGE_OFFSET        = 0;
    private static final int DATE_END_RANGE_OFFSET          = 8;

    private String       inName;
    private String       outName;
    private InputStream  input;
    private byte[]       first;
    private byte[]       second;
    private List<byte[]> selected;
    private int          recordSize;
    private boolean      bigEndian;
    private int          deNum;
    private String       label1;
    private String       label2;
    private String       label3;
    private AbsoluteDate headerStartEpoch;
    private AbsoluteDate headerFinalEpoch;
    private TimeScale    timeScale;
    private final Map<String, Double> headerConstants;

    /** Program entry point.
     * @param args program arguments (unused here)
     */
    public static void main(String[] args) {
        try {

            // configure Orekit
            File home       = new File(System.getProperty("user.home"));
            File orekitData = new File(home, "orekit-data");
            if (!orekitData.exists()) {
                System.err.format(Locale.US, "Failed to find %s folder%n",
                                  orekitData.getAbsolutePath());
                System.err.format(Locale.US, "You need to download %s from the %s page and unzip it in %s for this tutorial to work%n",
                                  "orekit-data.zip", "https://www.orekit.org/forge/projects/orekit/files",
                                  home.getAbsolutePath());
                System.exit(1);
            }
            DataProvidersManager manager = DataProvidersManager.getInstance();
            manager.addProvider(new DirectoryCrawler(orekitData));

            String inName  = null;
            String outName = null;
            List<String> constants = new ArrayList<String>();
            boolean allConstants = false;
            AbsoluteDate start = AbsoluteDate.PAST_INFINITY;
            AbsoluteDate end   = AbsoluteDate.FUTURE_INFINITY;
            for (int i = 0; i < args.length; ++i) {
                if ("-in".equals(args[i])) {
                    inName = args[++i];
                } else if ("-help".equals(args[i])) {
                    displayUsage(System.out);
                } else if ("-constant".equals(args[i])) {
                    constants.add(args[++i]);
                } else if ("-all-constants".equals(args[i])) {
                    allConstants = true;
                } else if ("-start".equals(args[i])) {
                    start = new AbsoluteDate(args[++i], TimeScalesFactory.getUTC());
                } else if ("-end".equals(args[i])) {
                    end = new AbsoluteDate(args[++i], TimeScalesFactory.getUTC());
                } else if ("-out".equals(args[i])) {
                    outName = args[++i];
                } else {
                    System.err.println("unknown command line option \"" + args[i] + "\"");
                    displayUsage(System.err);
                    System.exit(1);
                }
            }

            if (inName == null) {
                displayUsage(System.err);
                System.exit(1);
            }
            DEFile de = new DEFile(inName, outName);

            de.processHeader();
            System.out.println("header label 1     " + de.label1);
            System.out.println("header label 2     " + de.label2);
            System.out.println("header label 3     " + de.label3);
            System.out.println("header start epoch " + de.headerStartEpoch.toString(de.timeScale) +
                               " (" + de.timeScale.getName() + ")");
            System.out.println("header end epoch   " + de.headerFinalEpoch.toString(de.timeScale) +
                               " (" + de.timeScale.getName() + ")");

            for (String constant : constants) {
                Double value = de.headerConstants.get(constant);
                System.out.println(constant + "     " + ((value == null) ? "not present" : value));
            }

            if (allConstants) {
                for (Map.Entry<String, Double> entry : de.headerConstants.entrySet()) {
                    System.out.println(entry.getKey() + "     " + entry.getValue());
                }
            }

            int processed = de.processData(start, end);
            System.out.println("data records: " + processed);

            if (outName != null) {
                de.write();
                System.out.println(outName + " file created with " +
                                   de.selected.size() + " selected data records");
            }

        } catch (IOException ioe) {
            ioe.printStackTrace(System.err);
        } catch (OrekitException oe) {
            oe.printStackTrace(System.err);
        }
    }

    private static void displayUsage(final PrintStream stream) {
        stream.print("usage: java DEFile");
        stream.print(" -in filename");
        stream.print(" [-help]");
        stream.print(" [-constant name]");
        stream.print(" [-all-constants]");
        stream.print(" [-start date]");
        stream.print(" [-end date]");
        stream.print(" [-out filename]");
        stream.println();
    }

    private DEFile(String inName, String outName) throws FileNotFoundException {
        this.inName          = inName;
        this.outName         = outName;
        this.input           = new FileInputStream(inName);
        this.headerConstants = new HashMap<String, Double>();
        this.selected        = new ArrayList<byte[]>();
    }

    private void processHeader() throws IOException, OrekitException {

        first = readFirstRecord();
        second = new byte[recordSize];
        readInRecord(second, 0);

        label1 = extractString(first, HEADER_LABEL_1_OFFSET, HEADER_LABEL_SIZE);
        label2 = extractString(first, HEADER_LABEL_2_OFFSET, HEADER_LABEL_SIZE);
        label3 = extractString(first, HEADER_LABEL_3_OFFSET, HEADER_LABEL_SIZE);

        // constants defined in the file
        for (int i = 0; i < CONSTANTS_MAX_NUMBER; ++i) {
            // Note: for extracting the strings from the binary file, it makes no difference
            //       if the file is stored in big-endian or little-endian notation
            final String constantName = extractString(first, HEADER_CONSTANTS_NAMES_OFFSET + i * 6, 6);
            if (constantName.length() == 0) {
                // no more constants to read
                break;
            }
            final double constantValue = extractDouble(second, HEADER_CONSTANTS_VALUES_OFFSET + 8 * i, bigEndian);
            headerConstants.put(constantName, constantValue);
        }

        final Double timesc = headerConstants.get("TIMESC");
        if (timesc != null && !Double.isNaN(timesc) && timesc.intValue() == 1) {
            timeScale = TimeScalesFactory.getTCB();
        } else {
            timeScale = TimeScalesFactory.getTDB();
        }

        headerStartEpoch = extractDate(first, HEADER_START_EPOCH_OFFSET, bigEndian);
        headerFinalEpoch = extractDate(first, HEADER_END_EPOCH_OFFSET, bigEndian);

    }

    private int processData(AbsoluteDate selectStart, AbsoluteDate selectEnd)
        throws IOException, OrekitException {

        byte[] data = new byte[recordSize];
        int processed = 0;
        while (readInRecord(data, 0)) {
            // extract time range covered by the record
            final AbsoluteDate rangeStart = extractDate(data, DATA_START_RANGE_OFFSET, bigEndian);
            final AbsoluteDate rangeEnd   = extractDate(data, DATE_END_RANGE_OFFSET,   bigEndian);
            if (rangeEnd.compareTo(selectStart) >= 0 && rangeStart.compareTo(selectEnd) <= 0) {
                selected.add(data.clone());
            }
            ++processed;
        }

        return processed;

    }

    private void write() throws IOException {

        if (!selected.isEmpty()) {
            if (outName != null) {
                try (OutputStream out = new FileOutputStream(outName)) {

                    // patch header epoch
                    System.arraycopy(selected.get(0), DATA_START_RANGE_OFFSET,
                                     first, HEADER_START_EPOCH_OFFSET,
                                     8);
                    System.arraycopy(selected.get(selected.size() - 1), DATE_END_RANGE_OFFSET,
                                     first, HEADER_END_EPOCH_OFFSET,
                                     8);

                    // patch header labels
                    final AbsoluteDate       start = extractDate(first, HEADER_START_EPOCH_OFFSET, bigEndian);
                    final DateTimeComponents sc    = start.getComponents(timeScale);
                    final AbsoluteDate       end   = extractDate(first, HEADER_END_EPOCH_OFFSET, bigEndian);
                    final DateTimeComponents ec    = end.getComponents(timeScale);
                    System.arraycopy(padString("THIS IS NOT A GENUINE JPL DE FILE," +
                                    " THIS IS AN EXCERPT WITH A LIMITED TIME RANGE",
                                    HEADER_LABEL_SIZE), 0,
                                     first, HEADER_LABEL_1_OFFSET,
                                     HEADER_LABEL_SIZE);
                    System.arraycopy(padString(String.format(Locale.US,
                                                             "Start Epoch: JED=  %.1f %4d-%s-%02d %02d:%02d:%02.0f",
                                                             (start.durationFrom(AbsoluteDate.JULIAN_EPOCH)) /
                                                             Constants.JULIAN_DAY,
                                                             sc.getDate().getYear(),
                                                             sc.getDate().getMonthEnum().getUpperCaseAbbreviation(),
                                                             sc.getDate().getDay(),
                                                             sc.getTime().getHour(),
                                                             sc.getTime().getMinute(),
                                                             sc.getTime().getSecond()),
                                               HEADER_LABEL_SIZE), 0,
                                     first, HEADER_LABEL_2_OFFSET,
                                     HEADER_LABEL_SIZE);
                    System.arraycopy(padString(String.format(Locale.US,
                                                             "Final Epoch: JED=  %.1f %4d-%s-%02d %02d:%02d:%02.0f",
                                                             (end.durationFrom(AbsoluteDate.JULIAN_EPOCH)) /
                                                             Constants.JULIAN_DAY,
                                                             ec.getDate().getYear(),
                                                             ec.getDate().getMonthEnum().getUpperCaseAbbreviation(),
                                                             ec.getDate().getDay(),
                                                             ec.getTime().getHour(),
                                                             ec.getTime().getMinute(),
                                                             ec.getTime().getSecond()),
                                               HEADER_LABEL_SIZE), 0,
                                     first, HEADER_LABEL_3_OFFSET,
                                     HEADER_LABEL_SIZE);

                    // write patched header
                    out.write(first);
                    out.write(second);

                    // write selected data
                    for (byte[] data : selected) {
                        out.write(data);
                    }
                }
            }
        }

    }

    private byte[] readFirstRecord()
        throws OrekitException, IOException {

        // read first part of record, up to the record number
        final byte[] firstPart = new byte[HEADER_RECORD_SIZE_OFFSET + 4];
        if (!readInRecord(firstPart, 0)) {
            throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, inName);
        }

        // detect the endian format
        bigEndian = detectEndianess(firstPart);

        // get the ephemerides type
        deNum = extractInt(firstPart, HEADER_EPHEMERIS_TYPE_OFFSET, bigEndian);

        // the record size for this file
        recordSize = 0;

        if (deNum == INPOP_DE_NUMBER) {
            // INPOP files have an extended DE format, which includes also the record size
            recordSize = extractInt(firstPart, HEADER_RECORD_SIZE_OFFSET, bigEndian) << 3;
        } else {
            // compute the record size for original JPL files
            recordSize = computeRecordSize(firstPart, bigEndian, inName);
        }

        if (recordSize <= 0) {
            throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, inName);
        }

        // build a record with the proper size and finish read of the first complete record
        final byte[] record = new byte[recordSize];
        System.arraycopy(firstPart, 0, record, 0, firstPart.length);
        if (!readInRecord(record, firstPart.length)) {
            throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, inName);
        }

        return record;

    }

    private boolean readInRecord(final byte[] record, final int start)
        throws IOException {
        int index = start;
        while (index != record.length) {
            final int n = input.read(record, index, record.length - index);
            if (n < 0) {
                return false;
            }
            index += n;
        }
        return true;
    }

    /** Detect whether the JPL ephemerides file is stored in big-endian or
     * little-endian notation.
     * @param record the array containing the binary JPL header
     * @return <code>true</code> if the file is stored in big-endian,
     * <code>false</code> otherwise
     */
    private static boolean detectEndianess(final byte[] record) {

        // default to big-endian
        boolean bigEndian = true;

        // first try to read the DE number in big-endian format
        // the number is stored as unsigned int, so we have to convert it properly
        final long deNum = extractInt(record, HEADER_EPHEMERIS_TYPE_OFFSET, true) & 0xffffffffL;

        // simple heuristic: if the read value is larger than half the range of an integer
        //                   assume the file is in little-endian format
        if (deNum > (1 << 15)) {
            bigEndian = false;
        }

        return bigEndian;

    }

    /** Calculate the record size of a JPL ephemerides file.
     * @param record the byte array containing the header record
     * @param bigEndian indicates the endianess of the file
     * @param name the name of the data file
     * @return the record size for this file
     * @throws OrekitException if the file contains unexpected data
     */
    private static int computeRecordSize(final byte[] record,
                                         final boolean bigEndian,
                                         final String name)
        throws OrekitException {

        int recordSize = 0;
        boolean ok = true;
        // JPL files always have 3 position components
        final int nComp = 3;

        // iterate over the coefficient ptr array and sum up the record size
        // the coeffPtr array has the dimensions [12][nComp]
        for (int j = 0; j < 12; j++) {
            final int nCompCur = (j == 11) ? 2 : nComp;

            // the coeffPtr array starts at offset 2696
            // Note: the array element coeffPtr[j][0] is not needed for the calculation
            final int idx = 2696 + j * nComp * 4;
            final int coeffPtr1 = extractInt(record, idx + 4, bigEndian);
            final int coeffPtr2 = extractInt(record, idx + 8, bigEndian);

            // sanity checks
            ok = ok && (coeffPtr1 >= 0 || coeffPtr2 >= 0);

            recordSize += coeffPtr1 * coeffPtr2 * nCompCur;
        }

        // the libration ptr array starts at offset 2844 and has the dimension [3]
        // Note: the array element libratPtr[0] is not needed for the calculation
        final int libratPtr1 = extractInt(record, 2844 + 4, bigEndian);
        final int libratPtr2 = extractInt(record, 2844 + 8, bigEndian);

        // sanity checks
        ok = ok && (libratPtr1 >= 0 || libratPtr2 >= 0);

        recordSize += libratPtr1 * libratPtr2 * nComp + 2;
        recordSize <<= 3;

        if (!ok || recordSize <= 0) {
            throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
        }

        return recordSize;

    }

    /** Extract a date from a record.
     * @param record record to parse
     * @param offset offset of the double within the record
     * @param bigEndian if <code>true</code> the parsed date is extracted in big-endian
     * format, otherwise it is extracted in little-endian format
     * @return extracted date
     */
    private AbsoluteDate extractDate(final byte[] record,
                                     final int offset,
                                     final boolean bigEndian) {

        final double t = extractDouble(record, offset, bigEndian);
        int    jDay    = (int) FastMath.floor(t);
        double seconds = (t + 0.5 - jDay) * Constants.JULIAN_DAY;
        if (seconds >= Constants.JULIAN_DAY) {
            ++jDay;
            seconds -= Constants.JULIAN_DAY;
        }
        final AbsoluteDate date =
            new AbsoluteDate(new DateComponents(DateComponents.JULIAN_EPOCH, jDay),
                             new TimeComponents(seconds), timeScale);
        return date;
    }

    /** Extract a double from a record.
     * <p>Double numbers are stored according to IEEE 754 standard, with
     * most significant byte first.</p>
     * @param record record to parse
     * @param offset offset of the double within the record
     * @param bigEndian if <code>true</code> the parsed double is extracted in big-endian
     * format, otherwise it is extracted in little-endian format
     * @return extracted double
     */
    private static double extractDouble(final byte[] record, final int offset, final boolean bigEndian) {
        final long l8 = ((long) record[offset + 0]) & 0xffl;
        final long l7 = ((long) record[offset + 1]) & 0xffl;
        final long l6 = ((long) record[offset + 2]) & 0xffl;
        final long l5 = ((long) record[offset + 3]) & 0xffl;
        final long l4 = ((long) record[offset + 4]) & 0xffl;
        final long l3 = ((long) record[offset + 5]) & 0xffl;
        final long l2 = ((long) record[offset + 6]) & 0xffl;
        final long l1 = ((long) record[offset + 7]) & 0xffl;
        long l;
        if (bigEndian) {
            l = (l8 << 56) | (l7 << 48) | (l6 << 40) | (l5 << 32) |
                (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
        } else {
            l = (l1 << 56) | (l2 << 48) | (l3 << 40) | (l4 << 32) |
                (l5 << 24) | (l6 << 16) | (l7 <<  8) | l8;
        }
        return Double.longBitsToDouble(l);
    }

    /** Extract an int from a record.
     * @param record record to parse
     * @param offset offset of the double within the record
     * @param bigEndian if <code>true</code> the parsed int is extracted in big-endian
     * format, otherwise it is extracted in little-endian format
     * @return extracted int
     */
    private static int extractInt(final byte[] record, final int offset, final boolean bigEndian) {
        final int l4 = ((int) record[offset + 0]) & 0xff;
        final int l3 = ((int) record[offset + 1]) & 0xff;
        final int l2 = ((int) record[offset + 2]) & 0xff;
        final int l1 = ((int) record[offset + 3]) & 0xff;

        if (bigEndian) {
            return (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
        } else {
            return (l1 << 24) | (l2 << 16) | (l3 <<  8) | l4;
        }
    }

    /** Extract a String from a record.
     * @param record record to parse
     * @param offset offset of the string within the record
     * @param length maximal length of the string
     * @return extracted string, with whitespace characters stripped
     */
    private static String extractString(final byte[] record, final int offset, final int length) {
        try {
            return new String(record, offset, length, "US-ASCII").trim();
        } catch (UnsupportedEncodingException uee) {
            throw new OrekitInternalError(uee);
        }
    }

    /** Pad a string into a bytes array.
     * @param s string to pad
     * @param length length of the padded bytes array
     * @return padded bytes array
     */
    private static byte[] padString(final String s, final int length) {
        final Charset charSet = Charset.forName("US-ASCII");
        final byte[] array = new byte[length];
        Arrays.fill(array, charSet.encode(" ").get());
        final byte[] sb = charSet.encode(s).array();
        System.arraycopy(sb, 0, array, 0, FastMath.min(sb.length, array.length));
        return array;
    }

}
