/*
 * 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.action;

import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;

import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;

import java.util.ArrayList;

/**
 * Action used to send an outgoing message. It writes MMS messages to the telephony db
 * ({@link InsertNewMessageAction}) writes SMS messages to the telephony db). It also
 * initiates the actual sending. It will all be used for re-sending a failed message.
 * <p>
 * This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
 * access the EXTRA_* fields for setting up the 'sent' pending intent.
 */
public class SendMessageAction extends Action implements Parcelable {
    private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;

    /**
     * Queue sending of existing message (can only be called during execute of action)
     */
    static boolean queueForSendInBackground(final String messageId,
            final Action processingAction) {
        final SendMessageAction action = new SendMessageAction();
        return action.queueAction(messageId, processingAction);
    }

    public static final boolean DEFAULT_DELIVERY_REPORT_MODE  = false;
    public static final int MAX_SMS_RETRY = 3;

    // Core parameters needed for all types of message
    private static final String KEY_MESSAGE_ID = "message_id";
    private static final String KEY_MESSAGE = "message";
    private static final String KEY_MESSAGE_URI = "message_uri";
    private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";

    // For sms messages a few extra values are included in the bundle
    private static final String KEY_RECIPIENT = "recipient";
    private static final String KEY_RECIPIENTS = "recipients";
    private static final String KEY_SMS_SERVICE_CENTER = "sms_service_center";

    // Values we attach to the pending intent that's fired when the message is sent.
    // Only applicable when sending via the platform APIs on L+.
    public static final String KEY_SUB_ID = "sub_id";
    public static final String EXTRA_MESSAGE_ID = "message_id";
    public static final String EXTRA_UPDATED_MESSAGE_URI = "updated_message_uri";
    public static final String EXTRA_CONTENT_URI = "content_uri";
    public static final String EXTRA_RESPONSE_IMPORTANT = "response_important";

    /**
     * Constructor used for retrying sending in the background (only message id available)
     */
    private SendMessageAction() {
        super();
    }

    /**
     * Read message from database and queue actual sending
     */
    private boolean queueAction(final String messageId, final Action processingAction) {
        actionParameters.putString(KEY_MESSAGE_ID, messageId);

        final DatabaseWrapper db = DataModel.get().getDatabase();

        final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
        // Check message can be resent
        if (message != null && message.canSendMessage()) {
            final boolean isSms = message.getIsSms();
            long timestamp = System.currentTimeMillis();
            if (!isSms) {
                // MMS expects timestamp rounded to nearest second
                timestamp = 1000 * ((timestamp + 500) / 1000);
            }

            final ParticipantData self = BugleDatabaseOperations.getExistingParticipant(
                    db, message.getSelfId());
            final Uri messageUri = message.getSmsMessageUri();
            final String conversationId = message.getConversationId();

            // Update message status
            if (message.getYetToSend()) {
                if (message.getReceivedTimeStamp() == message.getRetryStartTimestamp()) {
                    // Initial sending of message
                    message.markMessageSending(timestamp);
                } else {
                    // Manual resend of message
                    message.markMessageManualResend(timestamp);
                }
            } else {
                // Automatic resend of message
                message.markMessageResending(timestamp);
            }
            if (!updateMessageAndStatus(isSms, message, null /* messageUri */, false /*notify*/)) {
                // If message is missing in the telephony database we don't need to send it
                return false;
            }

            final ArrayList<String> recipients =
                    BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);

            // Update action state with parameters needed for background sending
            actionParameters.putParcelable(KEY_MESSAGE_URI, messageUri);
            actionParameters.putParcelable(KEY_MESSAGE, message);
            actionParameters.putStringArrayList(KEY_RECIPIENTS, recipients);
            actionParameters.putInt(KEY_SUB_ID, self.getSubId());
            actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());

