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

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;
import android.text.TextUtils;

import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.receiver.SendStatusReceiver;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.UiUtils;

import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Class that sends chat message via SMS.
 *
 * The interface emulates a blocking sending similar to making an HTTP request.
 * It calls the SmsManager to send a (potentially multipart) message and waits
 * on the sent status on each part. The waiting has a timeout so it won't wait
 * forever. Once the sent status of all parts received, the call returns.
 * A successful sending requires success status for all parts. Otherwise, we
 * pick the highest level of failure as the error for the whole message, which
 * is used to determine if we need to retry the sending.
 */
public class SmsSender {
    private static final String TAG = LogUtil.BUGLE_TAG;

    public static final String EXTRA_PART_ID = "part_id";

    /*
     * A map for pending sms messages. The key is the random request UUID.
     */
    private static ConcurrentHashMap<Uri, SendResult> sPendingMessageMap =
            new ConcurrentHashMap<Uri, SendResult>();

    private static final Random RANDOM = new Random();

    /**
     * Class that holds the sent status for all parts of a multipart message sending
     */
    public static class SendResult {
        // Failure levels, used by the caller of the sender.
        // For temporary failures, possibly we could retry the sending
        // For permanent failures, we probably won't retry
        public static final int FAILURE_LEVEL_NONE = 0;
        public static final int FAILURE_LEVEL_TEMPORARY = 1;
        public static final int FAILURE_LEVEL_PERMANENT = 2;

        // Tracking the remaining pending parts in sending
        private int mPendingParts;
        // Tracking the highest level of failure among all parts
        private int mHighestFailureLevel;

        public SendResult(final int numOfParts) {
            Assert.isTrue(numOfParts > 0);
            mPendingParts = numOfParts;
            mHighestFailureLevel = FAILURE_LEVEL_NONE;
        }

        // Update the sent status of one part
        public void setPartResult(final int resultCode) {
            mPendingParts--;
            setHighestFailureLevel(resultCode);
        }

        public boolean hasPending() {
            return mPendingParts > 0;
        }

        public int getHighestFailureLevel() {
            return mHighestFailureLevel;
        }

        private int getFailureLevel(final int resultCode) {
            switch (resultCode) {
                case Activity.RESULT_OK:
                    return FAILURE_LEVEL_NONE;
                case SmsManager.RESULT_ERROR_NO_SERVICE:
                    return FAILURE_LEVEL_TEMPORARY;
                case SmsManager.RESULT_ERROR_RADIO_OFF:
                    return FAILURE_LEVEL_PERMANENT;
                case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
                    return FAILURE_LEVEL_PERMANENT;
                default: {
                    LogUtil.e(TAG, "SmsSender: Unexpected sent intent resultCode = " + resultCode);
                    return FAILURE_LEVEL_PERMANENT;
                }
            }
        }

        private void setHighestFailureLevel(final int resultCode) {
            final int level = getFailureLevel(resultCode);
            if (level > mHighestFailureLevel) {
                mHighestFailureLevel = level;
            }
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("SendResult:");
            sb.append("Pending=").append(mPendingParts).append(",");
            sb.append("HighestFailureLevel=").append(mHighestFailureLevel);
            return sb.toString();
        }
    }

