/*
 * File:     ExtractTrackDataCommand.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.commands;

import mpi.eudico.client.annotator.ElanLocale;

import mpi.eudico.client.annotator.timeseries.AbstractTSTrack;

import mpi.eudico.client.annotator.util.AnnotationDataRecord;
import mpi.eudico.client.annotator.util.ClientLogger;
import mpi.eudico.client.annotator.util.ProgressListener;

import mpi.eudico.server.corpora.clomimpl.abstr.AlignableAnnotation;
import mpi.eudico.server.corpora.clomimpl.abstr.RefAnnotation;
import mpi.eudico.server.corpora.clomimpl.abstr.TierImpl;
import mpi.eudico.server.corpora.clomimpl.abstr.TranscriptionImpl;

import java.awt.Cursor;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;

import java.util.ArrayList;
import java.util.Locale;


/**
 * A Command that extracts maximum, minimum or average from a timeseries track,
 * based on intervals/annotations on one tier and stores the information  in
 * annotations on a dependent tier.
 *
 * @author Han Sloetjes
 * @version 1.0
 */
public class ExtractTrackDataCommand implements UndoableCommand, ClientLogger {
    private String commandName;
    private ArrayList listeners;
    private TranscriptionImpl transcription;
    private String sourceTierName;
    private String destTierName;
    private ArrayList newAnnos;
    private ArrayList changedAnnos;

    /**
     * Creates a new ExtractTrackDataCommand instance
     *
     * @param commandName the name
     */
    public ExtractTrackDataCommand(String commandName) {
        this.commandName = commandName;
    }

    /**
     * The undo action; newly created annotations are deleted, changed
     * annotations are restored to their old state.
     */
    public void undo() {
        if (transcription != null) {
            TierImpl tier = (TierImpl) transcription.getTierWithId(destTierName);

            if (tier == null) {
                LOG.severe("Tier " + destTierName + " does no longer exist");

                return;
            }

            setWaitCursor(true);

            RefAnnotation ref;
            AnnotationDataRecord record;

            if (newAnnos != null) {
                for (int i = 0; i < newAnnos.size(); i++) {
                    record = (AnnotationDataRecord) newAnnos.get(i);
                    ref = (RefAnnotation) tier.getAnnotationAtTime(record.getBeginTime());

                    if (ref != null) {
                        tier.removeAnnotation(ref);
                    } else {
                        LOG.warning("Could not delete annotation: " +
                            record.getValue() + "bt: " + record.getBeginTime());
                    }
                }
            }

            ValueRecord valRec;

            if (changedAnnos != null) {
                for (int i = 0; i < changedAnnos.size(); i++) {
                    valRec = (ValueRecord) changedAnnos.get(i);
                    ref = (RefAnnotation) tier.getAnnotationAtTime(valRec.beginTime);

                    if (ref != null) {
                        ref.setValue(valRec.oldValue);
                    } else {
                        LOG.warning("Could not find annotation: " +
                            valRec.oldValue + "bt: " + valRec.beginTime);
                    }
                }
            }

            setWaitCursor(false);
        }
    }

    /**
     * The redo action.
     */
    public void redo() {
        if (transcription != null) {
            TierImpl tier = (TierImpl) transcription.getTierWithId(destTierName);

            if (tier == null) {
                LOG.severe("Tier " + destTierName + " does no longer exist");

                return;
            }

            setWaitCursor(true);

            RefAnnotation ref;
            AnnotationDataRecord record;
            long mid;

            if (newAnnos != null) {
                for (int i = 0; i < newAnnos.size(); i++) {
                    record = (AnnotationDataRecord) newAnnos.get(i);
                    ref = (RefAnnotation) tier.getAnnotationAtTime(record.getBeginTime());

                    if (ref == null) {
                        mid = (long) ((record.getBeginTime() +
                            record.getEndTime()) / 2);
                        ref = (RefAnnotation) tier.createAnnotation(mid, mid);

                        if (ref != null) {
                            ref.setValue(record.getValue());
                        } else {
                            LOG.warning(
                                "Could not create a reference annotation at time: " +
                                mid);
                        }
                    } else {
                        LOG.warning("Annotation was not deleted in undo: " +
                            record.getValue() + "bt: " + record.getBeginTime());
                    }
                }
            }

            ValueRecord valRec;

            if (changedAnnos != null) {
                for (int i = 0; i < changedAnnos.size(); i++) {
                    valRec = (ValueRecord) changedAnnos.get(i);
                    ref = (RefAnnotation) tier.getAnnotationAtTime(valRec.beginTime);

                    if (ref != null) {
                        ref.setValue(valRec.newValue);
                    } else {
                        LOG.warning("Could not find annotation: " +
                            valRec.newValue + "bt: " + valRec.beginTime);
                    }
                }
            }

            setWaitCursor(false);
        }
    }