            if (isSms) {
                final String smsc = BugleDatabaseOperations.getSmsServiceCenterForConversation(
                        db, conversationId);
                actionParameters.putString(KEY_SMS_SERVICE_CENTER, smsc);

                if (recipients.size() == 1) {
                    final String recipient = recipients.get(0);

                    actionParameters.putString(KEY_RECIPIENT, recipient);
                    // Queue actual sending for SMS
                    processingAction.requestBackgroundWork(this);

                    if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                        LogUtil.d(TAG, "SendMessageAction: Queued SMS message " + messageId
                                + " for sending");
                    }
                    return true;
                } else {
                    LogUtil.wtf(TAG, "Trying to resend a broadcast SMS - not allowed");
                }
            } else {
                // Queue actual sending for MMS
                processingAction.requestBackgroundWork(this);

                if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                    LogUtil.d(TAG, "SendMessageAction: Queued MMS message " + messageId
                            + " for sending");
                }
                return true;
            }
        }

        return false;
    }


    /**
     * Never called
     */
    @Override
    protected Object executeAction() {
        Assert.fail("SendMessageAction must be queued rather than started");
        return null;
    }

    /**
     * Send message on background worker thread
     */
    @Override
    protected Bundle doBackgroundWork() {
        final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
        final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
        Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
        Uri updatedMessageUri = null;
        final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
        final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
        final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);

        LogUtil.i(TAG, "SendMessageAction: Sending " + (isSms ? "SMS" : "MMS") + " message "
                + messageId + " in conversation " + message.getConversationId());

        int status;
        int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
        int resultCode = MessageData.UNKNOWN_RESULT_CODE;
        if (isSms) {
            Assert.notNull(messageUri);
            final String recipient = actionParameters.getString(KEY_RECIPIENT);
            final String messageText = message.getMessageText();
            final String smsServiceCenter = actionParameters.getString(KEY_SMS_SERVICE_CENTER);
            final boolean deliveryReportRequired = MmsUtils.isDeliveryReportRequired(subId);

            status = MmsUtils.sendSmsMessage(recipient, messageText, messageUri, subId,
                    smsServiceCenter, deliveryReportRequired);
        } else {
            final Context context = Factory.get().getApplicationContext();
            final ArrayList<String> recipients =
                    actionParameters.getStringArrayList(KEY_RECIPIENTS);
            if (messageUri == null) {
                final long timestamp = message.getReceivedTimeStamp();

                // Inform sync that message has been added at local received timestamp
                final SyncManager syncManager = DataModel.get().getSyncManager();
                syncManager.onNewMessageInserted(timestamp);

                // For MMS messages first need to write to telephony (resizing images if needed)
                updatedMessageUri = MmsUtils.insertSendingMmsMessage(context, recipients,
                        message, subId, subPhoneNumber, timestamp);
                if (updatedMessageUri != null) {
                    messageUri = updatedMessageUri;
                    // To prevent Sync seeing inconsistent state must write to DB on this thread
                    updateMessageUri(messageId, updatedMessageUri);

                    if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                        LogUtil.v(TAG, "SendMessageAction: Updated message " + messageId
                                + " with new uri " + messageUri);
                    }
                 }
            }
            if (messageUri != null) {
                // Actually send the MMS
                final Bundle extras = new Bundle();
                extras.putString(EXTRA_MESSAGE_ID, messageId);
                extras.putParcelable(EXTRA_UPDATED_MESSAGE_URI, updatedMessageUri);
                final MmsUtils.StatusPlusUri result = MmsUtils.sendMmsMessage(context, subId,
                        messageUri, extras);
                if (result == MmsUtils.STATUS_PENDING) {
                    // Async send, so no status yet
                    LogUtil.d(TAG, "SendMessageAction: Sending MMS message " + messageId
                            + " asynchronously; waiting for callback to finish processing");
                    return null;
                }
                status = result.status;
                rawStatus = result.rawStatus;
                resultCode = result.resultCode;
            } else {
                status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
            }
        }

        // When we fast-fail before calling the MMS lib APIs (e.g. airplane mode,
        // sending message is deleted).
        ProcessSentMessageAction.processMessageSentFastFailed(messageId, messageUri,
                updatedMessageUri, subId, isSms, status, rawStatus, resultCode);
        return null;
    }

    private void updateMessageUri(final String messageId, final Uri updatedMessageUri) {
        final DatabaseWrapper db = DataModel.get().getDatabase();
        db.beginTransaction();
        try {
            final ContentValues values = new ContentValues();
            values.put(MessageColumns.SMS_MESSAGE_URI, updatedMessageUri.toString());
            BugleDatabaseOperations.updateMessageRow(db, messageId, values);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    @Override
    protected Object processBackgroundResponse(final Bundle response) {
        // Nothing to do here, post-send tasks handled by ProcessSentMessageAction
        return null;
    }

    /**
     * Update message status to reflect success or failure
     */
    @Override
    protected Object processBackgroundFailure() {
        final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
        final MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
        final boolean isSms = message.getProtocol() == MessageData.PROTOCOL_SMS;
        final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
        final int resultCode = actionParameters.getInt(ProcessSentMessageAction.KEY_RESULT_CODE);
        final int httpStatusCode =
                actionParameters.getInt(ProcessSentMessageAction.KEY_HTTP_STATUS_CODE);

        ProcessSentMessageAction.processResult(messageId, null /* updatedMessageUri */,
                MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
                isSms, this, subId, resultCode, httpStatusCode);

        return null;
    }

    /**
     * Update the message status (and message itself if necessary)
     * @param isSms whether this is an SMS or MMS
     * @param message message to update
     * @param updatedMessageUri message uri for newly-inserted messages; null otherwise
     * @param clearSeen whether the message 'seen' status should be reset if error occurs
     */
    public static boolean updateMessageAndStatus(final boolean isSms, final MessageData message,
            final Uri updatedMessageUri, final boolean clearSeen) {
        final Context context = Factory.get().getApplicationContext();
        final DatabaseWrapper db = DataModel.get().getDatabase();

        // TODO: We're optimistically setting the type/box of outgoing messages to
        // 'SENT' even before they actually are. We should technically be using QUEUED or OUTBOX
        // instead, but if we do that, it's possible that the Messaging app will try to send them
        // as part of its clean-up logic that runs when it starts (http://b/18155366).
        //
        // We also use the wrong status when inserting queued SMS messages in
        // InsertNewMessageAction.insertBroadcastSmsMessage and insertSendingSmsMessage (should be
        // QUEUED or OUTBOX), and in MmsUtils.insertSendReq (should be OUTBOX).

        boolean updatedTelephony = true;
        int messageBox;
        int type;
        switch(message.getStatus()) {
            case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
            case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
                type = Sms.MESSAGE_TYPE_SENT;
                messageBox = Mms.MESSAGE_BOX_SENT;
                break;
            case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
            case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
                type = Sms.MESSAGE_TYPE_SENT;
                messageBox = Mms.MESSAGE_BOX_SENT;
                break;
            case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
            case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
                type = Sms.MESSAGE_TYPE_SENT;
                messageBox = Mms.MESSAGE_BOX_SENT;
                break;
            case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
            case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
                type = Sms.MESSAGE_TYPE_FAILED;
                messageBox = Mms.MESSAGE_BOX_FAILED;
                break;
            default:
                type = Sms.MESSAGE_TYPE_ALL;
                messageBox = Mms.MESSAGE_BOX_ALL;
                break;
        }
        // First in the telephony DB
        if (isSms) {
            // Ignore update message Uri
            if (type != Sms.MESSAGE_TYPE_ALL) {
                if (!MmsUtils.updateSmsMessageSendingStatus(context, message.getSmsMessageUri(),
                        type, message.getReceivedTimeStamp())) {
                    message.markMessageFailed(message.getSentTimeStamp());
                    updatedTelephony = false;
                }
            }
        } else if (message.getSmsMessageUri() != null) {
            if (messageBox != Mms.MESSAGE_BOX_ALL) {
                if (!MmsUtils.updateMmsMessageSendingStatus(context, message.getSmsMessageUri(),
                        messageBox, message.getReceivedTimeStamp())) {
                    message.markMessageFailed(message.getSentTimeStamp());
                    updatedTelephony = false;
                }
            }
        }
        if (updatedTelephony) {
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
                        + " message " + message.getMessageId()
                        + " in telephony (" + message.getSmsMessageUri() + ")");
            }
        } else {
            LogUtil.w(TAG, "SendMessageAction: Failed to update " + (isSms ? "SMS" : "MMS")
                    + " message " + message.getMessageId()
                    + " in telephony (" + message.getSmsMessageUri() + "); marking message failed");
        }

        // Update the local DB
        db.beginTransaction();
        try {
            if (updatedMessageUri != null) {
                // Update all message and part fields
                BugleDatabaseOperations.updateMessageInTransaction(db, message);
                BugleDatabaseOperations.refreshConversationMetadataInTransaction(
                        db, message.getConversationId(), false/* shouldAutoSwitchSelfId */,
                        false/*archived*/);
            } else {
                final ContentValues values = new ContentValues();
                values.put(MessageColumns.STATUS, message.getStatus());

                if (clearSeen) {
                    // When a message fails to send, the message needs to
                    // be unseen to be selected as an error notification.
                    values.put(MessageColumns.SEEN, 0);
                }
                values.put(MessageColumns.RECEIVED_TIMESTAMP, message.getReceivedTimeStamp());
                values.put(MessageColumns.RAW_TELEPHONY_STATUS, message.getRawTelephonyStatus());

                BugleDatabaseOperations.updateMessageRowIfExists(db, message.getMessageId(),
                        values);
            }
            db.setTransactionSuccessful();
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "SendMessageAction: Updated " + (isSms ? "SMS" : "MMS")
                        + " message " + message.getMessageId() + " in local db. Timestamp = "
                        + message.getReceivedTimeStamp());
            }
        } finally {
            db.endTransaction();
        }

        MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
        if (updatedMessageUri != null) {
            MessagingContentProvider.notifyPartsChanged();
        }

        return updatedTelephony;
    }

    private SendMessageAction(final Parcel in) {
        super(in);
    }

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

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

    @Override
    public void writeToParcel(final Parcel parcel, final int flags) {
        writeActionToParcel(parcel, flags);
    }
}
