/*
 * File:     ModifyAnnotationTimeCommand.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.util.AnnotationDataRecord;
import mpi.eudico.client.annotator.util.AnnotationRecreator;
import mpi.eudico.client.annotator.util.TimeShiftRecord;

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

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

import java.awt.Cursor;

import java.util.ArrayList;
import java.util.Vector;

import javax.swing.tree.DefaultMutableTreeNode;


/**
 * A command for modifying an annotation's begin and/or end time.
 *
 * @author Han Sloetjes
 */
public class ModifyAnnotationTimeCommand implements UndoableCommand {
    private String commandName;
    private AlignableAnnotation annotation;
    private DefaultMutableTreeNode rootNode;
    private TranscriptionImpl transcription;

    /**
     * note: this presumes that tier object references stay the same even if
     * they have been removed from and re-added to the transcription
     */
    private TierImpl tier;
    private TierImpl rootTier;
    private int timePropMode;
    private int leftOffset;
    private int rightOffset;
    private long oldBeginTime;
    private long oldEndTime;
    private long newBeginTime;
    private long newEndTime;
    private ArrayList removedAnnotations;
    private ArrayList changedAnnotations;

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

    /**
     * <b>Note: </b>it is assumed the types and order of the arguments are
     * correct.
     *
     * @param receiver the Annotation
     * @param arguments the arguments:  <ul><li>arg[0] = the new begin time of
     *        the annotation (Long)</li> <li>arg[1] = the new end time of the
     *        annotation (Long)</li> </ul>
     */
    public void execute(Object receiver, Object[] arguments) {
        if (receiver instanceof AlignableAnnotation) {
            annotation = (AlignableAnnotation) receiver;
        } else {
            return;
        }

        oldBeginTime = annotation.getBeginTimeBoundary();
        oldEndTime = annotation.getEndTimeBoundary();
        newBeginTime = ((Long) arguments[0]).longValue();
        newEndTime = ((Long) arguments[1]).longValue();
        leftOffset = (int) (newBeginTime - oldBeginTime);
        rightOffset = (int) (newEndTime - oldEndTime);

        // only do something if begin and/or end time has changed
        if ((oldBeginTime == newBeginTime) && (oldEndTime == newEndTime)) {
            return;
        }

        tier = (TierImpl) annotation.getTier();

        if (tier.hasParentTier()) {
            rootTier = tier.getRootTier();
        }

        transcription = (TranscriptionImpl) tier.getParent();

        setWaitCursor(true);

        changedAnnotations = new ArrayList();
        removedAnnotations = new ArrayList();

        // store information on all annotations that will be effected
        timePropMode = transcription.getTimeChangePropagationMode();

        switch (timePropMode) {
        case Transcription.NORMAL:
            storeNormal();

            break;

        case Transcription.BULLDOZER:
            storeBulldozer();

            break;

        case Transcription.SHIFT:
            storeShift();
        }

        // finally make the change
        annotation.updateTimeInterval(newBeginTime, newEndTime);

        setWaitCursor(false);
    }

    /**
     * Undo the changes made by this command.
     */
    public void undo() {
        // only do something if begin and/or end time has changed
        if ((oldBeginTime == newBeginTime) && (oldEndTime == newEndTime)) {
            return;
        }

        transcription.setNotifying(false);

        setWaitCursor(true);

        switch (timePropMode) {
        case Transcription.NORMAL:
            restoreNormal();

            break;

        case Transcription.BULLDOZER:
            restoreBulldozer();

            break;

        case Transcription.SHIFT:
            restoreShift();
        }

        transcription.setNotifying(true);

        setWaitCursor(false);
    }

    /**
     * Redo the changes made by this command.
     */
    public void redo() {
        // only do something if begin and/or end time has changed
        if ((oldBeginTime == newBeginTime) && (oldEndTime == newEndTime)) {
            return;
        }

        setWaitCursor(true);

        AlignableAnnotation annotation = (AlignableAnnotation) tier.getAnnotationAtTime(oldBeginTime);

        if (annotation != null) {
            annotation.updateTimeInterval(newBeginTime, newEndTime);
        }

        setWaitCursor(false);
    }