    public static void setResult(final Uri requestId, final int resultCode,
            final int errorCode, final int partId, int subId) {
        if (resultCode != Activity.RESULT_OK) {
            LogUtil.e(TAG, "SmsSender: failure in sending message part. "
                    + " requestId=" + requestId + " partId=" + partId
                    + " resultCode=" + resultCode + " errorCode=" + errorCode);
            if (errorCode != SendStatusReceiver.NO_ERROR_CODE) {
                final Context context = Factory.get().getApplicationContext();
                UiUtils.showToastAtBottom(getSendErrorToastMessage(context, subId, errorCode));
            }
        } else {
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "SmsSender: received sent result. " + " requestId=" + requestId
                        + " partId=" + partId + " resultCode=" + resultCode);
            }
        }
        if (requestId != null) {
            final SendResult result = sPendingMessageMap.get(requestId);
            if (result != null) {
                synchronized (result) {
                    result.setPartResult(resultCode);
                    if (!result.hasPending()) {
                        result.notifyAll();
                    }
                }
            } else {
                LogUtil.e(TAG, "SmsSender: ignoring sent result. " + " requestId=" + requestId
                        + " partId=" + partId + " resultCode=" + resultCode);
            }
        }
    }

    private static String getSendErrorToastMessage(final Context context, final int subId,
            final int errorCode) {
        final String carrierName = PhoneUtils.get(subId).getCarrierName();
        if (TextUtils.isEmpty(carrierName)) {
            return context.getString(R.string.carrier_send_error_unknown_carrier, errorCode);
        } else {
            return context.getString(R.string.carrier_send_error, carrierName, errorCode);
        }
    }

    // This should be called from a RequestWriter queue thread
    public static SendResult sendMessage(final Context context, final int subId, String dest,
            String message, final String serviceCenter, final boolean requireDeliveryReport,
            final Uri messageUri) throws SmsException {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "SmsSender: sending message. " +
                    "dest=" + dest + " message=" + message +
                    " serviceCenter=" + serviceCenter +
                    " requireDeliveryReport=" + requireDeliveryReport +
                    " requestId=" + messageUri);
        }
        if (TextUtils.isEmpty(message)) {
            throw new SmsException("SmsSender: empty text message");
        }
        // Get the real dest and message for email or alias if dest is email or alias
        // Or sanitize the dest if dest is a number
        if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway()) &&
                (MmsSmsUtils.isEmailAddress(dest) || MmsSmsUtils.isAlias(dest, subId))) {
            // The original destination (email address) goes with the message
            message = dest + " " + message;
            // the new address is the email gateway #
            dest = MmsConfig.get(subId).getEmailGateway();
        } else {
            // remove spaces and dashes from destination number
            // (e.g. "801 555 1212" -> "8015551212")
            // (e.g. "+8211-123-4567" -> "+82111234567")
            dest = PhoneNumberUtils.stripSeparators(dest);
        }
        if (TextUtils.isEmpty(dest)) {
            throw new SmsException("SmsSender: empty destination address");
        }
        // Divide the input message by SMS length limit
        final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
        final ArrayList<String> messages = smsManager.divideMessage(message);
        if (messages == null || messages.size() < 1) {
            throw new SmsException("SmsSender: fails to divide message");
        }
        // Prepare the send result, which collects the send status for each part
        final SendResult pendingResult = new SendResult(messages.size());
        sPendingMessageMap.put(messageUri, pendingResult);
        // Actually send the sms
        sendInternal(
                context, subId, dest, messages, serviceCenter, requireDeliveryReport, messageUri);
        // Wait for pending intent to come back
        synchronized (pendingResult) {
            final long smsSendTimeoutInMillis = BugleGservices.get().getLong(
                    BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS,
                    BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS_DEFAULT);
            final long beginTime = SystemClock.elapsedRealtime();
            long waitTime = smsSendTimeoutInMillis;
            // We could possibly be woken up while still pending
            // so make sure we wait the full timeout period unless
            // we have the send results of all parts.
            while (pendingResult.hasPending() && waitTime > 0) {
                try {
                    pendingResult.wait(waitTime);
                } catch (final InterruptedException e) {
                    LogUtil.e(TAG, "SmsSender: sending wait interrupted");
                }
                waitTime = smsSendTimeoutInMillis - (SystemClock.elapsedRealtime() - beginTime);
            }
        }
        // Either we timed out or have all the results (success or failure)
        sPendingMessageMap.remove(messageUri);
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "SmsSender: sending completed. " +
                    "dest=" + dest + " message=" + message + " result=" + pendingResult);
        }
        return pendingResult;
    }

    // Actually sending the message using SmsManager
    private static void sendInternal(final Context context, final int subId, String dest,
            final ArrayList<String> messages, final String serviceCenter,
            final boolean requireDeliveryReport, final Uri messageUri) throws SmsException {
        Assert.notNull(context);
        final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
        final int messageCount = messages.size();
        final ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(messageCount);
        final ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(messageCount);
        for (int i = 0; i < messageCount; i++) {
            // Make pending intents different for each message part
            final int partId = (messageCount <= 1 ? 0 : i + 1);
            if (requireDeliveryReport && (i == (messageCount - 1))) {
                // TODO we only care about the delivery status of the last part
                // Shall we have better tracking of delivery status of all parts?
                deliveryIntents.add(PendingIntent.getBroadcast(
                        context,
                        partId,
                        getSendStatusIntent(context, SendStatusReceiver.MESSAGE_DELIVERED_ACTION,
                                messageUri, partId, subId),
                        0/*flag*/));
            } else {
                deliveryIntents.add(null);
            }
            sentIntents.add(PendingIntent.getBroadcast(
                    context,
                    partId,
                    getSendStatusIntent(context, SendStatusReceiver.MESSAGE_SENT_ACTION,
                            messageUri, partId, subId),
                    0/*flag*/));
        }
        try {
            if (MmsConfig.get(subId).getSendMultipartSmsAsSeparateMessages()) {
                // If multipart sms is not supported, send them as separate messages
                for (int i = 0; i < messageCount; i++) {
                    smsManager.sendTextMessage(dest,
                            serviceCenter,
                            messages.get(i),
                            sentIntents.get(i),
                            deliveryIntents.get(i));
                }
            } else {
                smsManager.sendMultipartTextMessage(
                        dest, serviceCenter, messages, sentIntents, deliveryIntents);
            }
        } catch (final Exception e) {
            throw new SmsException("SmsSender: caught exception in sending " + e);
        }
    }

    private static Intent getSendStatusIntent(final Context context, final String action,
            final Uri requestUri, final int partId, final int subId) {
        // Encode requestId in intent data
        final Intent intent = new Intent(action, requestUri, context, SendStatusReceiver.class);
        intent.putExtra(SendStatusReceiver.EXTRA_PART_ID, partId);
        intent.putExtra(SendStatusReceiver.EXTRA_SUB_ID, subId);
        return intent;
    }
}