    /**
     * Extracts values from a time series track based on intervals of
     * annotations on one tier and stores the values in annotations on a
     * dependent tier. <b>Note: </b>it is assumed the types and order of the
     * arguments are correct.
     *
     * @param receiver the transcription
     * @param arguments the arguments: <ul><li>arg[0] the source tier
     *        (String)</li> <li>arg[1] the destination tier (String)</li>
     *        <li>arg[2] the track to extract the data from
     *        (AbstractTSTrack/Object)</li> <li>arg[3] what to extract
     *        maximum, minimum, average (String)</li> <li>arg[4] whether or
     *        not to overwrite existing annotation values (Boolean)</li> </ul>
     */
    public void execute(Object receiver, Object[] arguments) {
        transcription = (TranscriptionImpl) receiver;
        sourceTierName = (String) arguments[0];
        destTierName = (String) arguments[1];

        AbstractTSTrack track = (AbstractTSTrack) arguments[2];
        String method = (String) arguments[3];
        boolean overwrite = ((Boolean) arguments[4]).booleanValue();

        TierImpl sourceTier = (TierImpl) transcription.getTierWithId(sourceTierName);

        if (sourceTier == null) {
            progressInterrupt(ElanLocale.getString(
                    "TimeSeriesViewer.Extract.NotFound") + " " +
                sourceTierName);

            return;
        }

        TierImpl destTier = (TierImpl) transcription.getTierWithId(destTierName);

        if (destTier == null) {
            progressInterrupt(ElanLocale.getString(
                    "TimeSeriesViewer.Extract.NotFound") + " " + destTierName);

            return;
        }

        // ?? test sourceTier.isAlignable, sourceTier is ancestorof destTier, 
        // destTier is symbolicassociation ??
        if (track == null) {
            progressInterrupt(ElanLocale.getString(
                    "TimeSeriesViewer.Extract.NotFound") + " " + "track");

            return;
        }

        if (method == null) {
            progressInterrupt(ElanLocale.getString(
                    "TimeSeriesViewer.Extract.NoMethod"));

            return;
        }

        newAnnos = new ArrayList();
        changedAnnos = new ArrayList();

        ExtractThread thread = new ExtractThread(sourceTier, destTier, track,
                method, overwrite);

        try {
            thread.start();
        } catch (Exception e) {
            LOG.warning("Error in extraction progress: " + e.getMessage());
            progressInterrupt("Extraction progress interrupted");
            transcription.setNotifying(true);
        }
    }

    /**
     * Returns the name of the command.
     *
     * @return the command name
     */
    public String getName() {
        return commandName;
    }

    /**
     * Adds a ProgressListener to the list of ProgressListeners.
     *
     * @param pl the new ProgressListener
     */
    public synchronized void addProgressListener(ProgressListener pl) {
        if (listeners == null) {
            listeners = new ArrayList(2);
        }

        listeners.add(pl);
    }

    /**
     * Removes the specified ProgressListener from the list of listeners.
     *
     * @param pl the ProgressListener to remove
     */
    public synchronized void removeProgressListener(ProgressListener pl) {
        if ((pl != null) && (listeners != null)) {
            listeners.remove(pl);
        }
    }

    /**
     * Notifies any listeners of a progress update.
     *
     * @param percent the new progress percentage, [0 - 100]
     * @param message a descriptive message
     */
    private void progressUpdate(int percent, String message) {
        if (listeners != null) {
            for (int i = 0; i < listeners.size(); i++) {
                ((ProgressListener) listeners.get(i)).progressUpdated(this,
                    percent, message);
            }
        }
    }

    /**
     * Notifies any listeners that the process has completed.
     *
     * @param message a descriptive message
     */
    private void progressComplete(String message) {
        if (listeners != null) {
            for (int i = 0; i < listeners.size(); i++) {
                ((ProgressListener) listeners.get(i)).progressCompleted(this,
                    message);
            }
        }
    }

    /**
     * Notifies any listeners that the process has been interrupted.
     *
     * @param message a descriptive message
     */
    private void progressInterrupt(String message) {
        if (listeners != null) {
            for (int i = 0; i < listeners.size(); i++) {
                ((ProgressListener) listeners.get(i)).progressInterrupted(this,
                    message);
            }
        }
    }

