package com.jdblabs.timestamper.core;

import java.io.IOException;
import java.util.Collection;
import java.util.Timer;
import java.util.TimerTask;

/**
 * A remote target synchronized against the local timeline.
 * @author Jonathan Bernard (jdbernard@gmail.com)
 */
public class SyncTarget {

    protected final TimelineSource source;
    protected final Timeline localTimeline;
    protected final String name;

    protected Timer syncTimer;
    protected SyncTask syncTask;

    protected long syncInterval= 30 * 60 * 1000; // 30 minutes converted to ms
    protected boolean pushEnabled = true;
    protected boolean pullEnabled = true;
    protected boolean syncOnExit = true;

    /**
     *
     */
    protected class SyncTask extends TimerTask {
        @Override public void run() {
            synchronized(this) {
                try { SyncTarget.this.sync(); }
                catch (IOException ioe) { /* TODO */ }
            }}}

    protected SyncTarget(String name, TimelineSource source,
    Timeline localTimeline) {
        this.name = name;
        this.source = source;
        this.localTimeline = localTimeline;

        syncTimer = new Timer(source.toString() + " sync-timer");
        syncTask = new SyncTask();
        syncTimer.schedule(syncTask, syncInterval, syncInterval);
    }

    /**
     * Sync the local timeline with the remote timeline represented by this
     * SyncTarget object.
     * @return <b>true</b> if the two timelines were out of sync and have
     * been put into synch, <b>false</b> if the timelines were already in
     * sync and no action was requried.
     * @throws IOException if there is an error communicating with the remote
     * timeline. This includes AuthenticationException.
     */
    protected boolean sync() throws IOException {
        assert (pullEnabled || pushEnabled);

        Timeline remoteTimeline;
        boolean syncPerformed = false;

        // make sure we're authenticated to whatever source we're using
        if (!SyncTarget.this.source.isAuthenticated()) {
            SyncTarget.this.source.authenticate();
        }

        // try to copy the remote timeline locally
        remoteTimeline = SyncTarget.this.source.read();

        // if we are pulling markers from the remote line
        if (SyncTarget.this.pullEnabled) {
            // get all markers in the remote timeline not in the local one
            Collection<TimelineMarker> diffFromRemote =
                    remoteTimeline.difference(localTimeline);

            if (diffFromRemote.size() != 0) {
                // add all markers in the remote tline to the local one
                localTimeline.addAll(diffFromRemote);
                syncPerformed = true;
            }
        }

        // if we are pushing markers to the remote timeline
        if (SyncTarget.this.pushEnabled) {
            // get all markers in the local timeline but not in the remote
            Collection<TimelineMarker> diffFromLocal =
                    localTimeline.difference(remoteTimeline);

            if (diffFromLocal.size() != 0) {
                // add the difference to the remote timeline
                remoteTimeline.addAll(diffFromLocal);
                syncPerformed = true;
            }

            // try to persist the updated remote timeline
            SyncTarget.this.source.persist(remoteTimeline);
        }

        return syncPerformed;
    }

    public void shutdown() throws IOException {
        // TODO: move this onto the timer thread?
        if (syncOnExit) sync();

        syncTimer.cancel();
        syncTimer.purge();
    }

    public String getName() { return name; }

    public TimelineSource getSource() { return source; }

    public synchronized void setSyncInterval(long syncInterval) {
        this.syncInterval= syncInterval;

        syncTask.cancel();
        syncTask = new SyncTask();

        syncTimer.purge();
        syncTimer.schedule(syncTask, syncInterval, syncInterval);
    }

    public long getSyncInterval() { return syncInterval; }

    public synchronized void setPushEnabled(boolean pushEnabled) {
        this.pullEnabled = pushEnabled;
    }

    public boolean getPushEnabled() { return pushEnabled; }

    public synchronized void setPullEnabled(boolean pullEnabled) {
        this.pullEnabled = pullEnabled;
    }

    public boolean getPullEnabled() { return pullEnabled; }

    public synchronized void setSyncOnExit(boolean syncOnExit) {
        this.syncOnExit = syncOnExit;
    }

    public boolean getSyncOnExit() { return syncOnExit; }
}
