package net.i2p.util;

import java.util.Date;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import net.i2p.I2PAppContext;
import net.i2p.time.BuildTime;
import net.i2p.time.Timestamper;

/**
 * Alternate location for determining the time which takes into account an offset.
 * This offset will ideally be periodically updated so as to serve as the difference
 * between the local computer's current time and the time as known by some reference
 * (such as an NTP synchronized clock).
 *
 * Protected members are used in the subclass RouterClock,
 * which has access to a router's transports (particularly peer clock skews)
 * to second-guess the sanity of clock adjustments.
 *
 */
public class Clock implements Timestamper.UpdateListener {
    protected final I2PAppContext _context;
    protected final boolean _isSystemClockBad;
    protected long _startedOn;
    protected boolean _statCreated;
    protected volatile long _offset;
    protected boolean _alreadyChanged;
    private final Set<ClockUpdateListener> _listeners;
    
    public Clock(I2PAppContext context) {
        _context = context;
        _listeners = new CopyOnWriteArraySet<ClockUpdateListener>();
        long now = System.currentTimeMillis();
        long min = BuildTime.getEarliestTime();
        long max = BuildTime.getLatestTime();
        // If the system clock is obviously bad, set our offset so our time is something "close"
        // We do not call setOffset() here as it sets _alreadyChanged.
        // Don't use Log here.
        if (now < min) {
            // positive offset
            _offset = min - now;
            System.out.println("ERROR: System clock is invalid: " + new Date(now));
            now = min;
            _isSystemClockBad = true;
        } else if (now > max) {
            // negative offset
            _offset = max - now;
            System.out.println("ERROR: System clock is invalid: " + new Date(now));
            now = max;
            _isSystemClockBad = true;
        } else {
            _isSystemClockBad = false;
        }
        _startedOn = now;
    }

    public static Clock getInstance() {
        return I2PAppContext.getGlobalContext().clock();
    }
    
    /**
     *  This is a dummy, see RouterClock and RouterTimestamper for the real thing
     */
    public Timestamper getTimestamper() { return new Timestamper(); }
    
    /** we fetch it on demand to avoid circular dependencies (logging uses the clock) */
    protected Log getLog() { return _context.logManager().getLog(Clock.class); }

    /** if the clock is skewed by 3+ days, forget it */
    public final static long MAX_OFFSET = 3 * 24 * 60 * 60 * 1000;
    /** after we've started up and shifted the clock, don't allow shifts of more than 10 minutes */
    public final static long MAX_LIVE_OFFSET = 10 * 60 * 1000;
    /** if the clock skewed changes by less than this, ignore the update (so we don't slide all over the place) */
    public final static long MIN_OFFSET_CHANGE = 5 * 1000;

    /**
     * Specify how far away from the "correct" time the computer is - a positive
     * value means that the system time is slow, while a negative value means the system time is fast.
     *
     * @param offsetMs the delta from System.currentTimeMillis() (NOT the delta from now())
     */
    public void setOffset(long offsetMs) {
        setOffset(offsetMs, false);        
    }
    
    /**
     * Specify how far away from the "correct" time the computer is - a positive
     * value means that the system time is slow, while a negative value means the system time is fast.
     *
     * Warning - overridden in RouterClock
     *
     * @param offsetMs the delta from System.currentTimeMillis() (NOT the delta from now())
     */
    public synchronized void setOffset(long offsetMs, boolean force) {
        long delta = offsetMs - _offset;
        if (!force) {
            if (!_isSystemClockBad && (offsetMs > MAX_OFFSET || offsetMs < 0 - MAX_OFFSET)) {
                Log log = getLog();
                if (log.shouldLog(Log.WARN))
                    log.warn("Maximum offset shift exceeded [" + offsetMs + "], NOT HONORING IT");
                return;
            }
            
            // only allow substantial modifications before the first 10 minutes
            if (_alreadyChanged && (System.currentTimeMillis() - _startedOn > 10 * 60 * 1000)) {
                if ( (delta > MAX_LIVE_OFFSET) || (delta < 0 - MAX_LIVE_OFFSET) ) {
                    Log log = getLog();
                    if (log.shouldLog(Log.WARN))
                        log.warn("The clock has already been updated, but you want to change it by "
                                           + delta + " to " + offsetMs + "?  Did something break?");
                    return;
                }
            }
            
            if ((delta < MIN_OFFSET_CHANGE) && (delta > 0 - MIN_OFFSET_CHANGE)) {
                Log log = getLog();
                if (log.shouldLog(Log.DEBUG))
                    log.debug("Not changing offset since it is only " + delta + "ms");
                _alreadyChanged = true;
                return;
            }
        }
        if (_alreadyChanged) {
            if (delta > 15*1000)
                getLog().log(Log.CRIT, "Updating clock offset to " + offsetMs + "ms from " + _offset + "ms");
            else if (getLog().shouldLog(Log.INFO))
                getLog().info("Updating clock offset to " + offsetMs + "ms from " + _offset + "ms");
            
            if (!_statCreated) {
                _context.statManager().createRateStat("clock.skew", "Clock step adjustment (ms)", "Clock", new long[] { 60*60*1000 });
                _statCreated = true;
            }
            _context.statManager().addRateData("clock.skew", delta, 0);
        } else {
            Log log = getLog();
            if (log.shouldLog(Log.INFO))
                log.info("Initializing clock offset to " + offsetMs + "ms from " + _offset + "ms");
        }
        _alreadyChanged = true;
        _offset = offsetMs;
        fireOffsetChanged(delta);
    }

    /*
     * @return the current delta from System.currentTimeMillis() in milliseconds
     */
    public synchronized long getOffset() {
        return _offset;
    }
    
    public boolean getUpdatedSuccessfully() { return _alreadyChanged; }
    
    
    public void setNow(long realTime) {
        if (realTime < BuildTime.getEarliestTime() || realTime > BuildTime.getLatestTime()) {
            Log log = getLog();
            String msg = "Invalid time received: " + new Date(realTime);
            if (log.shouldWarn())
                log.warn(msg, new Exception());
            else
                log.logAlways(Log.WARN, msg);
            return;
        }
        long diff = realTime - System.currentTimeMillis();
        setOffset(diff);
    }

    /**
     *  Warning - overridden in RouterClock
     *
     *  @param stratum ignored
     *  @since 0.7.12
     */
    public void setNow(long realTime, int stratum) {
        setNow(realTime);
    }

    /**
     * Retrieve the current time synchronized with whatever reference clock is in
     * use.
     *
     */
    public long now() {
        return _offset + System.currentTimeMillis();
    }

    public void addUpdateListener(ClockUpdateListener lsnr) {
            _listeners.add(lsnr);
    }

    public void removeUpdateListener(ClockUpdateListener lsnr) {
            _listeners.remove(lsnr);
    }

    protected void fireOffsetChanged(long delta) {
            for (ClockUpdateListener lsnr : _listeners) {
                lsnr.offsetChanged(delta);
            }
    }

    public interface ClockUpdateListener {

        /**
         *  @param delta = (new offset - old offset),
         *         where each offset = (now() - System.currentTimeMillis())
         */
        public void offsetChanged(long delta);
    }
}
