package com.jdblabs.timestamper.core;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.jdbernard.util.SmartConfig;

/**
 * Represents a Timeline configuration. A configuration has one primary, or
 * local, Timeline with an associated TimelineSource, and a list of remote
 * Timelines synched to the local Timeline.
 * <br>
 * <table>
 * <tr><th>Property</th><th>Description</th></tr>
 * <tr><td><code>timeline.uri</code></td><td>The URI of the primary (local)
 *      timeline</td></tr>
 * <tr><td><code>timeline.persistOnUpdate?</code></td><td><code>true</code> to
 *      persist the local timeline when a new event is entered,
 *      <code>false</code> to disable.</td></tr>
 * <tr><td><code>remote.timeline.</code><i>name</i><code>.uri</code></td>
 *      <td>The URI for the <i>name</i> remote timeline.</td></tr>
 * <tr><td><code>remote.timeline.</code><i>name</i><code>.push?</code></td>
 *      <td><code>true</code> to enable pushing updates to the <i>name</i>
 *      remote timeline, <code>false</code> to disable.</td></tr>
 * <tr><td><code>remote.timeline.</code><i>name</i><code>.pull?</code></td>
 *      <td><code>true</code> to enable pulling updates from the <i>name</i>
 *      remote timeline, <code>false</code> to disable.</td></tr>
 * <tr><td><code>remote.timeline.</code><i>name</i><code>.syncOnExit?</code>
 *      </td><td><code>true</code> to force sync the <i>name</i> remote
 *      timeline on exit.</td></tr>
 * <tr><td><code>remote.timeline.</code><i>name</i>
 *      <code>.updateInterval</code></td><td>The time in milliseconds between
 *      synching the <i>name</i> remote timeline.</td></tr></table>
 * @author Jonathan Bernard ({@literal jdbernard@gmail.com})
 * @see com.jdbernard.timestamper.core.Timeline
 * @see com.jdbernard.timestamper.core.TimelineSource
 * @see com.jdbernard.timestamper.core.SyncTarget
 */
public class TimelineProperties {

    public static final String LOCAL_TIMELINE_URI = "timeline.uri";
    public static final String LOCAL_TIMELINE_PERSIST_ON_UPDATE = "timeline.persistOnUpdate?";
    public static final String REMOTE_TIMELINE_BASE = "remote.timeline.";

    private static final Pattern remoteTimelinePropPattern = 
            Pattern.compile("\\Q" + REMOTE_TIMELINE_BASE + "\\E([^\\s\\.=]+?)[\\.=].*");

    private SmartConfig config;
    private Timeline timeline;
    private TimelineSource timelineSource;
    private LinkedList<SyncTarget> syncTargets = new LinkedList<SyncTarget>();
    private boolean persistOnUpdate;

    /**
     * Create new TimelineProperties, using default values. This will create
     * a new configuration using a FileTimelineSource pointed at
     * <code>'timeline.default.txt'</code> in the current directory and no
     * remote Timelines. It will save this configuration to 
     * <code>'timeline.default.properties'</code> in the current directory.
     */
    public TimelineProperties() {
        File propertyFile = new File("timeline.default.properties");
        config = new SmartConfig(propertyFile);

        File timelineFile = new File("timeline.default.txt");
        URI timelineURI = timelineFile.toURI();

        timeline = new Timeline();
        timelineSource = TimelineSourceFactory.newInstance(timelineURI, config);
        persistOnUpdate = true;
        try { timelineSource.persist(timeline); }
        catch (IOException ioe) {
            // TODO
        }

        config.setProperty(LOCAL_TIMELINE_URI, timelineURI);
        config.setProperty(LOCAL_TIMELINE_PERSIST_ON_UPDATE, persistOnUpdate);
    }

