/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package android.media.videoeditor;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import android.graphics.Bitmap;
import android.graphics.Rect;
import android.media.videoeditor.MediaImageItem;
import android.media.videoeditor.MediaItem;
import android.media.MediaMetadataRetriever;
import android.util.Log;
import android.util.Xml;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.os.Debug;
import android.os.SystemProperties;
import android.os.Environment;

/**
 * The VideoEditor implementation {@hide}
 */
public class VideoEditorImpl implements VideoEditor {
    /*
     *  Logging
     */
    private static final String TAG = "VideoEditorImpl";

    /*
     *  The project filename
     */
    private static final String PROJECT_FILENAME = "videoeditor.xml";

    /*
     *  XML tags
     */
    private static final String TAG_PROJECT = "project";
    private static final String TAG_MEDIA_ITEMS = "media_items";
    private static final String TAG_MEDIA_ITEM = "media_item";
    private static final String TAG_TRANSITIONS = "transitions";
    private static final String TAG_TRANSITION = "transition";
    private static final String TAG_OVERLAYS = "overlays";
    private static final String TAG_OVERLAY = "overlay";
    private static final String TAG_OVERLAY_USER_ATTRIBUTES = "overlay_user_attributes";
    private static final String TAG_EFFECTS = "effects";
    private static final String TAG_EFFECT = "effect";
    private static final String TAG_AUDIO_TRACKS = "audio_tracks";
    private static final String TAG_AUDIO_TRACK = "audio_track";

    private static final String ATTR_ID = "id";
    private static final String ATTR_FILENAME = "filename";
    private static final String ATTR_AUDIO_WAVEFORM_FILENAME = "waveform";
    private static final String ATTR_RENDERING_MODE = "rendering_mode";
    private static final String ATTR_ASPECT_RATIO = "aspect_ratio";
    private static final String ATTR_REGENERATE_PCM = "regeneratePCMFlag";
    private static final String ATTR_TYPE = "type";
    private static final String ATTR_DURATION = "duration";
    private static final String ATTR_START_TIME = "start_time";
    private static final String ATTR_BEGIN_TIME = "begin_time";
    private static final String ATTR_END_TIME = "end_time";
    private static final String ATTR_VOLUME = "volume";
    private static final String ATTR_BEHAVIOR = "behavior";
    private static final String ATTR_DIRECTION = "direction";
    private static final String ATTR_BLENDING = "blending";
    private static final String ATTR_INVERT = "invert";
    private static final String ATTR_MASK = "mask";
    private static final String ATTR_BEFORE_MEDIA_ITEM_ID = "before_media_item";
    private static final String ATTR_AFTER_MEDIA_ITEM_ID = "after_media_item";
    private static final String ATTR_COLOR_EFFECT_TYPE = "color_type";
    private static final String ATTR_COLOR_EFFECT_VALUE = "color_value";
    private static final String ATTR_START_RECT_LEFT = "start_l";
    private static final String ATTR_START_RECT_TOP = "start_t";
    private static final String ATTR_START_RECT_RIGHT = "start_r";
    private static final String ATTR_START_RECT_BOTTOM = "start_b";
    private static final String ATTR_END_RECT_LEFT = "end_l";
    private static final String ATTR_END_RECT_TOP = "end_t";
    private static final String ATTR_END_RECT_RIGHT = "end_r";
    private static final String ATTR_END_RECT_BOTTOM = "end_b";
    private static final String ATTR_LOOP = "loop";
    private static final String ATTR_MUTED = "muted";
    private static final String ATTR_DUCK_ENABLED = "ducking_enabled";
    private static final String ATTR_DUCK_THRESHOLD = "ducking_threshold";
    private static final String ATTR_DUCKED_TRACK_VOLUME = "ducking_volume";
    private static final String ATTR_GENERATED_IMAGE_CLIP = "generated_image_clip";
    private static final String ATTR_IS_IMAGE_CLIP_GENERATED = "is_image_clip_generated";
    private static final String ATTR_GENERATED_TRANSITION_CLIP = "generated_transition_clip";
    private static final String ATTR_IS_TRANSITION_GENERATED = "is_transition_generated";
    private static final String ATTR_OVERLAY_RGB_FILENAME = "overlay_rgb_filename";
    private static final String ATTR_OVERLAY_FRAME_WIDTH = "overlay_frame_width";
    private static final String ATTR_OVERLAY_FRAME_HEIGHT = "overlay_frame_height";
    private static final String ATTR_OVERLAY_RESIZED_RGB_FRAME_WIDTH = "resized_RGBframe_width";
    private static final String ATTR_OVERLAY_RESIZED_RGB_FRAME_HEIGHT = "resized_RGBframe_height";
    private static final int ENGINE_ACCESS_MAX_TIMEOUT_MS = 500;
    /*
     *  Instance variables
     */
    private final Semaphore mLock;
    private final String mProjectPath;
    private final List<MediaItem> mMediaItems = new ArrayList<MediaItem>();
    private final List<AudioTrack> mAudioTracks = new ArrayList<AudioTrack>();
    private final List<Transition> mTransitions = new ArrayList<Transition>();
    private long mDurationMs;
    private int mAspectRatio;

    /*
     * Private Object for calling native Methods via MediaArtistNativeHelper
     */
    private MediaArtistNativeHelper mMANativeHelper;
    private boolean mPreviewInProgress = false;
    private final boolean mMallocDebug;

    /**
     * Constructor
     *
     * @param projectPath - The path where the VideoEditor stores all files
     *        related to the project
     */
    public VideoEditorImpl(String projectPath) throws IOException {
        String s;
        s = SystemProperties.get("libc.debug.malloc");
        if (s.equals("1")) {
            mMallocDebug = true;
            try {
                dumpHeap("HeapAtStart");
            } catch (Exception ex) {
                Log.e(TAG, "dumpHeap returned error in constructor");
            }
        } else {
            mMallocDebug = false;
        }
        mLock = new Semaphore(1, true);
        mMANativeHelper = new MediaArtistNativeHelper(projectPath, mLock, this);
        mProjectPath = projectPath;
        final File projectXml = new File(projectPath, PROJECT_FILENAME);
        if (projectXml.exists()) {
            try {
                load();
            } catch (Exception ex) {
                ex.printStackTrace();
                throw new IOException(ex.toString());
            }
        } else {
            mAspectRatio = MediaProperties.ASPECT_RATIO_16_9;
            mDurationMs = 0;
        }
    }

    /*
     * @return The MediaArtistNativeHelper object
     */
    MediaArtistNativeHelper getNativeContext() {
        return mMANativeHelper;
    }

    /*
     * {@inheritDoc}
     */
    public synchronized void addAudioTrack(AudioTrack audioTrack) {
        if (audioTrack == null) {
            throw new IllegalArgumentException("Audio Track is null");
        }

        if (mAudioTracks.size() == 1) {
            throw new IllegalArgumentException("No more tracks can be added");
        }

        mMANativeHelper.setGeneratePreview(true);

        /*
         * Add the audio track to AudioTrack list
         */
        mAudioTracks.add(audioTrack);

        /*
         * Form the audio PCM file path
         */
        final String audioTrackPCMFilePath = String.format(mProjectPath + "/"
                    + "AudioPcm" + audioTrack.getId() + ".pcm");

        /*
         * Create PCM only if not generated in previous session
         */
        if (new File(audioTrackPCMFilePath).exists()) {
            mMANativeHelper.setAudioflag(false);
        }

    }

    /*
     * {@inheritDoc}
     */
    public synchronized void addMediaItem(MediaItem mediaItem) {
        /*
         * Validate Media Item
         */
        if (mediaItem == null) {
            throw new IllegalArgumentException("Media item is null");
        }
        /*
         * Add the Media item to MediaItem list
         */
        if (mMediaItems.contains(mediaItem)) {
            throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId());
        }

        mMANativeHelper.setGeneratePreview(true);

        /*
         *  Invalidate the end transition if necessary
         */
        final int mediaItemsCount = mMediaItems.size();
        if (mediaItemsCount > 0) {
            removeTransitionAfter(mediaItemsCount - 1);
        }

        /*
         *  Add the new media item
         */
        mMediaItems.add(mediaItem);

        computeTimelineDuration();

