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

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;

import com.android.messaging.BugleApplication;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.widget.BugleWidgetProvider;
import com.android.messaging.widget.WidgetConversationProvider;
import com.google.common.annotations.VisibleForTesting;

import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.PrintWriter;

/**
 * A centralized provider for Uris exposed by Bugle.
 *  */
public class MessagingContentProvider extends ContentProvider {
    private static final String TAG = LogUtil.BUGLE_TAG;

    @VisibleForTesting
    public static final String AUTHORITY =
            "com.android.messaging.datamodel.MessagingContentProvider";
    private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';

    // Conversations query
    private static final String CONVERSATIONS_QUERY = "conversations";

    public static final Uri CONVERSATIONS_URI = Uri.parse(CONTENT_AUTHORITY + CONVERSATIONS_QUERY);
    static final Uri PARTS_URI = Uri.parse(CONTENT_AUTHORITY + DatabaseHelper.PARTS_TABLE);

    // Messages query
    private static final String MESSAGES_QUERY = "messages";

    static final Uri MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + MESSAGES_QUERY);

    public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY +
            MESSAGES_QUERY + "/conversation");

    // Conversation participants query
    private static final String PARTICIPANTS_QUERY = "participants";

    static class ConversationParticipantsQueryColumns extends ParticipantColumns {
        static final String CONVERSATION_ID = ConversationParticipantsColumns.CONVERSATION_ID;
    }

    static final Uri CONVERSATION_PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY +
            PARTICIPANTS_QUERY + "/conversation");

    public static final Uri PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + PARTICIPANTS_QUERY);

    // Conversation images query
    private static final String CONVERSATION_IMAGES_QUERY = "conversation_images";

    public static final Uri CONVERSATION_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
            CONVERSATION_IMAGES_QUERY);

    private static final String DRAFT_IMAGES_QUERY = "draft_images";

    public static final Uri DRAFT_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
            DRAFT_IMAGES_QUERY);

    /**
     * Notifies that <i>all</i> data exposed by the provider needs to be refreshed.
     * <p>
     * <b>IMPORTANT!</b> You probably shouldn't be calling this. Prefer to notify more specific
     * uri's instead. Currently only sync uses this, because sync can potentially update many
     * different tables at once.
     */
    public static void notifyEverythingChanged() {
        final Uri uri = Uri.parse(CONTENT_AUTHORITY);
        final Context context = Factory.get().getApplicationContext();
        final ContentResolver cr = context.getContentResolver();
        cr.notifyChange(uri, null);

        // Notify any conversations widgets the conversation list has changed.
        BugleWidgetProvider.notifyConversationListChanged(context);

        // Notify all conversation widgets to update.
        WidgetConversationProvider.notifyMessagesChanged(context, null /*conversationId*/);
    }

    /**
     * Build a participant uri from the conversation id.
     */
    public static Uri buildConversationParticipantsUri(final String conversationId) {
        final Uri.Builder builder = CONVERSATION_PARTICIPANTS_URI.buildUpon();
        builder.appendPath(conversationId);
        return builder.build();
    }

    public static void notifyParticipantsChanged(final String conversationId) {
        final Uri uri = buildConversationParticipantsUri(conversationId);
        final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
        cr.notifyChange(uri, null);
    }

    public static void notifyAllMessagesChanged() {
        final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
        cr.notifyChange(CONVERSATION_MESSAGES_URI, null);
    }

    public static void notifyAllParticipantsChanged() {
        final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
        cr.notifyChange(CONVERSATION_PARTICIPANTS_URI, null);
    }

    // Default value for unknown dimension of image
    public static final int UNSPECIFIED_SIZE = -1;

    // Internal
    private static final int CONVERSATIONS_QUERY_CODE = 10;

    private static final int CONVERSATION_QUERY_CODE = 20;
    private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30;
    private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40;
    private static final int CONVERSATION_IMAGES_QUERY_CODE = 50;
    private static final int DRAFT_IMAGES_QUERY_CODE = 60;
    private static final int PARTICIPANTS_QUERY_CODE = 70;

    // TODO: Move to a better structured URI namespace.
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE);
        sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
        sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
                CONVERSATION_MESSAGES_QUERY_CODE);
        sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
                CONVERSATION_PARTICIPANTS_QUERY_CODE);
        sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
        sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*",
                CONVERSATION_IMAGES_QUERY_CODE);
        sURIMatcher.addURI(AUTHORITY, DRAFT_IMAGES_QUERY + "/*",
                DRAFT_IMAGES_QUERY_CODE);
    }

    /**
     * Build a messages uri from the conversation id.
     */
    public static Uri buildConversationMessagesUri(final String conversationId) {
        final Uri.Builder builder = CONVERSATION_MESSAGES_URI.buildUpon();
        builder.appendPath(conversationId);
        return builder.build();
    }

    public static void notifyMessagesChanged(final String conversationId) {
        final Uri uri = buildConversationMessagesUri(conversationId);
        final Context context = Factory.get().getApplicationContext();
        final ContentResolver cr = context.getContentResolver();
        cr.notifyChange(uri, null);
        notifyConversationListChanged();

        // Notify the widget the messages changed
        WidgetConversationProvider.notifyMessagesChanged(context, conversationId);
    }

    /**
     * Build a conversation metadata uri from a conversation id.
     */
    public static Uri buildConversationMetadataUri(final String conversationId) {
        final Uri.Builder builder = CONVERSATIONS_URI.buildUpon();
        builder.appendPath(conversationId);
        return builder.build();
    }

    public static void notifyConversationMetadataChanged(final String conversationId) {
        final Uri uri = buildConversationMetadataUri(conversationId);
        final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
        cr.notifyChange(uri, null);
        notifyConversationListChanged();
    }

    public static void notifyPartsChanged() {
        final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
        cr.notifyChange(PARTS_URI, null);
    }

    public static void notifyConversationListChanged() {
        final Context context = Factory.get().getApplicationContext();
        final ContentResolver cr = context.getContentResolver();
        cr.notifyChange(CONVERSATIONS_URI, null);

        // Notify the widget the conversation list changed
        BugleWidgetProvider.notifyConversationListChanged(context);
    }

    /**
     * Build a conversation images uri from a conversation id.
     */
    public static Uri buildConversationImagesUri(final String conversationId) {
        final Uri.Builder builder = CONVERSATION_IMAGES_URI.buildUpon();
        builder.appendPath(conversationId);
        return builder.build();
    }

    /**
     * Build a draft images uri from a conversation id.
     */
    public static Uri buildDraftImagesUri(final String conversationId) {
        final Uri.Builder builder = DRAFT_IMAGES_URI.buildUpon();
        builder.appendPath(conversationId);
        return builder.build();
    }

    private DatabaseHelper mDatabaseHelper;
    private DatabaseWrapper mDatabaseWrapper;

    public MessagingContentProvider() {
        super();
    }

    @VisibleForTesting
    public void setDatabaseForTest(final DatabaseWrapper db) {
        Assert.isTrue(BugleApplication.isRunningTests());
        mDatabaseWrapper = db;
    }

    private DatabaseWrapper getDatabaseWrapper() {
        if (mDatabaseWrapper == null) {
            mDatabaseWrapper = mDatabaseHelper.getDatabase();
        }
        return mDatabaseWrapper;
    }

    @Override
    public Cursor query(final Uri uri, final String[] projection, String selection,
            final String[] selectionArgs, String sortOrder) {

        // Processes other than self are allowed to temporarily access the media
        // scratch space; we grant uri read access on a case-by-case basis. Dialer app and
        // contacts app would doQuery() on the vCard uri before trying to open the inputStream.
        // There's nothing that we need to return for this uri so just No-Op.
        //if (isMediaScratchSpaceUri(uri)) {
        //    return null;
        //}

        final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();

        String[] queryArgs = selectionArgs;
        final int match = sURIMatcher.match(uri);
        String groupBy = null;
        String limit = null;
        switch (match) {
            case CONVERSATIONS_QUERY_CODE:
                queryBuilder.setTables(ConversationListItemData.getConversationListView());
                // Hide empty conversations (ones with 0 sort_timestamp)
                queryBuilder.appendWhere(ConversationColumns.SORT_TIMESTAMP + " > 0 ");
                break;
            case CONVERSATION_QUERY_CODE:
                queryBuilder.setTables(ConversationListItemData.getConversationListView());
                if (uri.getPathSegments().size() == 2) {
                    queryBuilder.appendWhere(ConversationColumns._ID + "=?");
                    // Get the conversation id from the uri
                    queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
                } else {
                    throw new IllegalArgumentException("Malformed URI " + uri);
                }
                break;
            case CONVERSATION_PARTICIPANTS_QUERY_CODE:
                queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
                if (uri.getPathSegments().size() == 3 &&
                        TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
                    queryBuilder.appendWhere(ParticipantColumns._ID + " IN ( " + "SELECT "
                            + ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
                            + ParticipantColumns._ID
                            + " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
                            + " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID
                            + " =? UNION SELECT " + ParticipantColumns._ID + " FROM "
                            + DatabaseHelper.PARTICIPANTS_TABLE + " WHERE "
                            + ParticipantColumns.SUB_ID + " != "
                            + ParticipantData.OTHER_THAN_SELF_SUB_ID + " )");
                    // Get the conversation id from the uri
                    queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(2));
                } else {
                    throw new IllegalArgumentException("Malformed URI " + uri);
                }
                break;
            case PARTICIPANTS_QUERY_CODE:
                queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
                if (uri.getPathSegments().size() != 1) {
                    throw new IllegalArgumentException("Malformed URI " + uri);
                }
                break;
            case CONVERSATION_MESSAGES_QUERY_CODE:
                if (uri.getPathSegments().size() == 3 &&
                    TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
                    // Get the conversation id from the uri
                    final String conversationId = uri.getPathSegments().get(2);

                    // We need to handle this query differently, instead of falling through to the
                    // generic query call at the bottom. For performance reasons, the conversation
                    // messages query is executed as a raw query. It is invalid to specify
                    // selection/sorting for this query.

                    if (selection == null && selectionArgs == null && sortOrder == null) {
                        return queryConversationMessages(conversationId, uri);
                    } else {
                        throw new IllegalArgumentException(
                                "Cannot set selection or sort order with this query");
                    }
                } else {
                    throw new IllegalArgumentException("Malformed URI " + uri);
                }
            case CONVERSATION_IMAGES_QUERY_CODE:
                queryBuilder.setTables(ConversationImagePartsView.getViewName());
                if (uri.getPathSegments().size() == 2) {
                    // Exclude draft.
                    queryBuilder.appendWhere(
                            ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
                                    ConversationImagePartsView.Columns.STATUS + "<>" +
                                    MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
                    // Get the conversation id from the uri
                    queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
                } else {
                    throw new IllegalArgumentException("Malformed URI " + uri);
                }
                break;
            case DRAFT_IMAGES_QUERY_CODE:
                queryBuilder.setTables(ConversationImagePartsView.getViewName());
                if (uri.getPathSegments().size() == 2) {
                    // Draft only.
                    queryBuilder.appendWhere(
                            ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
                                    ConversationImagePartsView.Columns.STATUS + "=" +
                                    MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
                    // Get the conversation id from the uri
                    queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
                } else {
                    throw new IllegalArgumentException("Malformed URI " + uri);
                }
                break;
            default: {
                throw new IllegalArgumentException("Unknown URI " + uri);
            }
        }

        final Cursor cursor = getDatabaseWrapper().query(queryBuilder, projection, selection,
                queryArgs, groupBy, null, sortOrder, limit);
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    private Cursor queryConversationMessages(final String conversationId, final Uri notifyUri) {
        final String[] queryArgs = { conversationId };
        final Cursor cursor = getDatabaseWrapper().rawQuery(
                ConversationMessageData.getConversationMessagesQuerySql(), queryArgs);
        cursor.setNotificationUri(getContext().getContentResolver(), notifyUri);
        return cursor;
    }

    @Override
    public String getType(final Uri uri) {
        final StringBuilder sb = new
                StringBuilder("vnd.android.cursor.dir/vnd.android.messaging.");

        switch (sURIMatcher.match(uri)) {
            case CONVERSATIONS_QUERY_CODE: {
                sb.append(CONVERSATIONS_QUERY);
                break;
            }
            default: {
                throw new IllegalArgumentException("Unknown URI: " + uri);
            }
        }
        return sb.toString();
    }

    protected DatabaseHelper getDatabase() {
        return DatabaseHelper.getInstance(getContext());
    }

    @Override
    public ParcelFileDescriptor openFile(final Uri uri, final String fileMode)
            throws FileNotFoundException {
        throw new IllegalArgumentException("openFile not supported: " + uri);
    }

    @Override
    public Uri insert(final Uri uri, final ContentValues values) {
        throw new IllegalStateException("Insert not supported " + uri);
    }

    @Override
    public int delete(final Uri uri, final String selection, final String[] selectionArgs) {
        throw new IllegalArgumentException("Delete not supported: " + uri);
    }

    @Override
    public int update(final Uri uri, final ContentValues values, final String selection,
            final String[] selectionArgs) {
        throw new IllegalArgumentException("Update not supported: " + uri);
    }

    /**
     * Prepends new arguments to the existing argument list.
     *
     * @param oldArgList The current list of arguments. May be {@code null}
     * @param args The new arguments to prepend
     * @return A new argument list with the given arguments prepended
     */
    private String[] prependArgs(final String[] oldArgList, final String... args) {
        if (args == null || args.length == 0) {
            return oldArgList;
        }
        final int oldArgCount = (oldArgList == null ? 0 : oldArgList.length);
        final int newArgCount = args.length;

        final String[] newArgs = new String[oldArgCount + newArgCount];
        System.arraycopy(args, 0, newArgs, 0, newArgCount);
        if (oldArgCount > 0) {
            System.arraycopy(oldArgList, 0, newArgs, newArgCount, oldArgCount);
        }
        return newArgs;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) {
        // First dump out the default SMS app package name
        String defaultSmsApp = PhoneUtils.getDefault().getDefaultSmsApp();
        if (TextUtils.isEmpty(defaultSmsApp)) {
            if (OsUtil.isAtLeastKLP()) {
                defaultSmsApp = "None";
            } else {
                defaultSmsApp = "None (pre-Kitkat)";
            }
        }
        writer.println("Default SMS app: " + defaultSmsApp);
        // Now dump logs
        LogUtil.dump(writer);
    }

    @Override
    public boolean onCreate() {
        // This is going to wind up calling into createDatabase() below.
        mDatabaseHelper = (DatabaseHelper) getDatabase();
        // We cannot initialize mDatabaseWrapper yet as the Factory may not be initialized
        return true;
    }
}
