/* Copyright 2013 Applied Defense Solutions, Inc.
 * Licensed to CS Communication & Systèmes (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 org.orekit.models.earth;

import org.hipparchus.analysis.UnivariateFunction;
import org.hipparchus.optim.MaxEval;
import org.hipparchus.optim.nonlinear.scalar.GoalType;
import org.hipparchus.optim.univariate.BrentOptimizer;
import org.hipparchus.optim.univariate.SearchInterval;
import org.hipparchus.optim.univariate.UnivariateObjectiveFunction;
import org.hipparchus.util.FastMath;
import org.orekit.models.AtmosphericRefractionModel;

/** Implementation of refraction model for Earth exponential atmosphere based on ITU-R P.834-7 recommendation.
 * <p>Refraction angle is computed according to the International Telecommunication Union recommendation formula.
 *  For reference, see <b>ITU-R P.834-7</b> (October 2015).</p>
 *
 * @author Thierry Ceolin
 * @since 7.1
 */

public class EarthITU453AtmosphereRefraction implements AtmosphericRefractionModel {

    /** Altitude conversion factor. */
    private static final double KM_TO_M = 1000.0;

    /** Coefficients conversion factor. */
    private static final double INV_DEG_TO_INV_RAD = 180.0 / FastMath.PI;

    /** Default a coefficients to compute refractive index for a typical atmosphere. */
    private static final double DEFAULT_CORRECTION_ACOEF = 0.000315;

    /** Default b coefficients to compute refractive index for a typical atmosphere. */
    private static final double DEFAULT_CORRECTION_BCOEF = 0.1361 / KM_TO_M;

    /** Earth ray as defined in ITU-R P.834-7 (m). */
    private static final double EARTH_RAY = 6370.0 * KM_TO_M;

    /** Default coefficients array for Tau function (formula number 9).
     * The coefficients have been converted to SI units
     */
    private static final double[] CCOEF = {
        INV_DEG_TO_INV_RAD * 1.314,  INV_DEG_TO_INV_RAD * 0.6437,  INV_DEG_TO_INV_RAD * 0.02869,
        INV_DEG_TO_INV_RAD * 0.2305 / KM_TO_M, INV_DEG_TO_INV_RAD * 0.09428 / KM_TO_M, INV_DEG_TO_INV_RAD * 0.01096 / KM_TO_M,
        INV_DEG_TO_INV_RAD * 0.008583 / (KM_TO_M * KM_TO_M)
    };

    /** Default coefficients array for TauZero function (formula number 14).
     * The coefficients have been converted to SI units
     */
    private static final double[] CCOEF0 = {
        INV_DEG_TO_INV_RAD * 1.728, INV_DEG_TO_INV_RAD * 0.5411, INV_DEG_TO_INV_RAD * 0.03723,
        INV_DEG_TO_INV_RAD * 0.1815 / KM_TO_M, INV_DEG_TO_INV_RAD * 0.06272 / KM_TO_M, INV_DEG_TO_INV_RAD * 0.011380 / KM_TO_M,
        INV_DEG_TO_INV_RAD * 0.01727 / (KM_TO_M * KM_TO_M), INV_DEG_TO_INV_RAD * 0.008288 / (KM_TO_M * KM_TO_M)
    };

    /** Serializable UID. */
    private static final long serialVersionUID = 20160118L;

    /** station altitude (m). */
    private final double altitude;

    /** minimal elevation angle for the station (rad). */
    private final double thetamin;

    /** minimal elevation angle under free-space propagation (rad). */
    private final double theta0;

    /** elevation where elevation+refraction correction is minimal (near inequality formula number 11 validity domain). */
    private final double elev_star;

    /** refraction correction value where elevation+refraction correction is minimal (near inequality 11 validity domain). */
    private final double refrac_star;