        /*
         *  Generate project thumbnail only from first media Item on storyboard
         */
        if (mMediaItems.size() == 1) {
            generateProjectThumbnail();
        }
    }


    /*
     * {@inheritDoc}
     */
    public synchronized void addTransition(Transition transition) {
        if (transition == null) {
            throw new IllegalArgumentException("Null Transition");
        }

        final MediaItem beforeMediaItem = transition.getBeforeMediaItem();
        final MediaItem afterMediaItem = transition.getAfterMediaItem();
        /*
         * Check if the MediaItems are in sequence
         */
        if (mMediaItems == null) {
            throw new IllegalArgumentException("No media items are added");
        }

        if ((afterMediaItem != null) &&  (beforeMediaItem != null)) {
            final int afterMediaItemIndex = mMediaItems.indexOf(afterMediaItem);
            final int beforeMediaItemIndex = mMediaItems.indexOf(beforeMediaItem);

            if ((afterMediaItemIndex == -1) || (beforeMediaItemIndex == -1)) {
                throw new IllegalArgumentException
                    ("Either of the mediaItem is not found in the list");
            }

            if (afterMediaItemIndex != (beforeMediaItemIndex - 1) ) {
                throw new IllegalArgumentException("MediaItems are not in sequence");
            }
        }

        mMANativeHelper.setGeneratePreview(true);

        mTransitions.add(transition);
        /*
         *  Cross reference the transitions
         */
        if (afterMediaItem != null) {
            /*
             *  If a transition already exists at the specified position then
             *  invalidate it.
             */
            if (afterMediaItem.getEndTransition() != null) {
                afterMediaItem.getEndTransition().invalidate();
                mTransitions.remove(afterMediaItem.getEndTransition());
            }
            afterMediaItem.setEndTransition(transition);
        }

        if (beforeMediaItem != null) {
            /*
             *  If a transition already exists at the specified position then
             *  invalidate it.
             */
            if (beforeMediaItem.getBeginTransition() != null) {
                beforeMediaItem.getBeginTransition().invalidate();
                mTransitions.remove(beforeMediaItem.getBeginTransition());
            }
            beforeMediaItem.setBeginTransition(transition);
        }

        computeTimelineDuration();
    }

    /*
     * {@inheritDoc}
     */
    public void cancelExport(String filename) {
        if (mMANativeHelper != null && filename != null) {
            mMANativeHelper.stop(filename);
        }
    }

    /*
     * {@inheritDoc}
     */
    public void export(String filename, int height, int bitrate,
                       int audioCodec, int videoCodec,
                       ExportProgressListener listener)
                       throws IOException {
        int audcodec = 0;
        int vidcodec = 0;
        if (filename == null) {
            throw new IllegalArgumentException("export: filename is null");
        }

        final File tempPathFile = new File(filename);
        if (tempPathFile == null) {
            throw new IOException(filename + "can not be created");
        }

        if (mMediaItems.size() == 0) {
            throw new IllegalStateException("No MediaItems added");
        }

        switch (height) {
            case MediaProperties.HEIGHT_144:
                break;
            case MediaProperties.HEIGHT_288:
                break;
            case MediaProperties.HEIGHT_360:
                break;
            case MediaProperties.HEIGHT_480:
                break;
            case MediaProperties.HEIGHT_720:
                break;
            case MediaProperties.HEIGHT_1080:
                break;

            default: {
                String message = "Unsupported height value " + height;
                throw new IllegalArgumentException(message);
            }
        }

        switch (bitrate) {
            case MediaProperties.BITRATE_28K:
                break;
            case MediaProperties.BITRATE_40K:
                break;
            case MediaProperties.BITRATE_64K:
                break;
            case MediaProperties.BITRATE_96K:
                break;
            case MediaProperties.BITRATE_128K:
                break;
            case MediaProperties.BITRATE_192K:
                break;
            case MediaProperties.BITRATE_256K:
                break;
            case MediaProperties.BITRATE_384K:
                break;
            case MediaProperties.BITRATE_512K:
                break;
            case MediaProperties.BITRATE_800K:
                break;
            case MediaProperties.BITRATE_2M:
                break;
            case MediaProperties.BITRATE_5M:
                break;
            case MediaProperties.BITRATE_8M:
                break;

            default: {
                final String message = "Unsupported bitrate value " + bitrate;
                throw new IllegalArgumentException(message);
            }
        }
        computeTimelineDuration();
        final long audioBitrate = MediaArtistNativeHelper.Bitrate.BR_96_KBPS;
        final long fileSize = (mDurationMs * (bitrate + audioBitrate)) / 8000;
        if (MAX_SUPPORTED_FILE_SIZE <= fileSize) {
            throw new IllegalStateException("Export Size is more than 2GB");
        }
        switch (audioCodec) {
            case MediaProperties.ACODEC_AAC_LC:
                audcodec = MediaArtistNativeHelper.AudioFormat.AAC;
                break;
            case MediaProperties.ACODEC_AMRNB:
                audcodec = MediaArtistNativeHelper.AudioFormat.AMR_NB;
                break;

            default: {
                String message = "Unsupported audio codec type " + audioCodec;
                throw new IllegalArgumentException(message);
            }
        }

        switch (videoCodec) {
            case MediaProperties.VCODEC_H263:
                vidcodec = MediaArtistNativeHelper.VideoFormat.H263;
                break;
            case MediaProperties.VCODEC_H264:
                vidcodec = MediaArtistNativeHelper.VideoFormat.H264;
                break;
            case MediaProperties.VCODEC_MPEG4:
                vidcodec = MediaArtistNativeHelper.VideoFormat.MPEG4;
                break;

            default: {
                String message = "Unsupported video codec type " + videoCodec;
                throw new IllegalArgumentException(message);
            }
        }

        boolean semAcquireDone = false;
        try {
            lock();
            semAcquireDone = true;

            if (mMANativeHelper == null) {
                throw new IllegalStateException("The video editor is not initialized");
            }
            mMANativeHelper.setAudioCodec(audcodec);
            mMANativeHelper.setVideoCodec(vidcodec);
            mMANativeHelper.export(filename, mProjectPath, height,bitrate,
                               mMediaItems, mTransitions, mAudioTracks, listener);
        } catch (InterruptedException  ex) {
            Log.e(TAG, "Sem acquire NOT successful in export");
        } finally {
            if (semAcquireDone) {
                unlock();
            }
        }
    }

    /*
     * {@inheritDoc}
     */
    public void export(String filename, int height, int bitrate,
                       ExportProgressListener listener)
                       throws IOException {
        int defaultAudiocodec = MediaArtistNativeHelper.AudioFormat.AAC;
        int defaultVideocodec = MediaArtistNativeHelper.VideoFormat.H264;

        export(filename, height, bitrate, defaultAudiocodec,
                defaultVideocodec, listener);
    }

    /*
     * {@inheritDoc}
     */
    public void generatePreview(MediaProcessingProgressListener listener) {
        boolean semAcquireDone = false;
        try {
            lock();
            semAcquireDone = true;

            if (mMANativeHelper == null) {
                throw new IllegalStateException("The video editor is not initialized");
            }

            if ((mMediaItems.size() > 0) || (mAudioTracks.size() > 0)) {
                mMANativeHelper.previewStoryBoard(mMediaItems, mTransitions, mAudioTracks,
                        listener);
            }
        } catch (InterruptedException  ex) {
            Log.e(TAG, "Sem acquire NOT successful in previewStoryBoard");
        } finally {
            if (semAcquireDone) {
                unlock();
            }
        }
    }

    /*
     * {@inheritDoc}
     */
    public List<AudioTrack> getAllAudioTracks() {
        return mAudioTracks;
    }

    /*
     * {@inheritDoc}
     */
    public List<MediaItem> getAllMediaItems() {
        return mMediaItems;
    }

    /*
     * {@inheritDoc}
     */
    public List<Transition> getAllTransitions() {
        return mTransitions;
    }

    /*
     * {@inheritDoc}
     */
    public int getAspectRatio() {
        return mAspectRatio;
    }

    /*
     * {@inheritDoc}
     */
    public AudioTrack getAudioTrack(String audioTrackId) {
        for (AudioTrack at : mAudioTracks) {
            if (at.getId().equals(audioTrackId)) {
                return at;
            }
        }
        return null;
    }

    /*
     * {@inheritDoc}
     */
    public long getDuration() {
        /**
         *  Since MediaImageItem can change duration we need to compute the
         *  duration here
         */
        computeTimelineDuration();
        return mDurationMs;
    }

    /*
     * Force updates the timeline duration
     */
    void updateTimelineDuration() {
        computeTimelineDuration();
    }

    /*
     * {@inheritDoc}
     */
    public synchronized MediaItem getMediaItem(String mediaItemId) {
        for (MediaItem mediaItem : mMediaItems) {
            if (mediaItem.getId().equals(mediaItemId)) {
                return mediaItem;
            }
        }
        return null;
    }

    /*
     * {@inheritDoc}
     */
    public String getPath() {
        return mProjectPath;
    }

    /*
     * {@inheritDoc}
     */
    public Transition getTransition(String transitionId) {
        for (Transition transition : mTransitions) {
            if (transition.getId().equals(transitionId)) {
                return transition;
            }
        }
        return null;
    }

    /*
     * {@inheritDoc}
     */
    public synchronized void insertAudioTrack(AudioTrack audioTrack,
                                              String afterAudioTrackId) {
        if (mAudioTracks.size() == 1) {
            throw new IllegalArgumentException("No more tracks can be added");
        }

        if (afterAudioTrackId == null) {
            mMANativeHelper.setGeneratePreview(true);
            mAudioTracks.add(0, audioTrack);
        } else {
            final int audioTrackCount = mAudioTracks.size();
            for (int i = 0; i < audioTrackCount; i++) {
                AudioTrack at = mAudioTracks.get(i);
                if (at.getId().equals(afterAudioTrackId)) {
                    mMANativeHelper.setGeneratePreview(true);
                    mAudioTracks.add(i + 1, audioTrack);
                    return;
                }
            }

            throw new IllegalArgumentException("AudioTrack not found: " + afterAudioTrackId);
        }
    }

    /*
     * {@inheritDoc}
     */
    public synchronized void insertMediaItem(MediaItem mediaItem, String afterMediaItemId) {
        if (mMediaItems.contains(mediaItem)) {
            throw new IllegalArgumentException("Media item already exists: " + mediaItem.getId());
        }

        if (afterMediaItemId == null) {
            mMANativeHelper.setGeneratePreview(true);
            if (mMediaItems.size() > 0) {
                /**
                 *  Invalidate the transition at the beginning of the timeline
                 */
                removeTransitionBefore(0);
            }

            mMediaItems.add(0, mediaItem);
            computeTimelineDuration();
            generateProjectThumbnail();
        } else {
            final int mediaItemCount = mMediaItems.size();
            for (int i = 0; i < mediaItemCount; i++) {
                final MediaItem mi = mMediaItems.get(i);
                if (mi.getId().equals(afterMediaItemId)) {
                    mMANativeHelper.setGeneratePreview(true);
                    /**
                     *  Invalidate the transition at this position
                     */
                    removeTransitionAfter(i);
                    /**
                     *  Insert the new media item
                     */
                    mMediaItems.add(i + 1, mediaItem);
                    computeTimelineDuration();
                    return;
                }
            }

            throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId);
        }
    }

    /*
     * {@inheritDoc}
     */
    public synchronized void moveAudioTrack(String audioTrackId, String afterAudioTrackId) {
        throw new IllegalStateException("Not supported");
    }

    /*
     * {@inheritDoc}
     */
    public synchronized void moveMediaItem(String mediaItemId, String afterMediaItemId) {
        final MediaItem moveMediaItem = removeMediaItem(mediaItemId,true);
        if (moveMediaItem == null) {
            throw new IllegalArgumentException("Target MediaItem not found: " + mediaItemId);
        }

        if (afterMediaItemId == null) {
            if (mMediaItems.size() > 0) {
                mMANativeHelper.setGeneratePreview(true);

                /**
                 *  Invalidate adjacent transitions at the insertion point
                 */
                removeTransitionBefore(0);

                /**
                 *  Insert the media item at the new position
                 */
                mMediaItems.add(0, moveMediaItem);
                computeTimelineDuration();

                generateProjectThumbnail();
            } else {
                throw new IllegalStateException("Cannot move media item (it is the only item)");
            }
        } else {
            final int mediaItemCount = mMediaItems.size();
            for (int i = 0; i < mediaItemCount; i++) {
                final MediaItem mi = mMediaItems.get(i);
                if (mi.getId().equals(afterMediaItemId)) {
                    mMANativeHelper.setGeneratePreview(true);
                    /**
                     *  Invalidate adjacent transitions at the insertion point
                     */
                    removeTransitionAfter(i);
                    /**
                     *  Insert the media item at the new position
                     */
                    mMediaItems.add(i + 1, moveMediaItem);
                    computeTimelineDuration();
                    return;
                }
            }

            throw new IllegalArgumentException("MediaItem not found: " + afterMediaItemId);
        }
    }

    /*
     * {@inheritDoc}
     */
    public void release() {
        stopPreview();

        boolean semAcquireDone = false;
        try {
            lock();
            semAcquireDone = true;

            if (mMANativeHelper != null) {
                mMediaItems.clear();
                mAudioTracks.clear();
                mTransitions.clear();
                mMANativeHelper.releaseNativeHelper();
                mMANativeHelper = null;
            }
        } catch (Exception  ex) {
            Log.e(TAG, "Sem acquire NOT successful in export", ex);
        } finally {
            if (semAcquireDone) {
                unlock();
            }
        }
        if (mMallocDebug) {
            try {
                dumpHeap("HeapAtEnd");
            } catch (Exception ex) {
                Log.e(TAG, "dumpHeap returned error in release");
            }
        }
    }

    /*
     * {@inheritDoc}
     */
    public synchronized void removeAllMediaItems() {
        mMANativeHelper.setGeneratePreview(true);

        mMediaItems.clear();

        /**
         *  Invalidate all transitions
         */
        for (Transition transition : mTransitions) {
            transition.invalidate();
        }
        mTransitions.clear();

        mDurationMs = 0;
        /**
         * If a thumbnail already exists, then delete it
         */
        if ((new File(mProjectPath + "/" + THUMBNAIL_FILENAME)).exists()) {
            (new File(mProjectPath + "/" + THUMBNAIL_FILENAME)).delete();
        }

    }

    /*
     * {@inheritDoc}
     */
    public synchronized AudioTrack removeAudioTrack(String audioTrackId) {
        final AudioTrack audioTrack = getAudioTrack(audioTrackId);
        if (audioTrack != null) {
            mMANativeHelper.setGeneratePreview(true);
            mAudioTracks.remove(audioTrack);
            audioTrack.invalidate();
            mMANativeHelper.invalidatePcmFile();
            mMANativeHelper.setAudioflag(true);
        } else {
            throw new IllegalArgumentException(" No more audio tracks");
        }
        return audioTrack;
    }

    /*
     * {@inheritDoc}
     */
    public synchronized MediaItem removeMediaItem(String mediaItemId) {
        final String firstItemString = mMediaItems.get(0).getId();
        final MediaItem mediaItem = getMediaItem(mediaItemId);
        if (mediaItem != null) {
            mMANativeHelper.setGeneratePreview(true);
            /**
             *  Remove the media item
             */
            mMediaItems.remove(mediaItem);
            if (mediaItem instanceof MediaImageItem) {
                ((MediaImageItem)mediaItem).invalidate();
            }
            final List<Overlay> overlays = mediaItem.getAllOverlays();
            if (overlays.size() > 0) {
                for (Overlay overlay : overlays) {
                    if (overlay instanceof OverlayFrame) {
                        final OverlayFrame overlayFrame = (OverlayFrame)overlay;
                        overlayFrame.invalidate();
                    }
                }
            }

            /**
             *  Remove the adjacent transitions
             */
            removeAdjacentTransitions(mediaItem);
            computeTimelineDuration();
        }

        /**
         * If string equals first mediaItem, then
         * generate Project thumbnail
         */
        if (firstItemString.equals(mediaItemId)) {
            generateProjectThumbnail();
        }

        if (mediaItem instanceof MediaVideoItem) {
            /**
             * Delete the graph file
             */
            ((MediaVideoItem)mediaItem).invalidate();
        }
        return mediaItem;
    }

    private synchronized MediaItem removeMediaItem(String mediaItemId, boolean flag) {
        final String firstItemString = mMediaItems.get(0).getId();

        final MediaItem mediaItem = getMediaItem(mediaItemId);
        if (mediaItem != null) {
            mMANativeHelper.setGeneratePreview(true);
            /**
             *  Remove the media item
             */
            mMediaItems.remove(mediaItem);
            /**
             *  Remove the adjacent transitions
             */
            removeAdjacentTransitions(mediaItem);
            computeTimelineDuration();
        }

        /**
         * If string equals first mediaItem, then
         * generate Project thumbail
         */
        if (firstItemString.equals(mediaItemId)) {
            generateProjectThumbnail();
        }
        return mediaItem;
    }

    /*
     * {@inheritDoc}
     */
    public synchronized Transition removeTransition(String transitionId) {
        final Transition transition = getTransition(transitionId);
        if (transition == null) {
            throw new IllegalStateException("Transition not found: " + transitionId);
        }

        mMANativeHelper.setGeneratePreview(true);

        /**
         *  Remove the transition references
         */
        final MediaItem afterMediaItem = transition.getAfterMediaItem();
        if (afterMediaItem != null) {
            afterMediaItem.setEndTransition(null);
        }

        final MediaItem beforeMediaItem = transition.getBeforeMediaItem();
        if (beforeMediaItem != null) {
            beforeMediaItem.setBeginTransition(null);
        }

        mTransitions.remove(transition);
        transition.invalidate();
        computeTimelineDuration();
        return transition;
    }

    /*
     * {@inheritDoc}
     */
    public long renderPreviewFrame(SurfaceHolder surfaceHolder, long timeMs,
                                    OverlayData overlayData) {
        if (surfaceHolder == null) {
            throw new IllegalArgumentException("Surface Holder is null");
        }

        final Surface surface = surfaceHolder.getSurface();
        if (surface == null) {
            throw new IllegalArgumentException("Surface could not be retrieved from Surface holder");
        }

        if (surface.isValid() == false) {
            throw new IllegalStateException("Surface is not valid");
        }

        if (timeMs < 0) {
            throw new IllegalArgumentException("requested time not correct");
        } else if (timeMs > mDurationMs) {
            throw new IllegalArgumentException("requested time more than duration");
        }
        long result = 0;

        boolean semAcquireDone = false;
        try {
            semAcquireDone = lock(ENGINE_ACCESS_MAX_TIMEOUT_MS);
            if (semAcquireDone == false) {
                throw new IllegalStateException("Timeout waiting for semaphore");
            }

            if (mMANativeHelper == null) {
                throw new IllegalStateException("The video editor is not initialized");
            }

            if (mMediaItems.size() > 0) {
                final Rect frame = surfaceHolder.getSurfaceFrame();
                result = mMANativeHelper.renderPreviewFrame(surface,
                        timeMs, frame.width(), frame.height(), overlayData);
            } else {
                result = 0;
            }
        } catch (InterruptedException ex) {
            Log.w(TAG, "The thread was interrupted", new Throwable());
            throw new IllegalStateException("The thread was interrupted");
        } finally {
            if (semAcquireDone) {
                unlock();
            }
        }
        return result;
    }

    /**
     *  the project form XML
     */
    private void load() throws FileNotFoundException, XmlPullParserException, IOException {
        final File file = new File(mProjectPath, PROJECT_FILENAME);
        /**
         *  Load the metadata
         */
        final FileInputStream fis = new FileInputStream(file);
        try {
            final List<String> ignoredMediaItems = new ArrayList<String>();

            final XmlPullParser parser = Xml.newPullParser();
            parser.setInput(fis, "UTF-8");
            int eventType = parser.getEventType();
            String name;
            MediaItem currentMediaItem = null;
            Overlay currentOverlay = null;
            boolean regenerateProjectThumbnail = false;
            while (eventType != XmlPullParser.END_DOCUMENT) {
                switch (eventType) {
                    case XmlPullParser.START_TAG: {
                        name = parser.getName();
                        if (TAG_PROJECT.equals(name)) {
                            mAspectRatio = Integer.parseInt(parser.getAttributeValue("",
                                   ATTR_ASPECT_RATIO));

                            final boolean mRegenPCM =
                                Boolean.parseBoolean(parser.getAttributeValue("",
                                    ATTR_REGENERATE_PCM));
                            mMANativeHelper.setAudioflag(mRegenPCM);
                        } else if (TAG_MEDIA_ITEM.equals(name)) {
                            final String mediaItemId = parser.getAttributeValue("", ATTR_ID);
                            try {
                                currentMediaItem = parseMediaItem(parser);
                                mMediaItems.add(currentMediaItem);
                            } catch (Exception ex) {
                                Log.w(TAG, "Cannot load media item: " + mediaItemId, ex);
                                currentMediaItem = null;

                                // First media item is invalid, mark for project thumbnail removal
                                if (mMediaItems.size() == 0) {
                                    regenerateProjectThumbnail = true;
                                }
                                // Ignore the media item
                                ignoredMediaItems.add(mediaItemId);
                            }
                        } else if (TAG_TRANSITION.equals(name)) {
                            try {
                                final Transition transition = parseTransition(parser,
                                        ignoredMediaItems);
                                // The transition will be null if the bounding
                                // media items are ignored
                                if (transition != null) {
                                    mTransitions.add(transition);
                                }
                            } catch (Exception ex) {
                                Log.w(TAG, "Cannot load transition", ex);
                            }
                        } else if (TAG_OVERLAY.equals(name)) {
                            if (currentMediaItem != null) {
                                try {
                                    currentOverlay = parseOverlay(parser, currentMediaItem);
                                    currentMediaItem.addOverlay(currentOverlay);
                                } catch (Exception ex) {
                                    Log.w(TAG, "Cannot load overlay", ex);
                                }
                            }
                        } else if (TAG_OVERLAY_USER_ATTRIBUTES.equals(name)) {
                            if (currentOverlay != null) {
                                final int attributesCount = parser.getAttributeCount();
                                for (int i = 0; i < attributesCount; i++) {
                                    currentOverlay.setUserAttribute(parser.getAttributeName(i),
                                            parser.getAttributeValue(i));
                                }
                            }
                        } else if (TAG_EFFECT.equals(name)) {
                            if (currentMediaItem != null) {
                                try {
                                    final Effect effect = parseEffect(parser, currentMediaItem);
                                    currentMediaItem.addEffect(effect);

                                    if (effect instanceof EffectKenBurns) {
                                        final boolean isImageClipGenerated =
                                               Boolean.parseBoolean(parser.getAttributeValue("",
                                                                  ATTR_IS_IMAGE_CLIP_GENERATED));
                                        if(isImageClipGenerated) {
                                            final String filename = parser.getAttributeValue("",
                                                                  ATTR_GENERATED_IMAGE_CLIP);
                                            if (new File(filename).exists() == true) {
                                                ((MediaImageItem)currentMediaItem).
                                                            setGeneratedImageClip(filename);
                                                ((MediaImageItem)currentMediaItem).
                                                             setRegenerateClip(false);
                                             } else {
                                               ((MediaImageItem)currentMediaItem).
                                                             setGeneratedImageClip(null);
                                               ((MediaImageItem)currentMediaItem).
                                                             setRegenerateClip(true);
                                             }
                                        } else {
                                            ((MediaImageItem)currentMediaItem).
                                                             setGeneratedImageClip(null);
                                            ((MediaImageItem)currentMediaItem).
                                                            setRegenerateClip(true);
                                        }
                                    }
                                } catch (Exception ex) {
                                    Log.w(TAG, "Cannot load effect", ex);
                                }
                            }
                        } else if (TAG_AUDIO_TRACK.equals(name)) {
                            try {
                                final AudioTrack audioTrack = parseAudioTrack(parser);
                                addAudioTrack(audioTrack);
                            } catch (Exception ex) {
                                Log.w(TAG, "Cannot load audio track", ex);
                            }
                        }
                        break;
                    }

                    case XmlPullParser.END_TAG: {
                        name = parser.getName();
                        if (TAG_MEDIA_ITEM.equals(name)) {
                            currentMediaItem = null;
                        } else if (TAG_OVERLAY.equals(name)) {
                            currentOverlay = null;
                        }
                        break;
                    }

                    default: {
                        break;
                    }
                }
                eventType = parser.next();
            }
            computeTimelineDuration();
            // Regenerate project thumbnail
            if (regenerateProjectThumbnail) {
                generateProjectThumbnail();
                regenerateProjectThumbnail = false;
            }
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
    }

    /**
     * Parse the media item
     *
     * @param parser The parser
     * @return The media item
     */
    private MediaItem parseMediaItem(XmlPullParser parser) throws IOException {
        final String mediaItemId = parser.getAttributeValue("", ATTR_ID);
        final String type = parser.getAttributeValue("", ATTR_TYPE);
        final String filename = parser.getAttributeValue("", ATTR_FILENAME);
        final int renderingMode = Integer.parseInt(parser.getAttributeValue("",
                ATTR_RENDERING_MODE));

        final MediaItem currentMediaItem;
        if (MediaImageItem.class.getSimpleName().equals(type)) {
            final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION));
            currentMediaItem = new MediaImageItem(this, mediaItemId, filename,
                    durationMs, renderingMode);
        } else if (MediaVideoItem.class.getSimpleName().equals(type)) {
            final long beginMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));
            final long endMs = Long.parseLong(parser.getAttributeValue("", ATTR_END_TIME));
            final int volume = Integer.parseInt(parser.getAttributeValue("", ATTR_VOLUME));
            final boolean muted = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_MUTED));
            final String audioWaveformFilename = parser.getAttributeValue("",
                    ATTR_AUDIO_WAVEFORM_FILENAME);
            currentMediaItem = new MediaVideoItem(this, mediaItemId, filename,
                    renderingMode, beginMs, endMs, volume, muted, audioWaveformFilename);

            final long beginTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));
            final long endTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_END_TIME));
            ((MediaVideoItem)currentMediaItem).setExtractBoundaries(beginTimeMs, endTimeMs);

            final int volumePercent = Integer.parseInt(parser.getAttributeValue("", ATTR_VOLUME));
            ((MediaVideoItem)currentMediaItem).setVolume(volumePercent);
        } else {
            throw new IllegalArgumentException("Unknown media item type: " + type);
        }

        return currentMediaItem;
    }

    /**
     * Parse the transition
     *
     * @param parser The parser
     * @param ignoredMediaItems The list of ignored media items
     *
     * @return The transition
     */
    private Transition parseTransition(XmlPullParser parser, List<String> ignoredMediaItems) {
        final String transitionId = parser.getAttributeValue("", ATTR_ID);
        final String type = parser.getAttributeValue("", ATTR_TYPE);
        final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION));
        final int behavior = Integer.parseInt(parser.getAttributeValue("", ATTR_BEHAVIOR));

        final String beforeMediaItemId = parser.getAttributeValue("", ATTR_BEFORE_MEDIA_ITEM_ID);
        final MediaItem beforeMediaItem;
        if (beforeMediaItemId != null) {
            if (ignoredMediaItems.contains(beforeMediaItemId)) {
                // This transition is ignored
                return null;
            }

            beforeMediaItem = getMediaItem(beforeMediaItemId);
        } else {
            beforeMediaItem = null;
        }

        final String afterMediaItemId = parser.getAttributeValue("", ATTR_AFTER_MEDIA_ITEM_ID);
        final MediaItem afterMediaItem;
        if (afterMediaItemId != null) {
            if (ignoredMediaItems.contains(afterMediaItemId)) {
                // This transition is ignored
                return null;
            }

            afterMediaItem = getMediaItem(afterMediaItemId);
        } else {
            afterMediaItem = null;
        }

        final Transition transition;
        if (TransitionAlpha.class.getSimpleName().equals(type)) {
            final int blending = Integer.parseInt(parser.getAttributeValue("", ATTR_BLENDING));
            final String maskFilename = parser.getAttributeValue("", ATTR_MASK);
            final boolean invert = Boolean.getBoolean(parser.getAttributeValue("", ATTR_INVERT));
            transition = new TransitionAlpha(transitionId, afterMediaItem, beforeMediaItem,
                    durationMs, behavior, maskFilename, blending, invert);
        } else if (TransitionCrossfade.class.getSimpleName().equals(type)) {
            transition = new TransitionCrossfade(transitionId, afterMediaItem, beforeMediaItem,
                    durationMs, behavior);
        } else if (TransitionSliding.class.getSimpleName().equals(type)) {
            final int direction = Integer.parseInt(parser.getAttributeValue("", ATTR_DIRECTION));
            transition = new TransitionSliding(transitionId, afterMediaItem, beforeMediaItem,
                    durationMs, behavior, direction);
        } else if (TransitionFadeBlack.class.getSimpleName().equals(type)) {
            transition = new TransitionFadeBlack(transitionId, afterMediaItem, beforeMediaItem,
                    durationMs, behavior);
        } else {
            throw new IllegalArgumentException("Invalid transition type: " + type);
        }

        final boolean isTransitionGenerated = Boolean.parseBoolean(parser.getAttributeValue("",
                                                 ATTR_IS_TRANSITION_GENERATED));
        if (isTransitionGenerated == true) {
            final String transitionFile = parser.getAttributeValue("",
                                                ATTR_GENERATED_TRANSITION_CLIP);

            if (new File(transitionFile).exists()) {
                transition.setFilename(transitionFile);
            } else {
                transition.setFilename(null);
            }
        }

        // Use the transition
        if (beforeMediaItem != null) {
            beforeMediaItem.setBeginTransition(transition);
        }

        if (afterMediaItem != null) {
            afterMediaItem.setEndTransition(transition);
        }

        return transition;
    }

    /**
     * Parse the overlay
     *
     * @param parser The parser
     * @param mediaItem The media item owner
     *
     * @return The overlay
     */
    private Overlay parseOverlay(XmlPullParser parser, MediaItem mediaItem) {
        final String overlayId = parser.getAttributeValue("", ATTR_ID);
        final String type = parser.getAttributeValue("", ATTR_TYPE);
        final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION));
        final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));

        final Overlay overlay;
        if (OverlayFrame.class.getSimpleName().equals(type)) {
            final String filename = parser.getAttributeValue("", ATTR_FILENAME);
            overlay = new OverlayFrame(mediaItem, overlayId, filename, startTimeMs, durationMs);
        } else {
            throw new IllegalArgumentException("Invalid overlay type: " + type);
        }

        final String overlayRgbFileName = parser.getAttributeValue("", ATTR_OVERLAY_RGB_FILENAME);
        if (overlayRgbFileName != null) {
            ((OverlayFrame)overlay).setFilename(overlayRgbFileName);

            final int overlayFrameWidth = Integer.parseInt(parser.getAttributeValue("",
                                   ATTR_OVERLAY_FRAME_WIDTH));
            final int overlayFrameHeight = Integer.parseInt(parser.getAttributeValue("",
                                   ATTR_OVERLAY_FRAME_HEIGHT));

            ((OverlayFrame)overlay).setOverlayFrameWidth(overlayFrameWidth);
            ((OverlayFrame)overlay).setOverlayFrameHeight(overlayFrameHeight);

            final int resizedRGBFrameWidth = Integer.parseInt(parser.getAttributeValue("",
                                   ATTR_OVERLAY_RESIZED_RGB_FRAME_WIDTH));
            final int resizedRGBFrameHeight = Integer.parseInt(parser.getAttributeValue("",
                                   ATTR_OVERLAY_RESIZED_RGB_FRAME_HEIGHT));

            ((OverlayFrame)overlay).setResizedRGBSize(resizedRGBFrameWidth, resizedRGBFrameHeight);
        }

        return overlay;
    }

    /**
     * Parse the effect
     *
     * @param parser The parser
     * @param mediaItem The media item owner
     *
     * @return The effect
     */
    private Effect parseEffect(XmlPullParser parser, MediaItem mediaItem) {
        final String effectId = parser.getAttributeValue("", ATTR_ID);
        final String type = parser.getAttributeValue("", ATTR_TYPE);
        final long durationMs = Long.parseLong(parser.getAttributeValue("", ATTR_DURATION));
        final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));

        final Effect effect;
        if (EffectColor.class.getSimpleName().equals(type)) {
            final int colorEffectType = Integer.parseInt(parser.getAttributeValue("",
                                                       ATTR_COLOR_EFFECT_TYPE));
            final int color;
            if (colorEffectType == EffectColor.TYPE_COLOR
                    || colorEffectType == EffectColor.TYPE_GRADIENT) {
                color = Integer.parseInt(parser.getAttributeValue("", ATTR_COLOR_EFFECT_VALUE));
            } else {
                color = 0;
            }
            effect = new EffectColor(mediaItem, effectId, startTimeMs,
                    durationMs, colorEffectType, color);
        } else if (EffectKenBurns.class.getSimpleName().equals(type)) {
            final Rect startRect = new Rect(
                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_LEFT)),
                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_TOP)),
                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_RIGHT)),
                    Integer.parseInt(parser.getAttributeValue("", ATTR_START_RECT_BOTTOM)));
            final Rect endRect = new Rect(
                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_LEFT)),
                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_TOP)),
                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_RIGHT)),
                    Integer.parseInt(parser.getAttributeValue("", ATTR_END_RECT_BOTTOM)));
            effect = new EffectKenBurns(mediaItem, effectId, startRect, endRect,
                                        startTimeMs, durationMs);
        } else {
            throw new IllegalArgumentException("Invalid effect type: " + type);
        }

        return effect;
    }

    /**
     * Parse the audio track
     *
     * @param parser The parser
     *
     * @return The audio track
     */
    private AudioTrack parseAudioTrack(XmlPullParser parser) throws IOException {
        final String audioTrackId = parser.getAttributeValue("", ATTR_ID);
        final String filename = parser.getAttributeValue("", ATTR_FILENAME);
        final long startTimeMs = Long.parseLong(parser.getAttributeValue("", ATTR_START_TIME));
        final long beginMs = Long.parseLong(parser.getAttributeValue("", ATTR_BEGIN_TIME));
        final long endMs = Long.parseLong(parser.getAttributeValue("", ATTR_END_TIME));
        final int volume = Integer.parseInt(parser.getAttributeValue("", ATTR_VOLUME));
        final boolean muted = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_MUTED));
        final boolean loop = Boolean.parseBoolean(parser.getAttributeValue("", ATTR_LOOP));
        final boolean duckingEnabled = Boolean.parseBoolean(
                parser.getAttributeValue("", ATTR_DUCK_ENABLED));
        final int duckThreshold = Integer.parseInt(
                parser.getAttributeValue("", ATTR_DUCK_THRESHOLD));
        final int duckedTrackVolume = Integer.parseInt(parser.getAttributeValue("",
                                                     ATTR_DUCKED_TRACK_VOLUME));

        final String waveformFilename = parser.getAttributeValue("", ATTR_AUDIO_WAVEFORM_FILENAME);
        final AudioTrack audioTrack = new AudioTrack(this, audioTrackId,
                                                     filename, startTimeMs,
                                                     beginMs, endMs, loop,
                                                     volume, muted,
                                                     duckingEnabled,
                                                     duckThreshold,
                                                     duckedTrackVolume,
                                                     waveformFilename);

        return audioTrack;
    }

    /*
     * {@inheritDoc}
     */
    public void save() throws IOException {
        final XmlSerializer serializer = Xml.newSerializer();
        final StringWriter writer = new StringWriter();
        serializer.setOutput(writer);
        serializer.startDocument("UTF-8", true);
        serializer.startTag("", TAG_PROJECT);
        serializer.attribute("",
                             ATTR_ASPECT_RATIO, Integer.toString(mAspectRatio));

        serializer.attribute("", ATTR_REGENERATE_PCM,
                        Boolean.toString(mMANativeHelper.getAudioflag()));

        serializer.startTag("", TAG_MEDIA_ITEMS);
        for (MediaItem mediaItem : mMediaItems) {
            serializer.startTag("", TAG_MEDIA_ITEM);
            serializer.attribute("", ATTR_ID, mediaItem.getId());
            serializer.attribute("", ATTR_TYPE,
                                          mediaItem.getClass().getSimpleName());
            serializer.attribute("", ATTR_FILENAME, mediaItem.getFilename());
            serializer.attribute("", ATTR_RENDERING_MODE, Integer.toString(
                    mediaItem.getRenderingMode()));
            if (mediaItem instanceof MediaVideoItem) {
                final MediaVideoItem mvi = (MediaVideoItem)mediaItem;
                serializer
                .attribute("", ATTR_BEGIN_TIME,
                                     Long.toString(mvi.getBoundaryBeginTime()));
                serializer.attribute("", ATTR_END_TIME,
                                       Long.toString(mvi.getBoundaryEndTime()));
                serializer.attribute("", ATTR_VOLUME,
                                             Integer.toString(mvi.getVolume()));
                serializer.attribute("", ATTR_MUTED,
                                               Boolean.toString(mvi.isMuted()));
                if (mvi.getAudioWaveformFilename() != null) {
                    serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME,
                            mvi.getAudioWaveformFilename());
                }
            } else if (mediaItem instanceof MediaImageItem) {
                serializer.attribute("", ATTR_DURATION,
                        Long.toString(mediaItem.getTimelineDuration()));
            }

            final List<Overlay> overlays = mediaItem.getAllOverlays();
            if (overlays.size() > 0) {
                serializer.startTag("", TAG_OVERLAYS);
                for (Overlay overlay : overlays) {
                    serializer.startTag("", TAG_OVERLAY);
                    serializer.attribute("", ATTR_ID, overlay.getId());
                    serializer.attribute("",
                                 ATTR_TYPE, overlay.getClass().getSimpleName());
                    serializer.attribute("", ATTR_BEGIN_TIME,
                                         Long.toString(overlay.getStartTime()));
                    serializer.attribute("", ATTR_DURATION,
                                          Long.toString(overlay.getDuration()));
                    if (overlay instanceof OverlayFrame) {
                        final OverlayFrame overlayFrame = (OverlayFrame)overlay;
                        overlayFrame.save(getPath());
                        if (overlayFrame.getBitmapImageFileName() != null) {
                            serializer.attribute("", ATTR_FILENAME,
                                         overlayFrame.getBitmapImageFileName());
                        }

                        if (overlayFrame.getFilename() != null) {
                            serializer.attribute("",
                                                 ATTR_OVERLAY_RGB_FILENAME,
                                                 overlayFrame.getFilename());
                            serializer.attribute("", ATTR_OVERLAY_FRAME_WIDTH,
                                                 Integer.toString(overlayFrame.getOverlayFrameWidth()));
                            serializer.attribute("", ATTR_OVERLAY_FRAME_HEIGHT,
                                                 Integer.toString(overlayFrame.getOverlayFrameHeight()));
                            serializer.attribute("", ATTR_OVERLAY_RESIZED_RGB_FRAME_WIDTH,
                                                 Integer.toString(overlayFrame.getResizedRGBSizeWidth()));
                            serializer.attribute("", ATTR_OVERLAY_RESIZED_RGB_FRAME_HEIGHT,
                                                 Integer.toString(overlayFrame.getResizedRGBSizeHeight()));

                        }

                    }

                    /**
                     *  Save the user attributes
                     */
                    serializer.startTag("", TAG_OVERLAY_USER_ATTRIBUTES);
                    final Map<String, String> userAttributes = overlay.getUserAttributes();
                    for (String name : userAttributes.keySet()) {
                        final String value = userAttributes.get(name);
                        if (value != null) {
                            serializer.attribute("", name, value);
                        }
                    }
                    serializer.endTag("", TAG_OVERLAY_USER_ATTRIBUTES);

                    serializer.endTag("", TAG_OVERLAY);
                }
                serializer.endTag("", TAG_OVERLAYS);
            }

            final List<Effect> effects = mediaItem.getAllEffects();
            if (effects.size() > 0) {
                serializer.startTag("", TAG_EFFECTS);
                for (Effect effect : effects) {
                    serializer.startTag("", TAG_EFFECT);
                    serializer.attribute("", ATTR_ID, effect.getId());
                    serializer.attribute("",
                                  ATTR_TYPE, effect.getClass().getSimpleName());
                    serializer.attribute("", ATTR_BEGIN_TIME,
                            Long.toString(effect.getStartTime()));
                    serializer.attribute("", ATTR_DURATION,
                                           Long.toString(effect.getDuration()));
                    if (effect instanceof EffectColor) {
                        final EffectColor colorEffect = (EffectColor)effect;
                        serializer.attribute("", ATTR_COLOR_EFFECT_TYPE,
                                Integer.toString(colorEffect.getType()));
                        if (colorEffect.getType() == EffectColor.TYPE_COLOR ||
                                colorEffect.getType() == EffectColor.TYPE_GRADIENT) {
                            serializer.attribute("", ATTR_COLOR_EFFECT_VALUE,
                                    Integer.toString(colorEffect.getColor()));
                        }
                    } else if (effect instanceof EffectKenBurns) {
                        final Rect startRect = ((EffectKenBurns)effect).getStartRect();
                        serializer.attribute("", ATTR_START_RECT_LEFT,
                                Integer.toString(startRect.left));
                        serializer.attribute("", ATTR_START_RECT_TOP,
                                Integer.toString(startRect.top));
                        serializer.attribute("", ATTR_START_RECT_RIGHT,
                                Integer.toString(startRect.right));
                        serializer.attribute("", ATTR_START_RECT_BOTTOM,
                                Integer.toString(startRect.bottom));

                        final Rect endRect = ((EffectKenBurns)effect).getEndRect();
                        serializer.attribute("", ATTR_END_RECT_LEFT,
                                                Integer.toString(endRect.left));
                        serializer.attribute("", ATTR_END_RECT_TOP,
                                                 Integer.toString(endRect.top));
                        serializer.attribute("", ATTR_END_RECT_RIGHT,
                                               Integer.toString(endRect.right));
                        serializer.attribute("", ATTR_END_RECT_BOTTOM,
                                Integer.toString(endRect.bottom));
                        final MediaItem mItem = effect.getMediaItem();
                           if(((MediaImageItem)mItem).getGeneratedImageClip() != null) {
                               serializer.attribute("", ATTR_IS_IMAGE_CLIP_GENERATED,
                                       Boolean.toString(true));
                               serializer.attribute("", ATTR_GENERATED_IMAGE_CLIP,
                                     ((MediaImageItem)mItem).getGeneratedImageClip());
                            } else {
                                serializer.attribute("", ATTR_IS_IMAGE_CLIP_GENERATED,
                                     Boolean.toString(false));
                         }
                    }

                    serializer.endTag("", TAG_EFFECT);
                }
                serializer.endTag("", TAG_EFFECTS);
            }

            serializer.endTag("", TAG_MEDIA_ITEM);
        }
        serializer.endTag("", TAG_MEDIA_ITEMS);

        serializer.startTag("", TAG_TRANSITIONS);

        for (Transition transition : mTransitions) {
            serializer.startTag("", TAG_TRANSITION);
            serializer.attribute("", ATTR_ID, transition.getId());
            serializer.attribute("", ATTR_TYPE, transition.getClass().getSimpleName());
            serializer.attribute("", ATTR_DURATION, Long.toString(transition.getDuration()));
            serializer.attribute("", ATTR_BEHAVIOR, Integer.toString(transition.getBehavior()));
            serializer.attribute("", ATTR_IS_TRANSITION_GENERATED,
                                    Boolean.toString(transition.isGenerated()));
            if (transition.isGenerated() == true) {
                serializer.attribute("", ATTR_GENERATED_TRANSITION_CLIP, transition.mFilename);
            }
            final MediaItem afterMediaItem = transition.getAfterMediaItem();
            if (afterMediaItem != null) {
                serializer.attribute("", ATTR_AFTER_MEDIA_ITEM_ID, afterMediaItem.getId());
            }

            final MediaItem beforeMediaItem = transition.getBeforeMediaItem();
            if (beforeMediaItem != null) {
                serializer.attribute("", ATTR_BEFORE_MEDIA_ITEM_ID, beforeMediaItem.getId());
            }

            if (transition instanceof TransitionSliding) {
                serializer.attribute("", ATTR_DIRECTION,
                        Integer.toString(((TransitionSliding)transition).getDirection()));
            } else if (transition instanceof TransitionAlpha) {
                TransitionAlpha ta = (TransitionAlpha)transition;
                serializer.attribute("", ATTR_BLENDING,
                                     Integer.toString(ta.getBlendingPercent()));
                serializer.attribute("", ATTR_INVERT,
                                               Boolean.toString(ta.isInvert()));
                if (ta.getMaskFilename() != null) {
                    serializer.attribute("", ATTR_MASK, ta.getMaskFilename());
                }
            }
            serializer.endTag("", TAG_TRANSITION);
        }
        serializer.endTag("", TAG_TRANSITIONS);
        serializer.startTag("", TAG_AUDIO_TRACKS);
        for (AudioTrack at : mAudioTracks) {
            serializer.startTag("", TAG_AUDIO_TRACK);
            serializer.attribute("", ATTR_ID, at.getId());
            serializer.attribute("", ATTR_FILENAME, at.getFilename());
            serializer.attribute("", ATTR_START_TIME, Long.toString(at.getStartTime()));
            serializer.attribute("", ATTR_BEGIN_TIME, Long.toString(at.getBoundaryBeginTime()));
            serializer.attribute("", ATTR_END_TIME, Long.toString(at.getBoundaryEndTime()));
            serializer.attribute("", ATTR_VOLUME, Integer.toString(at.getVolume()));
            serializer.attribute("", ATTR_DUCK_ENABLED,
                                       Boolean.toString(at.isDuckingEnabled()));
            serializer.attribute("", ATTR_DUCKED_TRACK_VOLUME,
                                   Integer.toString(at.getDuckedTrackVolume()));
            serializer.attribute("", ATTR_DUCK_THRESHOLD,
                                   Integer.toString(at.getDuckingThreshhold()));
            serializer.attribute("", ATTR_MUTED, Boolean.toString(at.isMuted()));
            serializer.attribute("", ATTR_LOOP, Boolean.toString(at.isLooping()));
            if (at.getAudioWaveformFilename() != null) {
                serializer.attribute("", ATTR_AUDIO_WAVEFORM_FILENAME,
                        at.getAudioWaveformFilename());
            }

            serializer.endTag("", TAG_AUDIO_TRACK);
        }
        serializer.endTag("", TAG_AUDIO_TRACKS);

        serializer.endTag("", TAG_PROJECT);
        serializer.endDocument();

        /**
         *  Save the metadata XML file
         */
        final FileOutputStream out = new FileOutputStream(new File(getPath(),
                                                          PROJECT_FILENAME));
        out.write(writer.toString().getBytes());
        out.flush();
        out.close();
    }

    /*
     * {@inheritDoc}
     */
    public void setAspectRatio(int aspectRatio) {
        mAspectRatio = aspectRatio;
        /**
         *  Invalidate all transitions
         */
        mMANativeHelper.setGeneratePreview(true);

        for (Transition transition : mTransitions) {
            transition.invalidate();
        }

        final Iterator<MediaItem> it = mMediaItems.iterator();

        while (it.hasNext()) {
            final MediaItem t = it.next();
            List<Overlay> overlayList = t.getAllOverlays();
            for (Overlay overlay : overlayList) {

                ((OverlayFrame)overlay).invalidateGeneratedFiles();
            }
        }
    }

    /*
     * {@inheritDoc}
     */
    public void startPreview(SurfaceHolder surfaceHolder, long fromMs, long toMs,
                             boolean loop, int callbackAfterFrameCount,
                             PreviewProgressListener listener) {

        if (surfaceHolder == null) {
            throw new IllegalArgumentException();
        }

        final Surface surface = surfaceHolder.getSurface();
        if (surface == null) {
            throw new IllegalArgumentException("Surface could not be retrieved from surface holder");
        }

        if (surface.isValid() == false) {
            throw new IllegalStateException("Surface is not valid");
        }

        if (listener == null) {
            throw new IllegalArgumentException();
        }

        if (fromMs >= mDurationMs) {
            throw new IllegalArgumentException("Requested time not correct");
        }

        if (fromMs < 0) {
            throw new IllegalArgumentException("Requested time not correct");
        }

        boolean semAcquireDone = false;
        if (!mPreviewInProgress) {
            try{
                semAcquireDone = lock(ENGINE_ACCESS_MAX_TIMEOUT_MS);
                if (semAcquireDone == false) {
                    throw new IllegalStateException("Timeout waiting for semaphore");
                }

                if (mMANativeHelper == null) {
                    throw new IllegalStateException("The video editor is not initialized");
                }

                if (mMediaItems.size() > 0) {
                    mPreviewInProgress = true;
                    mMANativeHelper.previewStoryBoard(mMediaItems, mTransitions,
                                                      mAudioTracks, null);
                    mMANativeHelper.doPreview(surface, fromMs, toMs, loop,
                                     callbackAfterFrameCount, listener);
                }
                /**
                 *  Release The lock on complete by calling stopPreview
                 */
            } catch (InterruptedException ex) {
                Log.w(TAG, "The thread was interrupted", new Throwable());
                throw new IllegalStateException("The thread was interrupted");
            }
         } else {
            throw new IllegalStateException("Preview already in progress");
        }
    }

    /*
     * {@inheritDoc}
     */
    public long stopPreview() {
        long result = 0;
        if (mPreviewInProgress) {
            try {
                result = mMANativeHelper.stopPreview();
                /**
                 *  release on complete by calling stopPreview
                 */
                } finally {
                    mPreviewInProgress = false;
                    unlock();
                }
            return result;
        }
        else {
            return 0;
        }
    }

    /*
     * Remove transitions associated with the specified media item
     *
     * @param mediaItem The media item
     */
    private void removeAdjacentTransitions(MediaItem mediaItem) {
        final Transition beginTransition = mediaItem.getBeginTransition();
        if (beginTransition != null) {
            if (beginTransition.getAfterMediaItem() != null) {
                beginTransition.getAfterMediaItem().setEndTransition(null);
            }
            beginTransition.invalidate();
            mTransitions.remove(beginTransition);
        }

        final Transition endTransition = mediaItem.getEndTransition();
        if (endTransition != null) {
            if (endTransition.getBeforeMediaItem() != null) {
                endTransition.getBeforeMediaItem().setBeginTransition(null);
            }
            endTransition.invalidate();
            mTransitions.remove(endTransition);
        }

        mediaItem.setBeginTransition(null);
        mediaItem.setEndTransition(null);
    }

    /**
     * Remove the transition before this media item
     *
     * @param index The media item index
     */
    private void removeTransitionBefore(int index) {
        final MediaItem mediaItem = mMediaItems.get(index);
        final Iterator<Transition> it = mTransitions.iterator();
        while (it.hasNext()) {
            Transition t = it.next();
            if (t.getBeforeMediaItem() == mediaItem) {
                mMANativeHelper.setGeneratePreview(true);
                it.remove();
                t.invalidate();
                mediaItem.setBeginTransition(null);
                if (index > 0) {
                    mMediaItems.get(index - 1).setEndTransition(null);
                }
                break;
            }
        }
    }

    /**
     * Remove the transition after this media item
     *
     * @param mediaItem The media item
     */
    private void removeTransitionAfter(int index) {
        final MediaItem mediaItem = mMediaItems.get(index);
        final Iterator<Transition> it = mTransitions.iterator();
        while (it.hasNext()) {
            Transition t = it.next();
            if (t.getAfterMediaItem() == mediaItem) {
                mMANativeHelper.setGeneratePreview(true);
                it.remove();
                t.invalidate();
                mediaItem.setEndTransition(null);
                /**
                 *  Invalidate the reference in the next media item
                 */
                if (index < mMediaItems.size() - 1) {
                    mMediaItems.get(index + 1).setBeginTransition(null);
                }
                break;
            }
        }
    }

    /**
     * Compute the duration
     */
    private void computeTimelineDuration() {
        mDurationMs = 0;
        final int mediaItemsCount = mMediaItems.size();
        for (int i = 0; i < mediaItemsCount; i++) {
            final MediaItem mediaItem = mMediaItems.get(i);
            mDurationMs += mediaItem.getTimelineDuration();
            if (mediaItem.getEndTransition() != null) {
                if (i < mediaItemsCount - 1) {
                    mDurationMs -= mediaItem.getEndTransition().getDuration();
                }
            }
        }
    }

    /*
     * Generate the project thumbnail
     */
    private void generateProjectThumbnail() {
        /*
         * If a thumbnail already exists, then delete it first
         */
        if ((new File(mProjectPath + "/" + THUMBNAIL_FILENAME)).exists()) {
            (new File(mProjectPath + "/" + THUMBNAIL_FILENAME)).delete();
        }
        /*
         * Generate a new thumbnail for the project from first media Item
         */
        if (mMediaItems.size() > 0) {
            MediaItem mI = mMediaItems.get(0);
            /*
             * Keep aspect ratio of the image
             */
            int height = 480;
            int width = mI.getWidth() * height / mI.getHeight();

            Bitmap projectBitmap = null;
            String filename = mI.getFilename();
            if (mI instanceof MediaVideoItem) {
                MediaMetadataRetriever retriever = new MediaMetadataRetriever();
                retriever.setDataSource(filename);
                Bitmap bitmap = retriever.getFrameAtTime();
                retriever.release();
                retriever = null;
                if (bitmap == null) {
                    String msg = "Thumbnail extraction from " +
                                    filename + " failed";
                    throw new IllegalArgumentException(msg);
                }
                // Resize the thumbnail to the target size
                projectBitmap =
                    Bitmap.createScaledBitmap(bitmap, width, height, true);
            } else {
                try {
                    projectBitmap = mI.getThumbnail(width, height, 500);
                } catch (IllegalArgumentException e) {
                    String msg = "Project thumbnail extraction from " +
                                    filename + " failed";
                    throw new IllegalArgumentException(msg);
                } catch (IOException e) {
                    String msg = "IO Error creating project thumbnail";
                    throw new IllegalArgumentException(msg);
                }
            }

            try {
                FileOutputStream stream = new FileOutputStream(mProjectPath + "/"
                                                          + THUMBNAIL_FILENAME);
                projectBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
                stream.flush();
                stream.close();
            } catch (IOException e) {
                throw new IllegalArgumentException ("Error creating project thumbnail");
            } finally {
                projectBitmap.recycle();
            }
        }
    }

    /**
     * Clears the preview surface
     *
     * @param surfaceHolder SurfaceHolder where the preview is rendered
     * and needs to be cleared.
     */
    public void clearSurface(SurfaceHolder surfaceHolder) {
        if (surfaceHolder == null) {
            throw new IllegalArgumentException("Invalid surface holder");
        }

        final Surface surface = surfaceHolder.getSurface();
        if (surface == null) {
            throw new IllegalArgumentException("Surface could not be retrieved from surface holder");
        }

        if (surface.isValid() == false) {
            throw new IllegalStateException("Surface is not valid");
        }

        if (mMANativeHelper != null) {
            mMANativeHelper.clearPreviewSurface(surface);
        } else {
            Log.w(TAG, "Native helper was not ready!");
        }
    }

    /**
     * Grab the semaphore which arbitrates access to the editor
     *
     * @throws InterruptedException
     */
    private void lock() throws InterruptedException {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "lock: grabbing semaphore", new Throwable());
        }
        mLock.acquire();
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "lock: grabbed semaphore");
        }
    }

    /**
     * Tries to grab the semaphore with a specified time out which arbitrates access to the editor
     *
     * @param timeoutMs time out in ms.
     *
     * @return true if the semaphore is acquired, false otherwise
     * @throws InterruptedException
     */
    private boolean lock(long timeoutMs) throws InterruptedException {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "lock: grabbing semaphore with timeout " + timeoutMs, new Throwable());
        }

        boolean acquireSem = mLock.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS);
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "lock: grabbed semaphore status " + acquireSem);
        }

        return acquireSem;
    }

    /**
     * Release the semaphore which arbitrates access to the editor
     */
    private void unlock() {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "unlock: releasing semaphore");
        }
        mLock.release();
    }

    /**
     * Dumps the heap memory usage information to file
     */
    private static void dumpHeap (String filename) throws Exception {
        /* Cleanup as much as possible before dump
         */
        System.gc();
        System.runFinalization();
        Thread.sleep(1000);
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            String extDir =
             Environment.getExternalStorageDirectory().toString();

            /* If dump file already exists, then delete it first
            */
            if ((new File(extDir + "/" + filename + ".dump")).exists()) {
                (new File(extDir + "/" + filename + ".dump")).delete();
            }
            /* Dump native heap
            */
            FileOutputStream ost =
             new FileOutputStream(extDir + "/" + filename + ".dump");
            Debug.dumpNativeHeap(ost.getFD());
            ost.close();
        }
    }
}
