/*
 * 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.IOException;
import java.lang.ref.SoftReference;
import android.graphics.Bitmap;
import android.media.videoeditor.MediaArtistNativeHelper.ClipSettings;
import android.media.videoeditor.MediaArtistNativeHelper.Properties;
import android.media.videoeditor.VideoEditorProfile;
import android.view.Surface;
import android.view.SurfaceHolder;

/**
 * This class represents a video clip item on the storyboard
 * {@hide}
 */
public class MediaVideoItem extends MediaItem {

    /**
     *  Instance variables
     */
    private final int mWidth;
    private final int mHeight;
    private final int mAspectRatio;
    private final int mFileType;
    private final int mVideoType;
    private final int mVideoProfile;
    private final int mVideoLevel;
    private final int mVideoBitrate;
    private final long mDurationMs;
    private final int mAudioBitrate;
    private final int mFps;
    private final int mAudioType;
    private final int mAudioChannels;
    private final int mAudioSamplingFrequency;
    private long mBeginBoundaryTimeMs;
    private long mEndBoundaryTimeMs;
    private int mVolumePercentage;
    private boolean mMuted;
    private String mAudioWaveformFilename;
    private MediaArtistNativeHelper mMANativeHelper;
    private VideoEditorImpl mVideoEditor;
    private final int mVideoRotationDegree;
    /**
     *  The audio waveform data
     */
    private SoftReference<WaveformData> mWaveformData;

    /**
     * An object of this type cannot be instantiated with a default constructor
     */
    @SuppressWarnings("unused")
    private MediaVideoItem() throws IOException {
        this(null, null, null, RENDERING_MODE_BLACK_BORDER);
    }

    /**
     * Constructor
     *
     * @param editor The video editor reference
     * @param mediaItemId The MediaItem id
     * @param filename The image file name
     * @param renderingMode The rendering mode
     *
     * @throws IOException if the file cannot be opened for reading
     */
    public MediaVideoItem(VideoEditor editor, String mediaItemId, String filename,
            int renderingMode) throws IOException {
        this(editor, mediaItemId, filename, renderingMode, 0, END_OF_FILE, 100, false, null);
    }

    /**
     * Constructor
     *
     * @param editor The video editor reference
     * @param mediaItemId The MediaItem id
     * @param filename The image file name
     * @param renderingMode The rendering mode
     * @param beginMs Start time in milliseconds. Set to 0 to extract from the
     *           beginning
     * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to
     *           extract until the end
     * @param volumePercent in %/. 100% means no change; 50% means half value, 200%
     *            means double, 0% means silent.
     * @param muted true if the audio is muted
     * @param audioWaveformFilename The name of the audio waveform file
     *
     * @throws IOException if the file cannot be opened for reading
     */
    MediaVideoItem(VideoEditor editor, String mediaItemId, String filename,
            int renderingMode, long beginMs, long endMs, int volumePercent, boolean muted,
            String audioWaveformFilename)  throws IOException {
        super(editor, mediaItemId, filename, renderingMode);

        if (editor instanceof VideoEditorImpl) {
            mMANativeHelper = ((VideoEditorImpl)editor).getNativeContext();
            mVideoEditor = ((VideoEditorImpl)editor);
        }

        final Properties properties;
        try {
             properties = mMANativeHelper.getMediaProperties(filename);
        } catch ( Exception e) {
            throw new IllegalArgumentException(e.getMessage() + " : " + filename);
        }

        /** Check the platform specific maximum import resolution */
        VideoEditorProfile veProfile = VideoEditorProfile.get();
        if (veProfile == null) {
            throw new RuntimeException("Can't get the video editor profile");
        }
        final int maxInputWidth = veProfile.maxInputVideoFrameWidth;
        final int maxInputHeight = veProfile.maxInputVideoFrameHeight;
        if ((properties.width > maxInputWidth) ||
            (properties.height > maxInputHeight)) {
            throw new IllegalArgumentException(
                "Unsupported import resolution. Supported maximum width:" +
                maxInputWidth + " height:" + maxInputHeight +
                ", current width:" + properties.width +
                " height:" + properties.height);
        }
        /** Check the platform specific maximum video profile and level */
        if (!properties.profileSupported) {
            throw new IllegalArgumentException(
                "Unsupported video profile " + properties.profile);
        }
        if (!properties.levelSupported) {
            throw new IllegalArgumentException(
                "Unsupported video level " + properties.level);
        }
        switch (mMANativeHelper.getFileType(properties.fileType)) {
            case MediaProperties.FILE_3GP:
            case MediaProperties.FILE_MP4:
            case MediaProperties.FILE_M4V:
                break;

            default:
                throw new IllegalArgumentException("Unsupported Input File Type");
        }

        switch (mMANativeHelper.getVideoCodecType(properties.videoFormat)) {
            case MediaProperties.VCODEC_H263:
            case MediaProperties.VCODEC_H264:
            case MediaProperties.VCODEC_MPEG4:
                break;

            default:
                throw new IllegalArgumentException("Unsupported Video Codec Format in Input File");
        }

        mWidth = properties.width;
        mHeight = properties.height;
        mAspectRatio = mMANativeHelper.getAspectRatio(properties.width,
                properties.height);
        mFileType = mMANativeHelper.getFileType(properties.fileType);
        mVideoType = mMANativeHelper.getVideoCodecType(properties.videoFormat);
        mVideoProfile = properties.profile;
        mVideoLevel = properties.level;
        mDurationMs = properties.videoDuration;
        mVideoBitrate = properties.videoBitrate;
        mAudioBitrate = properties.audioBitrate;
        mFps = (int)properties.averageFrameRate;
        mAudioType = mMANativeHelper.getAudioCodecType(properties.audioFormat);
        mAudioChannels = properties.audioChannels;
        mAudioSamplingFrequency =  properties.audioSamplingFrequency;
        mBeginBoundaryTimeMs = beginMs;
        mEndBoundaryTimeMs = endMs == END_OF_FILE ? mDurationMs : endMs;
        mVolumePercentage = volumePercent;
        mMuted = muted;
        mAudioWaveformFilename = audioWaveformFilename;
        if (audioWaveformFilename != null) {
            mWaveformData = new SoftReference<WaveformData>(
                        new WaveformData(audioWaveformFilename));
        } else {
            mWaveformData = null;
        }
        mVideoRotationDegree = properties.videoRotation;
    }

