/*
 * File:     NewAnnotationCommand.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.ViewerManager2;

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.Annotation;
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 the creation of new annotations on a tier.<br>
 * Should become undoable eventually. Because existing annotations  can be
 * destroyed when creating a new annotation undo /redo is not  yet
 * implemented.
 *
 * @author Han Sloetjes
 */
public class NewAnnotationCommand implements UndoableCommand {
    private String commandName;
    private TierImpl tier;
    private TierImpl rootTier;
    private int timePropMode;
    private TranscriptionImpl transcription;
    private Annotation newAnnotation;
    private long begin;
    private long end;
    private long newAnnBegin;
    private long newAnnEnd;
    private ArrayList removedAnnotations;
    private ArrayList changedAnnotations;
    private int leftOffset;
    private int rightOffset;

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

    /**
     * The undo action.
     */
    public void undo() {
        if ((tier != null) && (newAnnotation != null)) {
            setWaitCursor(true);

            Annotation aa = tier.getAnnotationAtTime((newAnnBegin + newAnnEnd) / 2);

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

            if (tier.isTimeAlignable()) {
                transcription.setNotifying(false);

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

                    break;

                case Transcription.BULLDOZER:
                    restoreBulldozer();

                    break;

                case Transcription.SHIFT:
                    restoreShift();
                }

                transcription.setNotifying(true);
            }

            setWaitCursor(false);
        }
    }

    /**
     * The redo action.
     */
    public void redo() {
        if (tier != null) {
            setWaitCursor(true);

            newAnnotation = tier.createAnnotation(begin, end);

            setWaitCursor(false);
        }
    }

    /**
     * <b>Note: </b>it is assumed the types and order of the arguments are
     * correct.
     *
     * @param receiver the TierImpl
     * @param arguments the arguments:  <ul><li>arg[0] = the viewermanager
     *        (ViewerManager2)</li> <li>arg[1] = the begin time of the
     *        annotation (Long)</li> <li>arg[2] = the end time of the
     *        annotation (Long)</li> </ul>
     */
    public void execute(Object receiver, Object[] arguments) {
        tier = (TierImpl) receiver;
        begin = ((Long) arguments[1]).longValue();
        end = ((Long) arguments[2]).longValue();

        ViewerManager2 vm = (ViewerManager2) arguments[0];
        transcription = (TranscriptionImpl) vm.getTranscription();

        Command c = ELANCommandFactory.createCommand(transcription,
                ELANCommandFactory.ACTIVE_ANNOTATION);
        c.execute(vm, new Object[] { null });
        setWaitCursor(true);

        if (!tier.isTimeAlignable()) {
            // symbolic subdivision or symbolic association
            // nothing gets lost
            newAnnotation = tier.createAnnotation(begin, end);
        } else {
            changedAnnotations = new ArrayList();
            removedAnnotations = new ArrayList();

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

            timePropMode = transcription.getTimeChangePropagationMode();

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

                break;

            case Transcription.BULLDOZER:
                storeBulldozer();

                break;

            case Transcription.SHIFT:
                storeShift();
            }

            // finally create the annotation
            newAnnotation = tier.createAnnotation(begin, end);
        }

        if (newAnnotation != null) {
            newAnnBegin = newAnnotation.getBeginTimeBoundary();
            newAnnEnd = newAnnotation.getEndTimeBoundary();
        }

        setWaitCursor(false);
    }

    /**
     * Stores information of all effected annotations in normal time
     * propagation mode. Assumption: no annotations on parenttiers will be
     * effected.
     */
    private void storeNormal() {
        if (rootTier != null) {
            Vector possiblyEffectedAnn = rootTier.getOverlappingAnnotations(begin,
                    end);
            AbstractAnnotation aa;

            // use the changedAnnotations arraylist
            for (int i = 0; i < possiblyEffectedAnn.size(); i++) {
                aa = (AbstractAnnotation) possiblyEffectedAnn.get(i);
                changedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                        aa));
            }
        } else {
            Vector effectedAnn = tier.getOverlappingAnnotations(begin, end);
            AbstractAnnotation aa;

            for (int i = 0; i < effectedAnn.size(); i++) {
                aa = (AbstractAnnotation) effectedAnn.get(i);

                if (aa.getBeginTimeBoundary() < begin) {
                    changedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            aa));
                } else if (aa.getEndTimeBoundary() > end) {
                    changedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            aa));
                } else {
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            aa));
                }
            }
        }
    }

    /**
     * Restore the situation before the edit action; normal mode.
     */
    private void restoreNormal() {
        int curPropMode = 0;

        curPropMode = transcription.getTimeChangePropagationMode();

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

        if (rootTier != null) {
            long mid = (newAnnBegin + newAnnEnd) / 2;
            DefaultMutableTreeNode node = null;
            AnnotationDataRecord annRecord = null;

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

                if ((annRecord.getBeginTime() <= mid) &&
                        (annRecord.getEndTime() >= mid)) {
                    break;
                }
            }

            if (node == null) {
                return;
            }

            Annotation rootAnn = rootTier.getAnnotationAtTime(mid);

            if (rootAnn != null) {
                rootTier.removeAnnotation(rootAnn);
                AnnotationRecreator.createAnnotationFromTree(transcription, node);
            }
        } else {
            // 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() < begin) {
                        aa = (AbstractAnnotation) tier.getAnnotationAtTime(dataRecord.getBeginTime());
                    } else {
                        aa = (AbstractAnnotation) tier.getAnnotationAtTime(dataRecord.getEndTime() -
                                1);
                    }

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

            // 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.<br>
     * On root tiers: <br>
     * if the selection begin is within the boundaries of an existing
     * annotation this annotation will be shifted to the left; all other
     * overlapped  annotations will be shifted to the right. Information on
     * effected  annotations is stored in this order: first the annotation
     * that is shifted  to the left, then all other annotations on the left
     * side that will be  bulldozered (descending), then all annotations that
     * will be shifted to  the right in ascending order.
     */
    private void storeBulldozer() {
        if (rootTier != null) {
            // creation of an annotation on a time-subdivision tier
            // same as normal mode
            storeNormal();
        } else {
            // creation of an annotation on a root tier
            Vector effectedAnn = tier.getOverlappingAnnotations(begin, end);
            Vector allAnn = tier.getAnnotations();
            int index;

            if (effectedAnn.size() > 0) {
                Annotation aa;

                // check for bulldozer shift on the left + right side
                aa = (Annotation) effectedAnn.get(0);
                index = allAnn.indexOf(aa);

                if ((aa.getBeginTimeBoundary() <= begin) &&
                        (aa.getEndTimeBoundary() > begin)) {
                    leftOffset = (int) (begin - aa.getEndTimeBoundary());

                    if (end <= aa.getEndTimeBoundary()) {
                        rightOffset = 0;
                    } else {
                        if (effectedAnn.size() >= 2) {
                            aa = (Annotation) effectedAnn.get(1);
                            rightOffset = (int) (end -
                                aa.getBeginTimeBoundary());
                        }
                    }
                } else {
                    leftOffset = 0;
                    rightOffset = (int) (end - aa.getBeginTimeBoundary());
                }

                // store info, start on the left
                if (leftOffset < 0) {
                    AbstractAnnotation cur = (AbstractAnnotation) allAnn.get(index);
                    AbstractAnnotation prev;
                    int gapToBridge = -leftOffset;
                    int gapBridged = 0;

                    changedAnnotations.add(new TimeShiftRecord(
                            cur.getBeginTimeBoundary(),
                            cur.getEndTimeBoundary(), leftOffset));
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            cur));

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

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

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

                if (rightOffset > 0) {
                    if (leftOffset < 0) {
                        index++;
                    }

                    AbstractAnnotation cur = (AbstractAnnotation) allAnn.get(index);
                    AbstractAnnotation next;
                    int gapToBridge = rightOffset;
                    int gapBridged = 0;

                    changedAnnotations.add(new TimeShiftRecord(
                            cur.getBeginTimeBoundary(),
                            cur.getEndTimeBoundary(), rightOffset));
                    removedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            cur));

                    for (int i = index + 1; i < allAnn.size(); i++) {
                        next = (AbstractAnnotation) allAnn.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 deletes the annotation that was originally
     * created by the user,  next deletes the shifted annotations and then
     * recreates the original annotations.
     */
    private void restoreBulldozer() {
        if (rootTier != null) {
            // creation of an annotation on a time-subdivision tier
            // same as normal mode
            restoreNormal();
        } else {
            // creation of an annotation on a root tier
            int curPropMode = 0;

            curPropMode = transcription.getTimeChangePropagationMode();

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

            // work
            // delete the created annotation
            Annotation createdAnn = tier.getAnnotationAtTime(begin);

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

            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.
     */
    private void storeShift() {
        if (rootTier != null) {
            // time subdivision
            storeNormal();
        } else {
            Vector effectedAnn = tier.getOverlappingAnnotations(begin, end);
            AbstractAnnotation aa;

            if (effectedAnn.size() > 0) {
                aa = (AbstractAnnotation) effectedAnn.get(0);

                if ((aa.getBeginTimeBoundary() < begin) &&
                        (aa.getEndTimeBoundary() > begin)) {
                    changedAnnotations.add(AnnotationRecreator.createTreeForAnnotation(
                            aa));
                }
            }
        }
    }

    /**
     * Restores information of all effected annotations in shift time-
     * propagation mode.
     */
    private void restoreShift() {
        int curPropMode = 0;

        curPropMode = transcription.getTimeChangePropagationMode();

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

        if (rootTier != null) {
            // shift the slots backward	
            transcription.shiftBackward(begin, -(end - begin));
            restoreNormal();
        } else {
            DefaultMutableTreeNode node = null;

            if (changedAnnotations.size() > 0) {
                Annotation aa;
                node = (DefaultMutableTreeNode) changedAnnotations.get(0);

                AnnotationDataRecord annRecord = (AnnotationDataRecord) node.getUserObject();
                long begin = annRecord.getBeginTime();

                aa = tier.getAnnotationAtTime(begin);

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

            // shift the slots backward	
            transcription.shiftBackward(begin, -(end - begin));

            if (node != null) {
                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());
        }
    }
}
