/*
 * Copyright 2015 Andrew T. Flowers, K0SM
 * 
 * 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 dopplerpsk.event;

import dopplerpsk.*;
import java.io.File;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.orekit.bodies.BodyShape;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.bodies.OneAxisEllipsoid;
import org.orekit.data.DataProvidersManager;
import org.orekit.data.DirectoryCrawler;
import org.orekit.errors.OrekitException;
import org.orekit.frames.Frame;
import org.orekit.frames.FramesFactory;
import org.orekit.frames.TopocentricFrame;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.analytical.tle.TLE;
import org.orekit.propagation.analytical.tle.TLEPropagator;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.TimeScalesFactory;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;

/**
 * Periodically calculates spacecraft position and Doppler attributes.  This
 * class relies of the SGP4 algorithm in Orekit.
 *
 * @author Andrew T. Flowers
 */ 
public class OrekitSatelliteEventSource implements ISatelliteEventSource { 

    // Earth constants and reference frame
    private static final double ae = Constants.WGS84_EARTH_EQUATORIAL_RADIUS;
    private static final double f = Constants.WGS84_EARTH_FLATTENING;
    private static final double c = 299792458 * (1.0 / 1.000293);  //speed of light
    private final Frame IRTF2008;
    private final TLEPropagator prop;
    private final BodyShape earth;
    private final GeodeticPoint station;

    private final Set<ISatelliteEventSink> listeners;

    private final long updateInterval;  //in milliseconds

    private Timer timer;
    private final double uplinkFreq;
    private final String satName;

    /**
     * Periodically calculates spacecraft position and Doppler attributes.  This
     * class relies of the SGP4 algorithm in Orekit.
     * 
     * @param sat the satellite to track
     * @param stationLat ground station latitude in degrees
     * @param stationLon ground station longitude in degrees
     * @param stationAltMeters ground station altitude in meters ASL
     * @param uplinkFreq uplink frequency in Hz
     * @param updateIntervalSeconds determines how often to fire updates to 
     * listeners.  Additionally this parameters specifies the duration of the 
     * piecewise estimation of Doppler shift.
     * 
     * @throws OrekitException 
     */
    public OrekitSatelliteEventSource(
           Satellite sat,
            double stationLat,
            double stationLon,
            double stationAltMeters,
            double uplinkFreq,  //freq of transmitter in Hz
            double updateIntervalSeconds) throws OrekitException {
        
        
        listeners = new HashSet();
        
        this.updateInterval = (long) (updateIntervalSeconds * 1000);
        this.uplinkFreq = uplinkFreq;
        // ensure Orekit is configured with leap seconds.
        DataProvidersManager.getInstance().addProvider(new DirectoryCrawler(new File(".")));

        IRTF2008 = FramesFactory.getITRF(IERSConventions.IERS_2010, true);
        earth = new OneAxisEllipsoid(ae, f, IRTF2008);
        this.satName = sat.getName();
        station = new GeodeticPoint(stationLat, stationLon, stationAltMeters);
        TLE tle = new TLE(sat.getTleLine1(), sat.getTleLine2());
        prop = TLEPropagator.selectExtrapolator(tle);

    }

    /**
     * Starts the periodic update process.
     */
    public synchronized void start() {
        if (timer != null) {
            timer.cancel();
        }
        timer = new Timer();
        TimerTask task = new TimerTask() {
            public void run() {
                try {
                    calc();
                } catch (OrekitException ex) {
                    Logger.getLogger(OrekitSatelliteEventSource.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        };

        timer.scheduleAtFixedRate(task, 0, updateInterval);

    }

    /** 
     * stops the periodic update process.
     */
    public synchronized void stop() {
        if (timer != null) {
            timer.cancel();
        }
    }

    /**
     * Calculates spacecraft position and Doppler attributes and fires an
     * update to any listeners.
     * @throws OrekitException 
     */
    private void calc() throws OrekitException {


        TopocentricFrame stationFrame = new TopocentricFrame(earth, station, "earth station");

        Date nowDate = new Date();
        Date futureDate = new Date(nowDate.getTime() + updateInterval);
        AbsoluteDate now = new AbsoluteDate(nowDate, TimeScalesFactory.getUTC());
        AbsoluteDate future = new AbsoluteDate(futureDate, TimeScalesFactory.getUTC());

        SpacecraftState currentState = prop.propagate(now);
        SpacecraftState futureState = prop.propagate(future);

        // Gather data for current state (now).  We'll get the AZ/EL since that
        // is useful information for any listeners that might want to display
        // the current location of the satellite.
        
        // Note that Orekit works in Radians.
        
        double el = Math.toDegrees(
                stationFrame.getElevation(
                    currentState.getPVCoordinates().getPosition(),
                    currentState.getFrame(), 
                    currentState.getDate()
                )
        );
        double az = Math.toDegrees(
                stationFrame.getAzimuth(
                        currentState.getPVCoordinates().getPosition(),
                        currentState.getFrame(), 
                        currentState.getDate()
                )
        );
        
        double currentRangeRate = stationFrame.getRangeRate(
                currentState.getPVCoordinates(), 
                currentState.getFrame(), 
                currentState.getDate()
        );

        //Gather data for future state.  We only need the range rate for the
        //Doppler calculation.
        
        double futureRangeRate = stationFrame.getRangeRate(
                futureState.getPVCoordinates(), 
                futureState.getFrame(), 
                futureState.getDate()
        );

        // With the range rates we can calculate doppler shift now and in
        // the future.
        double currentdf = (((c + currentRangeRate) / c) * uplinkFreq) - uplinkFreq; //in Hz
        double futuredf = (((c + futureRangeRate) / c) * uplinkFreq) - uplinkFreq;  //in Hz

        // Calculate ddf (change in doppler shift) in Hz/s.  This is the rate of
        // doppler shift over the next time inteval.  
        double ddf = (futuredf - currentdf) / (updateInterval / 1000.0);  

        //Fire an event to any listeners
        SatelliteUpdateEvent evt = new SatelliteUpdateEvent(satName, az, el, currentdf, ddf);
        updateListeners(evt);

    }

    private synchronized void updateListeners(SatelliteUpdateEvent evt) {
        System.out.println(evt);
        for (ISatelliteEventSink s : listeners) {
            s.SatelliteUpdateEventOccured(evt);
        }
    }

    public synchronized void addEventListener(ISatelliteEventSink snk) {
        listeners.add(snk);
    }
    
    public synchronized void removeEventListener(ISatelliteEventSink snk) {
        listeners.remove(snk);
    }
}