    /**
     * Sets the start and end marks for trimming a video media item.
     * This method will adjust the duration of bounding transitions, effects
     * and overlays if the current duration of the transactions become greater
     * than the maximum allowable duration.
     *
     * @param beginMs Start time in milliseconds. Set to 0 to extract from the
     *           beginning
     * @param endMs End time in milliseconds. Set to {@link #END_OF_FILE} to
     *           extract until the end
     *
     * @throws IllegalArgumentException if the start time is greater or equal than
     *           end time, the end time is beyond the file duration, the start time
     *           is negative
     */
    public void setExtractBoundaries(long beginMs, long endMs) {
        if (beginMs > mDurationMs) {
            throw new IllegalArgumentException("setExtractBoundaries: Invalid start time");
        }

        if (endMs > mDurationMs) {
            throw new IllegalArgumentException("setExtractBoundaries: Invalid end time");
        }

        if ((endMs != -1) && (beginMs >= endMs) ) {
            throw new IllegalArgumentException("setExtractBoundaries: Start time is greater than end time");
        }

        if ((beginMs < 0) || ((endMs != -1) && (endMs < 0))) {
            throw new IllegalArgumentException("setExtractBoundaries: Start time or end time is negative");
        }

        mMANativeHelper.setGeneratePreview(true);

        if (beginMs != mBeginBoundaryTimeMs) {
            if (mBeginTransition != null) {
                mBeginTransition.invalidate();
            }
        }

        if (endMs != mEndBoundaryTimeMs) {
            if (mEndTransition != null) {
                mEndTransition.invalidate();
            }
        }

        mBeginBoundaryTimeMs = beginMs;
        mEndBoundaryTimeMs = endMs;
        adjustTransitions();
        mVideoEditor.updateTimelineDuration();
        /**
         *  Note that the start and duration of any effects and overlays are
         *  not adjusted nor are they automatically removed if they fall
         *  outside the new boundaries.
         */
    }

    /**
     * @return The boundary begin time
     */
    public long getBoundaryBeginTime() {
        return mBeginBoundaryTimeMs;
    }

    /**
     * @return The boundary end time
     */
    public long getBoundaryEndTime() {
        return mEndBoundaryTimeMs;
    }

    /*
     * {@inheritDoc}
     */
    @Override
    public void addEffect(Effect effect) {
        if (effect instanceof EffectKenBurns) {
            throw new IllegalArgumentException("Ken Burns effects cannot be applied to MediaVideoItem");
        }
        super.addEffect(effect);
    }

