/*
 * Copyright (C) 2015 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 com.android.messaging.datamodel.data;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteStatement;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;

import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.action.UpdateMessagePartSizeAction;
import com.android.messaging.datamodel.media.ImageRequest;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.GifTranscoder;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UriUtil;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * Represents a single message part. Messages consist of one or more parts which may contain
 * either text or media.
 */
public class MessagePartData implements Parcelable {
    public static final int UNSPECIFIED_SIZE = MessagingContentProvider.UNSPECIFIED_SIZE;

    public static final String[] ACCEPTABLE_GALLERY_MEDIA_TYPES =
            new String[] {
                // Acceptable image types
                ContentType.IMAGE_JPEG, ContentType.IMAGE_JPG, ContentType.IMAGE_PNG,
                ContentType.IMAGE_GIF, ContentType.IMAGE_WBMP, ContentType.IMAGE_X_MS_BMP,
                // Acceptable video types
                ContentType.VIDEO_3GP, ContentType.VIDEO_3GPP, ContentType.VIDEO_3G2,
                ContentType.VIDEO_H263, ContentType.VIDEO_M4V, ContentType.VIDEO_MP4,
                ContentType.VIDEO_MPEG, ContentType.VIDEO_MPEG4, ContentType.VIDEO_WEBM,
                // Acceptable audio types
                ContentType.AUDIO_MP3, ContentType.AUDIO_MP4, ContentType.AUDIO_MIDI,
                ContentType.AUDIO_MID, ContentType.AUDIO_AMR, ContentType.AUDIO_X_WAV,
                ContentType.AUDIO_AAC, ContentType.AUDIO_X_MIDI, ContentType.AUDIO_X_MID,
                ContentType.AUDIO_X_MP3
            };

    private static final String[] sProjection = {
        PartColumns._ID,
        PartColumns.MESSAGE_ID,
        PartColumns.TEXT,
        PartColumns.CONTENT_URI,
        PartColumns.CONTENT_TYPE,
        PartColumns.WIDTH,
        PartColumns.HEIGHT,
    };

    private static final int INDEX_ID = 0;
    private static final int INDEX_MESSAGE_ID = 1;
    private static final int INDEX_TEXT = 2;
    private static final int INDEX_CONTENT_URI = 3;
    private static final int INDEX_CONTENT_TYPE = 4;
    private static final int INDEX_WIDTH = 5;
    private static final int INDEX_HEIGHT = 6;
    // This isn't part of the projection
    private static final int INDEX_CONVERSATION_ID = 7;

    // SQL statement to insert a "complete" message part row (columns based on projection above).
    private static final String INSERT_MESSAGE_PART_SQL =
            "INSERT INTO " + DatabaseHelper.PARTS_TABLE + " ( "
                    + TextUtils.join(",", Arrays.copyOfRange(sProjection, 1, INDEX_CONVERSATION_ID))
                    + ", " + PartColumns.CONVERSATION_ID
                    + ") VALUES (?, ?, ?, ?, ?, ?, ?)";

    // Used for stuff that's ignored or arbitrarily compressed.
    private static final long NO_MINIMUM_SIZE = 0;

    private String mPartId;
    private String mMessageId;
    private String mText;
    private Uri mContentUri;
    private String mContentType;
    private int mWidth;
    private int mHeight;
    // This kind of part can only be attached once and with no other attachment
    private boolean mSinglePartOnly;

    /** Transient data: true if destroy was already called */
    private boolean mDestroyed;

    /**
     * Create an "empty" message part
     */
    protected MessagePartData() {
        this(null, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE);
    }

    /**
     * Create a populated text message part
     */
    protected MessagePartData(final String messageText) {
        this(null, messageText, ContentType.TEXT_PLAIN, null, UNSPECIFIED_SIZE, UNSPECIFIED_SIZE,
                false /*singlePartOnly*/);
    }

    /**
     * Create a populated attachment message part
     */
    protected MessagePartData(final String contentType, final Uri contentUri,
            final int width, final int height) {
        this(null, null, contentType, contentUri, width, height, false /*singlePartOnly*/);
    }

    /**
     * Create a populated attachment message part, with additional caption text
     */
    protected MessagePartData(final String messageText, final String contentType,
            final Uri contentUri, final int width, final int height) {
        this(null, messageText, contentType, contentUri, width, height, false /*singlePartOnly*/);
    }

