/*
 * File:     TSTrackManager.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.client.annotator.timeseries;

import mpi.eudico.client.annotator.commands.ELANCommandFactory;

import mpi.eudico.client.annotator.timeseries.config.TSSourceConfiguration;
import mpi.eudico.client.annotator.timeseries.config.TSTrackConfiguration;
import mpi.eudico.client.annotator.timeseries.io.TSConfigurationEncoder;
import mpi.eudico.client.annotator.timeseries.spi.TSServiceProvider;
import mpi.eudico.client.annotator.timeseries.spi.TSServiceRegistry;

import mpi.eudico.client.annotator.viewer.TimeSeriesViewer;

import mpi.eudico.server.corpora.clom.Transcription;

import mpi.eudico.server.corpora.clomimpl.abstr.LinkedFileDescriptor;
import mpi.eudico.server.corpora.clomimpl.abstr.TranscriptionImpl;

import java.awt.Component;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.Vector;


/**
 * Manages time series tracks, their sources and configurations  and one or
 * more TimeSeriesViewers.
 *
 * @author Han Sloetjes
 */
public class TSTrackManager implements TimeSeriesChangeListener {
    private TranscriptionImpl transcription;

    /** stores all defined tracks, from all source files */
    private ArrayList tracks;

    // sourcename - configuration mappings
    private HashMap trackSourceConfigs;
    private ArrayList listeners;
    private TSConfigurationEncoder encoder;
    private TSViewerPlayer syncPlayer;

    /**
     * Creates a new TSTrackManager instance
     *
     * @param transcription the transcription, the document identifier
     */
    public TSTrackManager(Transcription transcription) {
        this.transcription = (TranscriptionImpl) transcription;
        tracks = new ArrayList();
        trackSourceConfigs = new HashMap();
        encoder = new TSConfigurationEncoder();

        //encoder.encodeAndSave((TranscriptionImpl) transcription);
    }

    /**
     * Returns the list of all tracks from all sources that have been
     * associated with the document.
     *
     * @return the tracks that have been added to the manager
     */
    public ArrayList getRegisteredTracks() {
        return tracks;
    }

    /**
     * Returns the track with the specified name, if defined.
     *
     * @param name the name of the track
     *
     * @return the track or null
     */
    public AbstractTSTrack getTrack(String name) {
        if (name == null) {
            return null;
        }

        AbstractTSTrack tr;

        for (int i = 0; i < tracks.size(); i++) {
            tr = (AbstractTSTrack) tracks.get(i);

            if (tr.getName().equals(name)) {
                return tr;
            }
        }

        return null;
    }

    /**
     * Returns a key set of the source configurations that have been added to
     * the manager.
     *
     * @return the set of source configuration keys
     */
    public Set getConfigKeySet() {
        return trackSourceConfigs.keySet();
    }

    /**
     * Returns the source url's of the currently managed sources.
     *
     * @return a String array of source url's
     */
    public String[] getCurrentSourceNames() {
        try {
            return (String[]) getConfigKeySet().toArray(new String[] {  });
        } catch (Exception ex) {
            return new String[0];
        }
    }

    /**
     * Sets the offset of the specified time series source. All tracks configured from that
     * source receive the same offset.
     *
     * @param source the source identifier (source url)
     * @param offset the new offset for the ts source
     */
    public void setOffset(String source, int offset) {
        if (source == null) {
            return;
        }

        TSSourceConfiguration configuration = (TSSourceConfiguration) trackSourceConfigs.get(source);

        if (configuration != null) {
            configuration.setTimeOrigin(offset);

            // update the transcription
            Vector lfds = transcription.getLinkedFileDescriptors();

            for (int j = 0; j < lfds.size(); j++) {
                LinkedFileDescriptor lfd = (LinkedFileDescriptor) lfds.get(j);

                if (lfd.linkURL.equals(source)) {
                    lfd.timeOrigin = offset;
                    transcription.setChanged();

                    break;
                }
            }

            Iterator keyIt = configuration.objectKeySet().iterator();

            while (keyIt.hasNext()) {
                Object key = keyIt.next();
                Object o = configuration.getObject(key);

                if (o instanceof AbstractTSTrack) {
                    ((AbstractTSTrack) o).setTimeOffset(offset);
                } else if (o instanceof TSTrackConfiguration) { // default

                    TSTrackConfiguration tstc = (TSTrackConfiguration) o;
                    Object tr = tstc.getObject(tstc.getTrackName());

                    if (tr instanceof AbstractTSTrack) {
                        ((AbstractTSTrack) tr).setTimeOffset(offset); // default situation
                    } else {
                        Iterator objIt = tstc.objectKeySet().iterator();

                        while (objIt.hasNext()) {
                            Object ke = objIt.next();
                            Object oo = tstc.getObject(ke);

                            if (oo instanceof AbstractTSTrack) {
                                ((AbstractTSTrack) oo).setTimeOffset(offset);
                            }
                        }
                    }
                }
            }

            notifyListeners(new TimeSeriesChangeEvent(configuration,
                    TimeSeriesChangeEvent.CHANGE,
                    TimeSeriesChangeEvent.TS_SOURCE));
            encoder.encodeAndSave((TranscriptionImpl) transcription,
                trackSourceConfigs.values());
        }
    }