    /**
     * Stores information of all effected annotations in normal time
     * propagation mode. Assumption: no annotations on parenttiers will be
     * effected.
     */
    private void storeNormal() {
        // in case the annotation is on a time-subdivision tier
        // store the whole tree with the parent annotation as root
        if (rootTier != null) {
            AbstractAnnotation aa = (AbstractAnnotation) rootTier.getAnnotationAtTime(oldBeginTime);

            if (aa != null) {
                rootNode = AnnotationRecreator.createTreeForAnnotation(aa);
            }

            return;
        }

        if ((leftOffset > 0) || (rightOffset < 0)) {
            // children could have been destroyed
            rootNode = AnnotationRecreator.createTreeForAnnotation(annotation);
        }

        if (leftOffset < 0) {
            Vector v = tier.getOverlappingAnnotations(newBeginTime, oldBeginTime);

            if (v.size() > 0) {
                AbstractAnnotation ann = (AbstractAnnotation) v.get(0);

                if (ann.getBeginTimeBoundary() < newBeginTime) {
                    // this one will be changed, children might be deleted
                    // so store the whole tree with this annotation as root						
                    changedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            ann));
                } else {
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            ann));
                }

                for (int i = 1; i < v.size(); i++) {
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            (AbstractAnnotation) v.get(i)));
                }
            }
        }

        if (rightOffset > 0) {
            Vector v = tier.getOverlappingAnnotations(oldEndTime, newEndTime);

            if (v.size() > 0) {
                for (int i = 0; i < v.size(); i++) {
                    if (i != (v.size() - 1)) {
                        removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                                (AbstractAnnotation) v.get(i)));
                    } else {
                        //last ann
                        AbstractAnnotation ann = (AbstractAnnotation) v.get(i);

                        if (ann.getEndTimeBoundary() > newEndTime) {
                            changedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                                    ann));
                        } else {
                            removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                                    ann));
                        }
                    }
                }
            }
        }
    }

    /**
     * Restore the situation before the edit action; normal mode.
     */
    private void restoreNormal() {
        if (tier == null) {
            return; //warn??
        }

        int curPropMode = 0;

        curPropMode = transcription.getTimeChangePropagationMode();

        if (curPropMode != Transcription.NORMAL) {
            transcription.setTimeChangePropagationMode(Transcription.NORMAL);
        }

        // in case the annotation is on a time-subdivision tier, first remove the root annotation
        // and then restore it.        
        if (rootTier != null) {
            AbstractAnnotation aa = (AbstractAnnotation) rootTier.getAnnotationAtTime(newBeginTime);

            if (aa != null) {
                rootTier.removeAnnotation(aa);

                AnnotationRecreator.createAnnotationFromTree(transcription,
                    rootNode);
            }
        } else {
            AlignableAnnotation actAnnotation = (AlignableAnnotation) tier.getAnnotationAtTime(newBeginTime);

            // first delete changed annotations
            DefaultMutableTreeNode node;
            AnnotationDataRecord dataRecord;
            AbstractAnnotation aa;

            if (changedAnnotations.size() > 0) {
                for (int i = 0; i < changedAnnotations.size(); i++) {
                    node = (DefaultMutableTreeNode) changedAnnotations.get(i);
                    dataRecord = (AnnotationDataRecord) node.getUserObject();

                    if (dataRecord.getBeginTime() < oldBeginTime) {
                        aa = (AbstractAnnotation) tier.getAnnotationAtTime(dataRecord.getBeginTime());
                    } else {
                        aa = (AbstractAnnotation) tier.getAnnotationAtTime(dataRecord.getEndTime() -
                                1);
                    }

                    if (aa != null) {
                        tier.removeAnnotation(aa);
                    }
                }
            }

            if ((leftOffset > 0) || (rightOffset < 0)) {
                // children could have been destroyed
                if (actAnnotation != null) {
                    tier.removeAnnotation(actAnnotation);
                }

                AnnotationRecreator.createAnnotationFromTree(transcription,
                    rootNode);
            } else {
                if (actAnnotation != null) {
                    actAnnotation.updateTimeInterval(oldBeginTime, oldEndTime);
                }
            }

            // now recreate all annotations that have been deleted
            if (changedAnnotations.size() > 0) {
                for (int i = 0; i < changedAnnotations.size(); i++) {
                    node = (DefaultMutableTreeNode) changedAnnotations.get(i);
                    AnnotationRecreator.createAnnotationFromTree(transcription,
                        node);
                }
            }

            if (removedAnnotations.size() > 0) {
                for (int i = 0; i < removedAnnotations.size(); i++) {
                    node = (DefaultMutableTreeNode) removedAnnotations.get(i);
                    AnnotationRecreator.createAnnotationFromTree(transcription,
                        node);
                }
            }
        }

        // restore the time propagation mode
        transcription.setTimeChangePropagationMode(curPropMode);
    }

    /**
     * Stores information of all effected annotations in bulldozer time
     * propagation mode. Assumption: no annotations on parenttiers will be
     * effected.
     */
    private void storeBulldozer() {
        // in case the annotation is on a time-subdivision tier
        // store the whole tree with the parent annotation as root
        if (rootTier != null) {
            storeNormal();

            return;
        }

        if ((leftOffset > 0) || (rightOffset < 0)) {
            // children could have been destroyed
            rootNode = AnnotationRecreator.createTreeForAnnotation(annotation);
        }

        // annotations will only be shifted; store the times...
        // store annotations in the order they need to be restored
        // check left side....
        Vector annos = tier.getAnnotations();

        if (annos == null) {
            return;
        }

        if (leftOffset < 0) {
            AbstractAnnotation cur = annotation;
            AbstractAnnotation prev;
            int index = annos.indexOf(annotation);
            int gapToBridge = -leftOffset;
            int gapBridged = 0;

            for (int i = index - 1; i >= 0; i--) {
                prev = (AbstractAnnotation) annos.get(i);

                int dist = (int) (cur.getBeginTimeBoundary() -
                    prev.getEndTimeBoundary());
                gapBridged += dist;

                if (gapBridged < gapToBridge) {
                    // check if the new begintime will be less than zero: annotations could be lost
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            prev));

                    changedAnnotations.add(new TimeShiftRecord(
                            prev.getBeginTimeBoundary(),
                            prev.getEndTimeBoundary(),
                            -(gapToBridge - gapBridged)));

                    cur = prev;
                } else {
                    break;
                }
            }
        }

        // check right side
        if (rightOffset > 0) {
            AbstractAnnotation cur = annotation;
            AbstractAnnotation next;
            int index = annos.indexOf(annotation);
            int gapToBridge = rightOffset;
            int gapBridged = 0;

            for (int i = index + 1; i < annos.size(); i++) {
                next = (AbstractAnnotation) annos.get(i);

                int dist = (int) (next.getBeginTimeBoundary() -
                    cur.getEndTimeBoundary());
                gapBridged += dist;

                if (gapBridged < gapToBridge) {
                    changedAnnotations.add(new TimeShiftRecord(
                            next.getBeginTimeBoundary(),
                            next.getEndTimeBoundary(), gapToBridge -
                            gapBridged));
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            next));
                    cur = next;
                } else {
                    break;
                }
            }
        }
    }

    /**
     * Restores information of all effected annotations in bulldozer time
     * propagation mode. First restore the annotation that was originally
     * changed by the user,  next shift the annotations to the left and to the
     * right back to their original locations.
     */
    private void restoreBulldozer() {
        if (tier == null) {
            return; //warn??
        }

        int curPropMode = 0;

        curPropMode = transcription.getTimeChangePropagationMode();

        if (curPropMode != Transcription.NORMAL) {
            transcription.setTimeChangePropagationMode(Transcription.NORMAL);
        }

        // in case the annotation is on a time-subdivision tier, first remove the root annotation
        // and then restore it.
        if (rootTier != null) {
            restoreNormal();
        } else {
            AlignableAnnotation actAnnotation = (AlignableAnnotation) tier.getAnnotationAtTime(newBeginTime);

            if ((leftOffset > 0) || (rightOffset < 0)) {
                // children could have been destroyed
                if (actAnnotation != null) {
                    tier.removeAnnotation(actAnnotation);
                    AnnotationRecreator.createAnnotationFromTree(transcription,
                        rootNode);
                }
            } else {
                // restore the original, edited annotation
                if (actAnnotation != null) {
                    actAnnotation.updateTimeInterval(oldBeginTime, oldEndTime);
                }
            }

            if (changedAnnotations.size() > 0) {
                TimeShiftRecord tsRecord;
                AlignableAnnotation aa;
                DefaultMutableTreeNode node;

                for (int i = 0; i < changedAnnotations.size(); i++) {
                    tsRecord = (TimeShiftRecord) changedAnnotations.get(i);
                    aa = (AlignableAnnotation) tier.getAnnotationAtTime(tsRecord.newBegin);
                    node = (DefaultMutableTreeNode) removedAnnotations.get(i);

                    if (aa != null) {
                        tier.removeAnnotation(aa);
                        AnnotationRecreator.createAnnotationFromTree(transcription,
                            node);
                    }
                }
            }
        }

        // restore the time propagation mode
        transcription.setTimeChangePropagationMode(curPropMode);
    }

    /**
     * Stores information on all effected annotations in shift propagation
     * mode. On a time subdivision tier shift mode is identical to normal
     * mode. On root tiers the changes on the left side are treated the same
     * way as in normal mode, changes on the right side shift all time slots
     * on  the right side of the selection to the right.
     */
    private void storeShift() {
        // in case the annotation is on a time-subdivision tier
        // store the whole tree with the parent annotation as root
        if (rootTier != null) {
            storeNormal();

            return;
        }

        // we are on a root tier
        if ((leftOffset > 0) || (rightOffset < 0)) {
            // children could have been destroyed
            rootNode = AnnotationRecreator.createTreeForAnnotation(annotation);
        }

        // copy from storeNormal for the left side
        if (leftOffset < 0) {
            Vector v = tier.getOverlappingAnnotations(newBeginTime, oldBeginTime);

            if (v.size() > 0) {
                AbstractAnnotation ann = (AbstractAnnotation) v.get(0);

                if (ann.getBeginTimeBoundary() < newBeginTime) {
                    // this one will be changed, children might be deleted
                    // so store the whole tree with this annotation as root						
                    changedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            ann));
                } else {
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            ann));
                }

                for (int i = 1; i < v.size(); i++) {
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            (AbstractAnnotation) v.get(i)));
                }
            }
        }
    }

    /**
     * Restores information of all effected annotations in shift time
     * propagation mode. On root tiers first delete all changed annotations,
     * then  shift the time slots back and finally recreate/restore any
     * removed annotations.
     */
    private void restoreShift() {
        if (tier == null) {
            return; //warn??
        }

        int curPropMode = 0;

        curPropMode = transcription.getTimeChangePropagationMode();

        if (curPropMode != Transcription.NORMAL) {
            transcription.setTimeChangePropagationMode(Transcription.NORMAL);
        }

        // in case the annotation is on a time-subdivision tier, first remove the root annotation
        // and then restore it.
        if (rootTier != null) {
            restoreNormal();
        } else {
            // we are on a root tier
            AlignableAnnotation actAnnotation = (AlignableAnnotation) tier.getAnnotationAtTime(newBeginTime);

            // first delete changed annotations on the left side of the current annotation
            DefaultMutableTreeNode node;
            AnnotationDataRecord dataRecord;
            AbstractAnnotation aa;

            if (changedAnnotations.size() > 0) {
                for (int i = 0; i < changedAnnotations.size(); i++) {
                    node = (DefaultMutableTreeNode) changedAnnotations.get(i);
                    dataRecord = (AnnotationDataRecord) node.getUserObject();
                    aa = (AbstractAnnotation) tier.getAnnotationAtTime(dataRecord.getBeginTime());

                    if (aa != null) {
                        tier.removeAnnotation(aa);
                    }
                }
            }

            if ((leftOffset > 0) || (rightOffset < 0)) {
                // children could have been destroyed
                if (actAnnotation != null) {
                    tier.removeAnnotation(actAnnotation);
                }
            }

            if (rightOffset > 0) {
                // shift the slots backward	
                transcription.shiftBackward(oldEndTime, -rightOffset);
            }

            if ((leftOffset > 0) || (rightOffset < 0)) {
                AnnotationRecreator.createAnnotationFromTree(transcription,
                    rootNode);
            } else {
                // restore/update the original, edited annotation
                if (actAnnotation != null) {
                    actAnnotation.updateTimeInterval(oldBeginTime, oldEndTime);
                }
            }

            // now recreate removed and shifted annotations
            if (changedAnnotations.size() > 0) {
                for (int i = 0; i < changedAnnotations.size(); i++) {
                    node = (DefaultMutableTreeNode) changedAnnotations.get(i);
                    AnnotationRecreator.createAnnotationFromTree(transcription,
                        node);
                }
            }

            if (removedAnnotations.size() > 0) {
                for (int i = 0; i < removedAnnotations.size(); i++) {
                    node = (DefaultMutableTreeNode) removedAnnotations.get(i);
                    AnnotationRecreator.createAnnotationFromTree(transcription,
                        node);
                }
            }
        }

        // restore the time propagation mode
        transcription.setTimeChangePropagationMode(curPropMode);
    }

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

    /**
     * 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());
        }
    }
}