    /**
     * Create a populated attachment message part, with additional caption text, single part only
     */
    protected MessagePartData(final String messageText, final String contentType,
            final Uri contentUri, final int width, final int height, final boolean singlePartOnly) {
        this(null, messageText, contentType, contentUri, width, height, singlePartOnly);
    }

    /**
     * Create a populated message part
     */
    private MessagePartData(final String messageId, final String messageText,
            final String contentType, final Uri contentUri, final int width, final int height,
            final boolean singlePartOnly) {
        mMessageId = messageId;
        mText = messageText;
        mContentType = contentType;
        mContentUri = contentUri;
        mWidth = width;
        mHeight = height;
        mSinglePartOnly = singlePartOnly;
    }

    /**
     * Create a "text" message part
     */
    public static MessagePartData createTextMessagePart(final String messageText) {
        return new MessagePartData(messageText);
    }

    /**
     * Create a "media" message part
     */
    public static MessagePartData createMediaMessagePart(final String contentType,
            final Uri contentUri, final int width, final int height) {
        return new MessagePartData(contentType, contentUri, width, height);
    }

    /**
     * Create a "media" message part with caption
     */
    public static MessagePartData createMediaMessagePart(final String caption,
            final String contentType, final Uri contentUri, final int width, final int height) {
        return new MessagePartData(null, caption, contentType, contentUri, width, height,
                false /*singlePartOnly*/
        );
    }

    /**
     * Create an empty "text" message part
     */
    public static MessagePartData createEmptyMessagePart() {
        return new MessagePartData("");
    }

    /**
     * Creates a new message part reading from the cursor
     */
    public static MessagePartData createFromCursor(final Cursor cursor) {
        final MessagePartData part = new MessagePartData();
        part.bind(cursor);
        return part;
    }

    public static String[] getProjection() {
        return sProjection;
    }

    /**
     * Updates the part id.
     * Can be used to reset the partId just prior to persisting (which will assign a new partId)
     *  or can be called on a part that does not yet have a valid part id to set it.
     */
    public void updatePartId(final String partId) {
        Assert.isTrue(TextUtils.isEmpty(partId) || TextUtils.isEmpty(mPartId));
        mPartId = partId;
    }

    /**
     * Updates the messageId for the part.
     * Can be used to reset the messageId prior to persisting (which will assign a new messageId)
     *  or can be called on a part that does not yet have a valid messageId to set it.
     */
    public void updateMessageId(final String messageId) {
        Assert.isTrue(TextUtils.isEmpty(messageId) || TextUtils.isEmpty(mMessageId));
        mMessageId = messageId;
    }

    protected static String getMessageId(final Cursor cursor) {
        return cursor.getString(INDEX_MESSAGE_ID);
    }

    protected void bind(final Cursor cursor) {
        mPartId = cursor.getString(INDEX_ID);
        mMessageId = cursor.getString(INDEX_MESSAGE_ID);
        mText = cursor.getString(INDEX_TEXT);
        mContentUri = UriUtil.uriFromString(cursor.getString(INDEX_CONTENT_URI));
        mContentType = cursor.getString(INDEX_CONTENT_TYPE);
        mWidth = cursor.getInt(INDEX_WIDTH);
        mHeight = cursor.getInt(INDEX_HEIGHT);
    }

    public final void populate(final ContentValues values) {
        // Must have a valid messageId on a part
        Assert.isTrue(!TextUtils.isEmpty(mMessageId));
        values.put(PartColumns.MESSAGE_ID, mMessageId);
        values.put(PartColumns.TEXT, mText);
        values.put(PartColumns.CONTENT_URI, UriUtil.stringFromUri(mContentUri));
        values.put(PartColumns.CONTENT_TYPE, mContentType);
        if (mWidth != UNSPECIFIED_SIZE) {
            values.put(PartColumns.WIDTH, mWidth);
        }
        if (mHeight != UNSPECIFIED_SIZE) {
            values.put(PartColumns.HEIGHT, mHeight);
        }
    }