    /**
     * Returns the time offset (origin) of the specified source
     *
     * @param source the source url string (id)
     *
     * @return the offset of the specified timeseries source and thus the offset
     * of all tracks originating from that source
     */
    public int getOffset(String source) {
        if (source == null) {
            return 0;
        }

        TSSourceConfiguration configuration = (TSSourceConfiguration) trackSourceConfigs.get(source);

        if (configuration != null) {
            return configuration.getTimeOrigin();
        }

        return 0;
    }

    /**
     * If there is a configurable track source show the configuration dialog.
     * If there are more than one sources show a selection dialog with the
     * sources. Otherwise just show a message dialog that there is nothing to
     * configure.
     *
     * @param parent the parent component
     */
    public void configureTracks(Component parent) {
        TSServiceRegistry registry = TSServiceRegistry.getInstance();
        TSServiceProvider provider;
        ArrayList configurables = new ArrayList(3);
        Iterator confIt = getConfigKeySet().iterator();
        TSSourceConfiguration config;

        while (confIt.hasNext()) {
            config = (TSSourceConfiguration) trackSourceConfigs.get(confIt.next());

            if (config.getProviderClassName() != null) {
                provider = registry.getProviderByClassName(config.getProviderClassName());
            } else {
                provider = registry.getProviderForFile(config.getSource());
            }

            if (provider != null) {
                if (provider.isConfigurable()) {
                    configurables.add(config);
                }
            }
        }

        TSConfigurationUI cui = new TSConfigurationUI();

        switch (configurables.size()) {
        case 0:

            // show message
            cui.showNoConfigMessage(parent);

            break;

        case 1:

            // show config for the one
            cui.showConfigDialog(parent,
                (TSSourceConfiguration) configurables.get(0), this);

            break;

        default:

            // show selection option pane
            TSSourceConfiguration cfg = cui.selectConfigurableSource(parent,
                    configurables);

            if (cfg != null) {
                cui.showConfigDialog(parent, cfg, this);
            }

            break;
        }
    }

    /**
     * Informs the track manager about the creation of a new track from the
     * specified source file. The track(configuration) might or might not have
     * been added to the source configuration.  In the latter case it is added
     * to the source config here.  The track is added to the list of
     * registered tracks and listeners are notified.
     *
     * @param sourceConfig the configuration of the source file
     * @param trackConfig the configuration of the track
     */
    public void addTrack(TSSourceConfiguration sourceConfig,
        TSTrackConfiguration trackConfig) {
        if ((sourceConfig == null) || (trackConfig == null)) {
            return;
        }

        // check if the track has been added to the source configuration
        if (sourceConfig.getObject(trackConfig.getTrackName()) == null) {
            sourceConfig.putObject(trackConfig.getTrackName(), trackConfig);
        }

        Object tr = trackConfig.getObject(trackConfig.getTrackName());

        if (tr instanceof TimeSeriesTrack) {
            tracks.add(tr); // default situation
        } else {
            Iterator objIt = trackConfig.objectKeySet().iterator();

            while (objIt.hasNext()) {
                Object key = objIt.next();
                Object oo = trackConfig.getObject(key);

                if (oo instanceof TimeSeriesTrack) {
                    tracks.add(oo);
                }
            }
        }

        notifyListeners(new TimeSeriesChangeEvent(trackConfig,
                TimeSeriesChangeEvent.ADD, TimeSeriesChangeEvent.TRACK));
    }