    /** Creates a new default instance.
     * @param altitude altitude of the ground station from which measurement is performed (m)
     */
    public EarthITU453AtmosphereRefraction(final double altitude) {
        this.altitude = altitude;
        thetamin = getMinimalElevation(altitude);
        theta0   = thetamin - getTau(thetamin);

        final UnivariateFunction refrac = new UnivariateFunction() {
            public double value (final double elev) {
                return elev + getBaseRefraction(elev);
            }
        };

        final double rel = 1.e-5;
        final double abs = 1.e-10;
        final BrentOptimizer optimizer = new BrentOptimizer(rel, abs);

        // Call optimizer
        elev_star = optimizer.optimize(new MaxEval(200),
                                       new UnivariateObjectiveFunction(refrac),
                                       GoalType.MINIMIZE,
                                       new SearchInterval(-FastMath.PI / 30., FastMath.PI / 4)).getPoint();
        refrac_star = getBaseRefraction(elev_star);
    }

    /** Compute the refractive index correction in the case of a typical atmosphere.
     * ITU-R P.834-7, formula number 8, page 3
     * @param alt altitude of the station at the Earth surface (m)
     * @return the refractive index
     */
    private double getRefractiveIndex(final double alt) {

        return 1.0 + DEFAULT_CORRECTION_ACOEF * FastMath.exp(-DEFAULT_CORRECTION_BCOEF * alt);
    }

    /** Compute the minimal elevation angle for a station.
     * ITU-R P.834-7, formula number 10, page 3
     * @param alt altitude of the station at the Earth surface (m)
     * @return the minimal elevation angle (rad)
     */
    private double getMinimalElevation(final double alt) {

        return -FastMath.acos( EARTH_RAY / (EARTH_RAY + alt) * getRefractiveIndex(0.0) / getRefractiveIndex(alt));
    }


    /** Compute the refraction correction in the case of a reference atmosphere.
     * ITU-R P.834-7, formula number 9, page 3
     * @param elevation elevation angle (rad)
     * @return the refraction correction angle (rad)
     */
    private double getTau(final double elevation) {

        final double eld = FastMath.toDegrees(elevation);
        final double tmp0 = CCOEF[0] + CCOEF[1] * eld + CCOEF[2] * eld * eld;
        final double tmp1 = altitude * (CCOEF[3] + CCOEF[4] * eld + CCOEF[5] * eld * eld);
        final double tmp2 = altitude * altitude * CCOEF[6];
        return 1.0 / (tmp0 + tmp1 + tmp2);
    }


    /** Compute the refraction correction in the case of a reference atmosphere.
     * ITU-R P.834-7, formula number 14, page 3
     * @param elevationZero elevation angle (rad)
     * @return the refraction correction angle (rad)
     */

    private double getTauZero(final double elevationZero) {

        final double eld = FastMath.toDegrees(elevationZero);
        final double tmp0 = CCOEF0[0] + CCOEF0[1] * eld + CCOEF0[2] * eld * eld;
        final double tmp1 = altitude * (CCOEF0[3] + CCOEF0[4] * eld + CCOEF0[5] * eld * eld);
        final double tmp2 = altitude * altitude * (CCOEF0[6] + CCOEF0[7] * eld);
        return 1.0 / (tmp0 + tmp1 + tmp2);
    }

    /** Compute the refraction correction in the case of a reference atmosphere without validity domain.
     * The computation is done even if the inequality (formula number 11) is not verified
     * ITU-R P.834-7, formula number 14, page 3
     * @param elevation elevation angle (rad)
     * @return the refraction correction angle (rad)
     */
    private double getBaseRefraction(final double elevation) {
        return getTauZero(elevation);
    }

    /** Get the station minimal elevation angle.
     * @return the minimal elevation angle (rad)
     */
    public double getThetaMin() {
        return thetamin;
    }

    /** Get the station elevation angle under free-space propagation .
     * @return the elevation angle under free-space propagation (rad)
     */
    public double getTheta0() {
        return theta0;
    }

    @Override
    /** {@inheritDoc} */
    // elevation (rad)
    // return refraction correction (rad)
    public double getRefraction(final double elevation) {
        if (elevation < elev_star ) {
            return refrac_star;
        }
        // The validity of the formula is extended for negative elevation,
        // ensuring that the refraction correction angle doesn't make visible a satellite with a too negative elevation
        // elev_star is used instead of thetam (minimal elevation angle).
        return getTauZero(elevation);
    }

}