    /**
     * Load TimelineProperties from a config file.
     * @param propFile
     */
    public TimelineProperties(File propFile) throws IOException {
        String strURI;
        URI timelineURI;

        config = new SmartConfig(propFile);

        // load persist on update information
        persistOnUpdate = (Boolean) config.getProperty(
            LOCAL_TIMELINE_PERSIST_ON_UPDATE, true);

        // get the URI for the primary timeline
        strURI = (String) config.getProperty(LOCAL_TIMELINE_URI, "");

        // no primary timeline, default to file-based timeline
        if ("".equals(strURI)) {
            File defaultTimelineFile = new File("timeline.default.txt");
            try {
                if (!defaultTimelineFile.exists())
                    defaultTimelineFile.createNewFile();
            } catch (IOException ioe) {
                // TODO
            }
            timelineURI = defaultTimelineFile.toURI();
        } else { // we do have a URI
            try { timelineURI = new URI(strURI); }
            catch (URISyntaxException urise) {
                throw new IOException("Unable to load the timeline: the timeline "
                        + "URI is invalid.", urise);
            }
        }

        // create our timeline source and read the timeline
        timelineSource = TimelineSourceFactory.newInstance(timelineURI, config);
        timeline = timelineSource.read();

        // search keys for remote timeline entries
        for (Object keyObj : config.keySet()) {
            if (!(keyObj instanceof String)) continue;

            String key = (String) keyObj;
            String stName;
            String remoteBase;
            SyncTarget st;

            Matcher m = remoteTimelinePropPattern.matcher(key);
            if (!m.matches()) continue;

            stName = m.group(1);

            // skip if we have already setup this remote sync
            boolean stExists = false;
            for (SyncTarget target : syncTargets)
                if (target.getName().equals(stName))
                    stExists = true;
            if (stExists) continue;

            remoteBase = REMOTE_TIMELINE_BASE + stName;

            strURI = (String) config.getProperty(remoteBase + ".uri", "");
            try { timelineURI = new URI(strURI); }
            catch (URISyntaxException urise) { /* TODO */ }

            // add a new SyncTarget to the list
            st = new SyncTarget(stName, TimelineSourceFactory
                    .newInstance(timelineURI, config), timeline);
            syncTargets.add(st);

            // check for synch options
            st.setPullEnabled((Boolean)config.getProperty(
                remoteBase + ".pull?", true));
            st.setPushEnabled((Boolean)config.getProperty(
                remoteBase + ".push?", true));
            st.setSyncOnExit((Boolean)config.getProperty(
                remoteBase + ".syncOnExit?", true));
            st.setSyncInterval((Long)config.getProperty(
                remoteBase + ".updateInterval", 1800000l)); // thirty minutes

        }
    }

    public void save() throws IOException {
        timelineSource.persist(timeline);

        config.setProperty(LOCAL_TIMELINE_PERSIST_ON_UPDATE, persistOnUpdate);
        config.setProperty(LOCAL_TIMELINE_URI, timelineSource.getURI());

        for (SyncTarget st : syncTargets) {
            String remoteBase = REMOTE_TIMELINE_BASE + st.getName();
            config.setProperty(remoteBase + ".uri", st.getSource().getURI());
            config.setProperty(remoteBase + ".pull?", st.getPullEnabled());
            config.setProperty(remoteBase + ".push?", st.getPushEnabled());
            config.setProperty(remoteBase + ".syncOnExit?", st.getSyncOnExit());
            config.setProperty(remoteBase + ".updateInterval",
                    st.getSyncInterval());
        }
    }

    public Timeline reloadTimeline() throws IOException {
        timeline = timelineSource.read();
        return timeline;
    }

    public Timeline getTimeline() { return timeline; }

    public void setTimelineSource(TimelineSource newSource) {
        this.timelineSource = newSource;
    }

    public TimelineSource getTimelineSource() { return timelineSource; }

    public Collection<SyncTarget> getSyncTargets() { return syncTargets; }

    public boolean getPersistOnUpdate() { return persistOnUpdate; }

    public void setPersistOnUpdate(boolean persistOnUpdate) {
        this.persistOnUpdate = persistOnUpdate;
        if (persistOnUpdate) try { save(); } catch (IOException ioe) {}
    }
}