    /**
     * Informs the track manager about the deletion of a track from the
     * specified source file. The track(configuration) might or might not have
     * been removed from the source configuration.  In the latter case it is
     * removed from the source config here.  The track is removed from the
     * list of registered tracks and listeners are notified.
     *
     * @param sourceConfig the configuration of the source file
     * @param trackConfig the configuration of the track
     */
    public void removeTrack(TSSourceConfiguration sourceConfig,
        TSTrackConfiguration trackConfig) {
        if ((sourceConfig == null) || (trackConfig == null)) {
            return;
        }

        // check if the track has been removed from the source configuration
        if (sourceConfig.getObject(trackConfig.getTrackName()) != null) {
            sourceConfig.removeObject(trackConfig.getTrackName());
        }

        Object tr = trackConfig.getObject(trackConfig.getTrackName());

        if (tr instanceof TimeSeriesTrack) {
            tracks.remove(tr); // default situation
        } else {
            Iterator objIt = trackConfig.objectKeySet().iterator();

            while (objIt.hasNext()) {
                Object key = objIt.next();
                Object oo = trackConfig.getObject(key);

                if (oo instanceof TimeSeriesTrack) {
                    tracks.remove(oo);
                }
            }
        }

        notifyListeners(new TimeSeriesChangeEvent(trackConfig,
                TimeSeriesChangeEvent.DELETE, TimeSeriesChangeEvent.TRACK));
    }

    /**
     * Removes a track from the list of registered tracks.
     *
     * @param sourceConfig the configuration of the source file
     * @param track the track object
     *
     * @see #removeTrack(TSSourceConfiguration, TSTrackConfiguration)
     */
    public void removeTrack(TSSourceConfiguration sourceConfig,
        TimeSeriesTrack track) {
        if ((sourceConfig == null) || (track == null)) {
            return;
        }

        // check if the track has been removed from the source configuration
        if (sourceConfig.getObject(track.getName()) != null) {
            sourceConfig.removeObject(track.getName());
        }

        tracks.remove(track);

        notifyListeners(new TimeSeriesChangeEvent(track,
                TimeSeriesChangeEvent.DELETE, TimeSeriesChangeEvent.TRACK));
    }

    /**
     * Called when a track configuration has changed.
     *
     * @param trackConfig the track configuration
     */
    public void trackChanged(TSTrackConfiguration trackConfig) {
        if (trackConfig == null) {
            return;
        }

        // check if the track is in the list
        AbstractTSTrack track = null;
        Object tr = trackConfig.getObject(trackConfig.getTrackName());

        if (tr instanceof AbstractTSTrack) {
            track = (AbstractTSTrack) tr;

            if (tracks.contains(track)) {
                notifyListeners(new TimeSeriesChangeEvent(track,
                        TimeSeriesChangeEvent.CHANGE,
                        TimeSeriesChangeEvent.TRACK));
            }
        } else {
            Iterator objIt = trackConfig.objectKeySet().iterator();

            while (objIt.hasNext()) {
                Object key = objIt.next();
                Object oo = trackConfig.getObject(key);

                if (oo instanceof AbstractTSTrack) {
                    track = (AbstractTSTrack) oo;

                    if (tracks.contains(track)) {
                        notifyListeners(new TimeSeriesChangeEvent(track,
                                TimeSeriesChangeEvent.CHANGE,
                                TimeSeriesChangeEvent.TRACK));
                    }
                }
            }
        }
    }

    /**
     * Called when a track has changed.
     *
     * @param track the track
     */
    public void trackChanged(TimeSeriesTrack track) {
        if (track == null) {
            return;
        }

        if (tracks.contains(track)) {
            // notify listeners; listeners might have to check what has changed
            notifyListeners(new TimeSeriesChangeEvent(track,
                    TimeSeriesChangeEvent.CHANGE, TimeSeriesChangeEvent.TRACK));
        }
    }

    /**
     * Generic request to send a notification to listeners
     *
     * @param type ADD, CHANGE, DELETE
     */
    public void tsSourceChanged(int type) {
        notifyListeners(new TimeSeriesChangeEvent(this, type,
                TimeSeriesChangeEvent.TS_SOURCE));
        encoder.encodeAndSave(transcription, trackSourceConfigs.values());
    }