    /**
     * Note this is not thread safe so callers need to make sure they own the wrapper + statements
     * while they call this and use the returned value.
     */
    public SQLiteStatement getInsertStatement(final DatabaseWrapper db,
                                              final String conversationId) {
        final SQLiteStatement insert = db.getStatementInTransaction(
                DatabaseWrapper.INDEX_INSERT_MESSAGE_PART, INSERT_MESSAGE_PART_SQL);
        insert.clearBindings();
        insert.bindString(INDEX_MESSAGE_ID, mMessageId);
        if (mText != null) {
            insert.bindString(INDEX_TEXT, mText);
        }
        if (mContentUri != null) {
            insert.bindString(INDEX_CONTENT_URI, mContentUri.toString());
        }
        if (mContentType != null) {
            insert.bindString(INDEX_CONTENT_TYPE, mContentType);
        }
        insert.bindLong(INDEX_WIDTH, mWidth);
        insert.bindLong(INDEX_HEIGHT, mHeight);
        insert.bindString(INDEX_CONVERSATION_ID, conversationId);
        return insert;
    }

    public final String getPartId() {
        return mPartId;
    }

    public final String getMessageId() {
        return mMessageId;
    }

    public final String getText() {
        return mText;
    }

    public final Uri getContentUri() {
        return mContentUri;
    }

    public boolean isAttachment() {
        return mContentUri != null;
    }

    public boolean isText() {
        return ContentType.isTextType(mContentType);
    }

    public boolean isImage() {
        return ContentType.isImageType(mContentType);
    }

    public boolean isMedia() {
        return ContentType.isMediaType(mContentType);
    }

    public boolean isVCard() {
        return ContentType.isVCardType(mContentType);
    }

    public boolean isAudio() {
        return ContentType.isAudioType(mContentType);
    }

    public boolean isVideo() {
        return ContentType.isVideoType(mContentType);
    }

    public final String getContentType() {
        return mContentType;
    }

    public final int getWidth() {
        return mWidth;
    }

    public final int getHeight() {
        return mHeight;
    }

    public static boolean isSupportedMediaType(final String contentType) {
        return ContentType.isVCardType(contentType)
                || Arrays.asList(ACCEPTABLE_GALLERY_MEDIA_TYPES).contains(contentType);
    }

    /**
    *
    * @return true if this part can only exist by itself, with no other attachments
    */
    public boolean getSinglePartOnly() {
        return mSinglePartOnly;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    protected MessagePartData(final Parcel in) {
        mMessageId = in.readString();
        mText = in.readString();
        mContentUri = UriUtil.uriFromString(in.readString());
        mContentType = in.readString();
        mWidth = in.readInt();
        mHeight = in.readInt();
    }

    @Override
    public void writeToParcel(final Parcel dest, final int flags) {
        Assert.isTrue(!mDestroyed);
        dest.writeString(mMessageId);
        dest.writeString(mText);
        dest.writeString(UriUtil.stringFromUri(mContentUri));
        dest.writeString(mContentType);
        dest.writeInt(mWidth);
        dest.writeInt(mHeight);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof MessagePartData)) {
          return false;
        }