    /**
     * Changes the cursor to either a 'busy' cursor or the default cursor.
     *
     * @param showWaitCursor when <code>true</code> show the 'busy' cursor
     */
    private void setWaitCursor(boolean showWaitCursor) {
        if (showWaitCursor) {
            ELANCommandFactory.getRootFrame(transcription).getRootPane()
                              .setCursor(Cursor.getPredefinedCursor(
                    Cursor.WAIT_CURSOR));
        } else {
            ELANCommandFactory.getRootFrame(transcription).getRootPane()
                              .setCursor(Cursor.getDefaultCursor());
        }
    }

    /**
     * Class for minimal storage of a time value and an old and new textual
     * value.
     *
     * @author Han Sloetjes, MPI
     */
    private class ValueRecord {
        /** begin time */
        public final long beginTime;

        /** the old value */
        public final String oldValue;

        /** the new value */
        public final String newValue;

        /**
         * Creates a new ValueRecord instance
         *
         * @param beginTime begin time
         * @param oldValue the old value
         * @param newValue the new value
         */
        ValueRecord(long beginTime, String oldValue, String newValue) {
            this.beginTime = beginTime;
            this.oldValue = oldValue;
            this.newValue = newValue;
        }
    }

    /**
     * The actual extraction is executed in a separate thread; a listener  with
     * a user interface will be able to update itself (progress bar)
     *
     * @author Han Sloetjes, MPI
     */
    private class ExtractThread extends Thread {
        /** the source tier */
        private TierImpl sourceTier;

        /** the destination tier */
        private TierImpl destTier;

        /** the track */
        private AbstractTSTrack track;

        /** id for the calculation method; min, max or ave */
        private String method;

        /** the overwrite flag */
        private boolean overwrite = false;
        private final char[] modes = new char[] { '<', '>', '=' };
        private char mode = modes[0];

        /**
         * Creates a new ExtractThread instance
         *
         * @param sourceTier the source tier
         * @param destTier the destination tier
         * @param track the time series track
         * @param method the calculation method
         * @param overwrite overwrite flag
         */
        ExtractThread(TierImpl sourceTier, TierImpl destTier,
            AbstractTSTrack track, String method, boolean overwrite) {
            super(ExtractThread.class.getName());
            this.sourceTier = sourceTier;
            this.destTier = destTier;
            this.track = track;
            this.method = method;

            if ("max".equalsIgnoreCase(method)) {
                mode = modes[1];
            } else if ("ave".equalsIgnoreCase(method)) {
                mode = modes[2];
            }

            this.overwrite = overwrite;
        }

        /**
         * The actual work
         *
         * @see Runnable#run()
         */
        public void run() {
            transcription.setNotifying(false);

            ArrayList annos = new ArrayList(sourceTier.getAnnotations());
            float perAnn = 100f / annos.size();

            DecimalFormat format = new DecimalFormat("#0.####",
                    new DecimalFormatSymbols(Locale.US));
            AlignableAnnotation aa;
            RefAnnotation ref;
            long begin;
            long end;
            float value = 0f;

            for (int i = 0; i < annos.size(); i++) {
                aa = (AlignableAnnotation) annos.get(i);
                begin = aa.getBeginTimeBoundary();
                end = aa.getEndTimeBoundary();

                try {
                    switch (mode) {
                    case '<':
                        value = track.getMinimum(begin, end);

                        break;

                    case '>':
                        value = track.getMaximum(begin, end);

                        break;

                    case '=':
                        value = track.getAverage(begin, end);
                    }
                } catch (Exception ex) {
                    transcription.setNotifying(true);
                    LOG.warning("Exception occured during calculation: " +
                        ex.getMessage());
                    progressInterrupt("Exception occured during calculation: " +
                        ex.getMessage());

                    return;
                }

                ref = (RefAnnotation) destTier.getAnnotationAtTime(begin);

                String valString;

                if (ref == null) {
                    long mid = (long) ((begin + end) / 2);
                    ref = (RefAnnotation) destTier.createAnnotation(mid, mid);

                    if (ref != null) {
                        //ref.setValue(String.valueOf(value));
                        ref.setValue(format.format(value));

                        // store for undo
                        newAnnos.add(new AnnotationDataRecord(ref));
                    } else {
                        LOG.warning(
                            "Could not create a reference annotation at time: " +
                            mid);
                    }
                } else if (overwrite) {
                    // store for undo
                    // valString = String.valueOf(value);
                    valString = format.format(value);

                    changedAnnos.add(new ValueRecord(begin, ref.getValue(),
                            valString));
                    ref.setValue(valString);
                }

                progressUpdate((int) ((i + 1) * perAnn), null);
            }

            transcription.setNotifying(true);
            progressComplete("Operation complete...");
        }

        /**
         * Interrupts the current merging process.
         */
        public void interrupt() {
            progressInterrupt("Operation interrupted...");
            super.interrupt();
        }
    }
}