    /**
     * Adds a timeseries source configuration object to the manager. Any track
     * objects found through this source configuration will be added to the
     * list of managed tracks.
     *
     * @param configuration a configuration object associated with a single
     *        source (file)
     */
    public void addTrackSource(TSSourceConfiguration configuration) {
        if (configuration != null) {
            trackSourceConfigs.put(configuration.getSource(), configuration);

            // add tracks to the list
            Iterator keyIt = configuration.objectKeySet().iterator();

            while (keyIt.hasNext()) {
                Object key = keyIt.next();
                Object o = configuration.getObject(key);

                if (o instanceof TimeSeriesTrack) {
                    tracks.add(o);
                } else if (o instanceof TSTrackConfiguration) { // default

                    TSTrackConfiguration tstc = (TSTrackConfiguration) o;
                    Object tr = tstc.getObject(tstc.getTrackName());

                    if (tr instanceof TimeSeriesTrack) {
                        tracks.add(tr); // default situation
                    } else {
                        Iterator objIt = tstc.objectKeySet().iterator();

                        while (objIt.hasNext()) {
                            Object ke = objIt.next();
                            Object oo = tstc.getObject(ke);

                            if (oo instanceof TimeSeriesTrack) {
                                tracks.add(oo);
                            }
                        }
                    }
                }
            }

            if (syncPlayer == null) {
                TimeSeriesViewer tsViewer = ELANCommandFactory.getViewerManager(transcription)
                                                              .createTimeSeriesViewer();

                //ELANCommandFactory.getViewerManager(transcription).disableViewer(tsViewer);
                tsViewer.setTrackManager(this);
                syncPlayer = new TSViewerPlayer(tsViewer,
                        ELANCommandFactory.getViewerManager(transcription)
                                          .getMasterMediaPlayer()
                                          .getMediaDuration());
                syncPlayer.setTrackManager(this); // adds the sources
                ELANCommandFactory.getViewerManager(transcription)
                                  .addMediaPlayer(syncPlayer);
                ELANCommandFactory.getLayoutManager(transcription).add(syncPlayer);
                ELANCommandFactory.getViewerManager(transcription)
                                  .destroyMediaPlayer(syncPlayer);
            }

            notifyListeners(new TimeSeriesChangeEvent(configuration,
                    TimeSeriesChangeEvent.ADD, TimeSeriesChangeEvent.TS_SOURCE));
            encoder.encodeAndSave((TranscriptionImpl) transcription,
                trackSourceConfigs.values());
        }
    }

    /**
     * Removes a source configuration from the managers map
     *
     * @param configuration a configuration object
     *
     * @see #removeTrackSource(String)
     */
    public void removeTrackSource(TSSourceConfiguration configuration) {
        if (configuration != null) {
            removeTrackSource(configuration.getSource());
        }
    }

    /**
     * Removes a timeseries source configuration object from the manager. Any
     * track objects found through this source configuration will be removed
     * from the list of managed tracks.
     *
     * @param source name of the configuration (=path)
     */
    public void removeTrackSource(String source) {
        if (source != null) {
            TSSourceConfiguration configuration = (TSSourceConfiguration) trackSourceConfigs.remove(source);

            if (configuration != null) {
                Iterator keyIt = configuration.objectKeySet().iterator();

                while (keyIt.hasNext()) {
                    Object key = keyIt.next();
                    Object o = configuration.getObject(key);

                    if (o instanceof TimeSeriesTrack) {
                        tracks.remove(o);
                    } else if (o instanceof TSTrackConfiguration) { // default

                        TSTrackConfiguration tstc = (TSTrackConfiguration) o;
                        Object tr = tstc.getObject(tstc.getTrackName());

                        if (tr instanceof TimeSeriesTrack) {
                            tracks.remove(tr); // default situation
                        } else {
                            Iterator objIt = tstc.objectKeySet().iterator();

                            while (objIt.hasNext()) {
                                Object ke = objIt.next();
                                Object oo = tstc.getObject(ke);

                                if (oo instanceof TimeSeriesTrack) {
                                    tracks.remove(oo);
                                }
                            }
                        }
                    }

                    notifyListeners(new TimeSeriesChangeEvent(o,
                            TimeSeriesChangeEvent.DELETE,
                            TimeSeriesChangeEvent.TRACK));
                }
            }

            notifyListeners(new TimeSeriesChangeEvent(configuration,
                    TimeSeriesChangeEvent.DELETE,
                    TimeSeriesChangeEvent.TS_SOURCE));

            if (trackSourceConfigs.isEmpty()) {
                if (syncPlayer != null) {
                    ELANCommandFactory.getLayoutManager(transcription).remove(syncPlayer);
                    ELANCommandFactory.getViewerManager(transcription)
                                      .destroyViewer(syncPlayer.getViewer());
                    ELANCommandFactory.getViewerManager(transcription)
                                      .destroyMediaPlayer(syncPlayer);
                    syncPlayer = null;
                }
            }
        }

        encoder.encodeAndSave(transcription, trackSourceConfigs.values());
    }