        MessagePartData lhs = (MessagePartData) o;
        return mWidth == lhs.mWidth && mHeight == lhs.mHeight &&
                TextUtils.equals(mMessageId, lhs.mMessageId) &&
                TextUtils.equals(mText, lhs.mText) &&
                TextUtils.equals(mContentType, lhs.mContentType) &&
                (mContentUri == null ? lhs.mContentUri == null
                                     : mContentUri.equals(lhs.mContentUri));
    }

    @Override public int hashCode() {
        int result = 17;
        result = 31 * result + mWidth;
        result = 31 * result + mHeight;
        result = 31 * result + (mMessageId == null ? 0 : mMessageId.hashCode());
        result = 31 * result + (mText == null ? 0 : mText.hashCode());
        result = 31 * result + (mContentType == null ? 0 : mContentType.hashCode());
        result = 31 * result + (mContentUri == null ? 0 : mContentUri.hashCode());
        return result;
      }

    public static final Parcelable.Creator<MessagePartData> CREATOR
            = new Parcelable.Creator<MessagePartData>() {
        @Override
        public MessagePartData createFromParcel(final Parcel in) {
            return new MessagePartData(in);
        }

        @Override
        public MessagePartData[] newArray(final int size) {
            return new MessagePartData[size];
        }
    };

    protected Uri shouldDestroy() {
        // We should never double-destroy.
        Assert.isTrue(!mDestroyed);
        mDestroyed = true;
        Uri contentUri = mContentUri;
        mContentUri = null;
        mContentType = null;
        // Only destroy the image if it's staged in our scratch space.
        if (!MediaScratchFileProvider.isMediaScratchSpaceUri(contentUri)) {
            contentUri = null;
        }
        return contentUri;
    }

    /**
     * If application owns content associated with this part delete it (on background thread)
     */
    public void destroyAsync() {
        final Uri contentUri = shouldDestroy();
        if (contentUri != null) {
            SafeAsyncTask.executeOnThreadPool(new Runnable() {
                @Override
                public void run() {
                    Factory.get().getApplicationContext().getContentResolver().delete(
                            contentUri, null, null);
                }
            });
        }
    }

    /**
     * If application owns content associated with this part delete it
     */
    public void destroySync() {
        final Uri contentUri = shouldDestroy();
        if (contentUri != null) {
            Factory.get().getApplicationContext().getContentResolver().delete(
                    contentUri, null, null);
        }
    }

    /**
     * If this is an image part, decode the image header and potentially save the size to the db.
     */
    public void decodeAndSaveSizeIfImage(final boolean saveToStorage) {
        if (isImage()) {
            final Rect imageSize = ImageUtils.decodeImageBounds(
                    Factory.get().getApplicationContext(), mContentUri);
            if (imageSize.width() != ImageRequest.UNSPECIFIED_SIZE &&
                    imageSize.height() != ImageRequest.UNSPECIFIED_SIZE) {
                mWidth = imageSize.width();
                mHeight = imageSize.height();
                if (saveToStorage) {
                    UpdateMessagePartSizeAction.updateSize(mPartId, mWidth, mHeight);
                }
            }
        }
    }

    /**
     * Computes the minimum size that this MessagePartData could be compressed/downsampled/encoded
     * before sending to meet the maximum message size imposed by the carriers. This is used to
     * determine right before sending a message whether a message could possibly be sent. If not
     * then the user is given a chance to unselect some/all of the attachments.
     *
     * TODO: computing the minimum size could be expensive. Should we cache the
     * computed value in db to be retrieved later?
     *
     * @return the carrier-independent minimum size, in bytes.
     */
    @DoesNotRunOnMainThread
    public long getMinimumSizeInBytesForSending() {
        Assert.isNotMainThread();
        if (!isAttachment()) {
            // No limit is imposed on non-attachment part (i.e. plain text), so treat it as zero.
            return NO_MINIMUM_SIZE;
        } else if (isImage()) {
            // GIFs are resized by the native transcoder (exposed by GifTranscoder).
            if (ImageUtils.isGif(mContentType, mContentUri)) {
                final long originalImageSize = UriUtil.getContentSize(mContentUri);
                // Wish we could save the size here, but we don't have a part id yet
                decodeAndSaveSizeIfImage(false /* saveToStorage */);
                return GifTranscoder.canBeTranscoded(mWidth, mHeight) ?
                        GifTranscoder.estimateFileSizeAfterTranscode(originalImageSize)
                        : originalImageSize;
            }
            // Other images should be arbitrarily resized by ImageResizer before sending.
            return MmsUtils.MIN_IMAGE_BYTE_SIZE;
        } else if (isMedia()) {
            // We can't compress attachments except images.
            return UriUtil.getContentSize(mContentUri);
        } else {
            // This is some unknown media type that we don't know how to handle. Log an error
            // and try sending it anyway.
            LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Unknown attachment type " + getContentType());
            return NO_MINIMUM_SIZE;
        }
    }

    @Override
    public String toString() {
        if (isText()) {
            return LogUtil.sanitizePII(getText());
        } else {
            return getContentType() + " (" + getContentUri() + ")";
        }
    }

    /**
     *
     * @return true if this part can only exist by itself, with no other attachments
     */
    public boolean isSinglePartOnly() {
        return mSinglePartOnly;
    }

    public void setSinglePartOnly(final boolean isSinglePartOnly) {
        mSinglePartOnly = isSinglePartOnly;
    }
}