    /*
     * {@inheritDoc}
     */
    @Override
    public Bitmap getThumbnail(int width, int height, long timeMs) {
        if (timeMs > mDurationMs) {
            throw new IllegalArgumentException("Time Exceeds duration");
        }

        if (timeMs < 0) {
            throw new IllegalArgumentException("Invalid Time duration");
        }

        if ((width <= 0) || (height <= 0)) {
            throw new IllegalArgumentException("Invalid Dimensions");
        }

        if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) {
            int temp = width;
            width = height;
            height = temp;
        }

        return mMANativeHelper.getPixels(
                getFilename(), width, height, timeMs, mVideoRotationDegree);
    }

    /*
     * {@inheritDoc}
     */
    @Override
    public void getThumbnailList(int width, int height,
                                 long startMs, long endMs,
                                 int thumbnailCount,
                                 int[] indices,
                                 GetThumbnailListCallback callback)
                                 throws IOException {
        if (startMs > endMs) {
            throw new IllegalArgumentException("Start time is greater than end time");
        }

        if (endMs > mDurationMs) {
            throw new IllegalArgumentException("End time is greater than file duration");
        }

        if ((height <= 0) || (width <= 0)) {
            throw new IllegalArgumentException("Invalid dimension");
        }

        if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) {
            int temp = width;
            width = height;
            height = temp;
        }

        mMANativeHelper.getPixelsList(getFilename(), width, height,
                startMs, endMs, thumbnailCount, indices, callback,
                mVideoRotationDegree);
    }

    /*
     * {@inheritDoc}
     */
    @Override
    void invalidateTransitions(long startTimeMs, long durationMs) {
        /**
         *  Check if the item overlaps with the beginning and end transitions
         */
        if (mBeginTransition != null) {
            if (isOverlapping(startTimeMs, durationMs,
                    mBeginBoundaryTimeMs, mBeginTransition.getDuration())) {
                mBeginTransition.invalidate();
            }
        }

        if (mEndTransition != null) {
            final long transitionDurationMs = mEndTransition.getDuration();
            if (isOverlapping(startTimeMs, durationMs,
                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs)) {
                mEndTransition.invalidate();
            }
        }
    }

    /*
     * {@inheritDoc}
     */
    @Override
    void invalidateTransitions(long oldStartTimeMs, long oldDurationMs, long newStartTimeMs,
            long newDurationMs) {
        /**
         *  Check if the item overlaps with the beginning and end transitions
         */
        if (mBeginTransition != null) {
            final long transitionDurationMs = mBeginTransition.getDuration();
            final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs,
                    mBeginBoundaryTimeMs, transitionDurationMs);
            final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs,
                    mBeginBoundaryTimeMs, transitionDurationMs);
            /**
             * Invalidate transition if:
             *
             * 1. New item overlaps the transition, the old one did not
             * 2. New item does not overlap the transition, the old one did
             * 3. New and old item overlap the transition if begin or end
             * time changed
             */
            if (newOverlap != oldOverlap) { // Overlap has changed
                mBeginTransition.invalidate();
            } else if (newOverlap) { // Both old and new overlap
                if ((oldStartTimeMs != newStartTimeMs) ||
                        !(oldStartTimeMs + oldDurationMs > transitionDurationMs &&
                        newStartTimeMs + newDurationMs > transitionDurationMs)) {
                    mBeginTransition.invalidate();
                }
            }
        }

        if (mEndTransition != null) {
            final long transitionDurationMs = mEndTransition.getDuration();
            final boolean oldOverlap = isOverlapping(oldStartTimeMs, oldDurationMs,
                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs);
            final boolean newOverlap = isOverlapping(newStartTimeMs, newDurationMs,
                    mEndBoundaryTimeMs - transitionDurationMs, transitionDurationMs);
            /**
             * Invalidate transition if:
             *
             * 1. New item overlaps the transition, the old one did not
             * 2. New item does not overlap the transition, the old one did
             * 3. New and old item overlap the transition if begin or end
             * time changed
             */
            if (newOverlap != oldOverlap) { // Overlap has changed
                mEndTransition.invalidate();
            } else if (newOverlap) { // Both old and new overlap
                if ((oldStartTimeMs + oldDurationMs != newStartTimeMs + newDurationMs) ||
                        ((oldStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs) ||
                        newStartTimeMs > mEndBoundaryTimeMs - transitionDurationMs)) {
                    mEndTransition.invalidate();
                }
            }
        }
    }

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

    /*
     * {@inheritDoc}
     */
    @Override
    public int getFileType() {
        return mFileType;
    }

    /*
     * {@inheritDoc}
     */
    @Override
    public int getWidth() {
        if (mVideoRotationDegree == 90 ||
             mVideoRotationDegree == 270) {
            return mHeight;
        } else {
            return mWidth;
        }
    }

    /*
     * {@inheritDoc}
     */
    @Override
    public int getHeight() {
        if (mVideoRotationDegree == 90 ||
             mVideoRotationDegree == 270) {
            return mWidth;
        } else {
            return mHeight;
        }
    }

    /*
     * {@inheritDoc}
     */
    @Override
    public long getDuration() {
        return mDurationMs;
    }

    /*
     * {@inheritDoc}
     */
    @Override
    public long getTimelineDuration() {
        return mEndBoundaryTimeMs - mBeginBoundaryTimeMs;
    }

    /**
     * Render a frame according to the playback (in the native aspect ratio) for
     * the specified media item. All effects and overlays applied to the media
     * item are ignored. The extract boundaries are also ignored. This method
     * can be used to playback frames when implementing trimming functionality.
     *
     * @param surfaceHolder SurfaceHolder used by the application
     * @param timeMs time corresponding to the frame to display (relative to the
     *            the beginning of the media item).
     * @return The accurate time stamp of the frame that is rendered .
     * @throws IllegalStateException if a playback, preview or an export is
     *             already in progress
     * @throws IllegalArgumentException if time is negative or greater than the
     *             media item duration
     */
    public long renderFrame(SurfaceHolder surfaceHolder, long timeMs) {
        if (surfaceHolder == null) {
            throw new IllegalArgumentException("Surface Holder is null");
        }

        if (timeMs > mDurationMs || timeMs < 0) {
            throw new IllegalArgumentException("requested time not correct");
        }

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

        if (mFilename != null) {
            return mMANativeHelper.renderMediaItemPreviewFrame(surface,
                    mFilename,timeMs,mWidth,mHeight);
        } else {
            return 0;
        }
    }


    /**
     * This API allows to generate a file containing the sample volume levels of
     * the Audio track of this media item. This function may take significant
     * time and is blocking. The file can be retrieved using
     * getAudioWaveformFilename().
     *
     * @param listener The progress listener
     *
     * @throws IOException if the output file cannot be created
     * @throws IllegalArgumentException if the mediaItem does not have a valid
     *             Audio track
     */
    public void extractAudioWaveform(ExtractAudioWaveformProgressListener listener)
        throws IOException {
        int frameDuration = 0;
        int sampleCount = 0;
        final String projectPath = mMANativeHelper.getProjectPath();
        /**
         *  Waveform file does not exist
         */
        if (mAudioWaveformFilename == null ) {
            /**
             * Since audioWaveformFilename will not be supplied,it is  generated
             */
            String mAudioWaveFileName = null;

            mAudioWaveFileName =
                String.format(projectPath + "/" + "audioWaveformFile-"+ getId() + ".dat");
            /**
             * Logic to get frame duration = (no. of frames per sample * 1000)/
             * sampling frequency
             */
            if (mMANativeHelper.getAudioCodecType(mAudioType) ==
                MediaProperties.ACODEC_AMRNB ) {
                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRNB*1000)/
                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRNB;
            } else if (mMANativeHelper.getAudioCodecType(mAudioType) ==
                MediaProperties.ACODEC_AMRWB ) {
                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AMRWB * 1000)/
                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AMRWB;
            } else if (mMANativeHelper.getAudioCodecType(mAudioType) ==
                MediaProperties.ACODEC_AAC_LC ) {
                frameDuration = (MediaProperties.SAMPLES_PER_FRAME_AAC * 1000)/
                MediaProperties.DEFAULT_SAMPLING_FREQUENCY;
                sampleCount = MediaProperties.SAMPLES_PER_FRAME_AAC;
            }

            mMANativeHelper.generateAudioGraph( getId(),
                    mFilename,
                    mAudioWaveFileName,
                    frameDuration,
                    MediaProperties.DEFAULT_CHANNEL_COUNT,
                    sampleCount,
                    listener,
                    true);
            /**
             * Record the generated file name
             */
            mAudioWaveformFilename = mAudioWaveFileName;
        }
        mWaveformData =
            new SoftReference<WaveformData>(new WaveformData(mAudioWaveformFilename));
    }

    /**
     * Get the audio waveform file name if {@link #extractAudioWaveform()} was
     * successful. The file format is as following:
     * <ul>
     *  <li>first 4 bytes provide the number of samples for each value, as big-endian signed</li>
     *  <li>4 following bytes is the total number of values in the file, as big-endian signed</li>
     *  <li>all values follow as bytes Name is unique.</li>
     *</ul>
     * @return the name of the file, null if the file has not been computed or
     *         if there is no Audio track in the mediaItem
     */
    String getAudioWaveformFilename() {
        return mAudioWaveformFilename;
    }

    /**
     * Invalidate the AudioWaveform File
     */
    void invalidate() {
        if (mAudioWaveformFilename != null) {
            new File(mAudioWaveformFilename).delete();
            mAudioWaveformFilename = null;
        }
    }

    /**
     * @return The waveform data
     */
    public WaveformData getWaveformData() throws IOException {
        if (mWaveformData == null) {
            return null;
        }

        WaveformData waveformData = mWaveformData.get();
        if (waveformData != null) {
            return waveformData;
        } else if (mAudioWaveformFilename != null) {
            try {
                waveformData = new WaveformData(mAudioWaveformFilename);
            } catch(IOException e) {
                throw e;
            }
            mWaveformData = new SoftReference<WaveformData>(waveformData);
            return waveformData;
        } else {
            return null;
        }
    }

    /**
     * Set volume of the Audio track of this mediaItem
     *
     * @param volumePercent in %/. 100% means no change; 50% means half value, 200%
     *            means double, 0% means silent.
     * @throws UsupportedOperationException if volume value is not supported
     */
    public void setVolume(int volumePercent) {
        if ((volumePercent <0) || (volumePercent >100)) {
            throw new IllegalArgumentException("Invalid volume");
        }

        mVolumePercentage = volumePercent;
    }

    /**
     * Get the volume value of the audio track as percentage. Call of this
     * method before calling setVolume will always return 100%
     *
     * @return the volume in percentage
     */
    public int getVolume() {
        return mVolumePercentage;
    }

    /**
     * @param muted true to mute the media item
     */
    public void setMute(boolean muted) {
        mMANativeHelper.setGeneratePreview(true);
        mMuted = muted;
        if (mBeginTransition != null) {
            mBeginTransition.invalidate();
        }
        if (mEndTransition != null) {
            mEndTransition.invalidate();
        }
    }

    /**
     * @return true if the media item is muted
     */
    public boolean isMuted() {
        return mMuted;
    }

    /**
     * @return The video type
     */
    public int getVideoType() {
        return mVideoType;
    }

    /**
     * @return The video profile
     */
    public int getVideoProfile() {
        return mVideoProfile;
    }

    /**
     * @return The video profile
     */
    public int getVideoLevel() {
        return mVideoLevel;
    }

    /**
     * @return The video bitrate
     */
    public int getVideoBitrate() {
        return mVideoBitrate;
    }

    /**
     * @return The audio bitrate
     */
    public int getAudioBitrate() {
        return mAudioBitrate;
    }

    /**
     * @return The number of frames per second
     */
    public int getFps() {
        return mFps;
    }

    /**
     * @return The audio codec
     */
    public int getAudioType() {
        return mAudioType;
    }

    /**
     * @return The number of audio channels
     */
    public int getAudioChannels() {
        return mAudioChannels;
    }

    /**
     * @return The audio sample frequency
     */
    public int getAudioSamplingFrequency() {
        return mAudioSamplingFrequency;
    }

    /**
     * @return The Video media item properties in ClipSettings class object
     * {@link android.media.videoeditor.MediaArtistNativeHelper.ClipSettings}
     */
    ClipSettings getVideoClipProperties() {
        ClipSettings clipSettings = new ClipSettings();
        clipSettings.clipPath = getFilename();
        clipSettings.fileType = mMANativeHelper.getMediaItemFileType(getFileType());
        clipSettings.beginCutTime = (int)getBoundaryBeginTime();
        clipSettings.endCutTime = (int)getBoundaryEndTime();
        clipSettings.mediaRendering = mMANativeHelper.getMediaItemRenderingMode(getRenderingMode());
        clipSettings.rotationDegree = mVideoRotationDegree;

        return clipSettings;
    }
}