    /**
     * Called after a change in configuration settings.
     *
     * @param configuration the changed configuration
     */
    public void trackSourceChanged(TSSourceConfiguration configuration) {
        if (configuration == null) {
            return;
        }

        // check if the config is in the list
        if (trackSourceConfigs.get(configuration.getSource()) != null) {
            // check if the time origins of the tracks in this config need to be updated
            int origin = configuration.getTimeOrigin();
            AbstractTSTrack track = null;

            Iterator keyIt = configuration.objectKeySet().iterator();

            while (keyIt.hasNext()) {
                Object key = keyIt.next();
                Object o = configuration.getObject(key);

                if (o instanceof AbstractTSTrack) {
                    track = (AbstractTSTrack) o;

                    if (track.getTimeOffset() != origin) {
                        track.setTimeOffset(origin);
                        notifyListeners(new TimeSeriesChangeEvent(track,
                                TimeSeriesChangeEvent.CHANGE,
                                TimeSeriesChangeEvent.TRACK));
                    }
                } else if (o instanceof TSTrackConfiguration) { // default

                    TSTrackConfiguration tstc = (TSTrackConfiguration) o;
                    Object tr = tstc.getObject(tstc.getTrackName());

                    if (tr instanceof AbstractTSTrack) {
                        track = (AbstractTSTrack) tr;

                        if (track.getTimeOffset() != origin) {
                            track.setTimeOffset(origin);
                            notifyListeners(new TimeSeriesChangeEvent(track,
                                    TimeSeriesChangeEvent.CHANGE,
                                    TimeSeriesChangeEvent.TRACK));
                        }
                    } else {
                        Iterator objIt = tstc.objectKeySet().iterator();

                        while (objIt.hasNext()) {
                            Object ke = objIt.next();
                            Object oo = tstc.getObject(ke);

                            if (oo instanceof AbstractTSTrack) {
                                track = (AbstractTSTrack) oo;

                                if (track.getTimeOffset() != origin) {
                                    track.setTimeOffset(origin);
                                    notifyListeners(new TimeSeriesChangeEvent(
                                            track,
                                            TimeSeriesChangeEvent.CHANGE,
                                            TimeSeriesChangeEvent.TRACK));
                                }
                            }
                        }
                    }
                }
            }

            notifyListeners(new TimeSeriesChangeEvent(configuration,
                    TimeSeriesChangeEvent.CHANGE,
                    TimeSeriesChangeEvent.TS_SOURCE));
            encoder.encodeAndSave(transcription, trackSourceConfigs.values());
        }
    }

    /**
     * Adds a listener to the list of time series change listeners.
     *
     * @param li the listener
     */
    public void addTimeSeriesChangeListener(TimeSeriesChangeListener li) {
        if (listeners == null) {
            listeners = new ArrayList();
        }

        listeners.add(li);
    }

    /**
     * Removes the listeners from the list
     *
     * @param li the listener
     */
    public void removeTimeSeriesChangeListener(TimeSeriesChangeListener li) {
        if (listeners != null) {
            listeners.remove(li);
        }
    }

    /**
     * Dispatches the event to all interested in time series change events
     *
     * @param event the time series change event
     */
    private void notifyListeners(TimeSeriesChangeEvent event) {
        if (listeners != null) {
            for (int i = 0; i < listeners.size(); i++) {
                ((TimeSeriesChangeListener) listeners.get(i)).timeSeriesChanged(event);
            }
        }
    }

    /**
     * Dispatches events to the registered listeners.
     *
     * @param event DOCUMENT ME!
     */
    public void timeSeriesChanged(TimeSeriesChangeEvent event) {
        if (event.getEditSourceType() == TimeSeriesChangeEvent.TRACK) {
            if (event.getSource() instanceof TSTrackConfiguration) {
                TSTrackConfiguration trc = (TSTrackConfiguration) event.getSource();
                Object tr = trc.getObject(trc.getTrackName());

                if (event.getEditType() == TimeSeriesChangeEvent.ADD) {
                    if (!tracks.contains(tr)) {
                        tracks.add(tr);
                    }
                } else if (event.getEditType() == TimeSeriesChangeEvent.DELETE) {
                    tracks.remove(tr);
                }
            }
        }

        notifyListeners(event);
        encoder.encodeAndSave(transcription, trackSourceConfigs.values());
    }
}
