/*
 * Copyright (c) 2008-2009, Motorola, Inc.
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Motorola, Inc. nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.android.bluetooth.opp;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.SQLException;
import android.content.UriMatcher;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.LiveFolders;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * This provider allows application to interact with Bluetooth OPP manager
 */

public final class BluetoothOppProvider extends ContentProvider {

    private static final String TAG = "BluetoothOppProvider";
    private static final boolean D = Constants.DEBUG;
    private static final boolean V = Constants.VERBOSE;

    /** Database filename */
    private static final String DB_NAME = "btopp.db";

    /** Current database version */
    private static final int DB_VERSION = 1;

    /** Database version from which upgrading is a nop */
    private static final int DB_VERSION_NOP_UPGRADE_FROM = 0;

    /** Database version to which upgrading is a nop */
    private static final int DB_VERSION_NOP_UPGRADE_TO = 1;

    /** Name of table in the database */
    private static final String DB_TABLE = "btopp";

    /** MIME type for the entire share list */
    private static final String SHARE_LIST_TYPE = "vnd.android.cursor.dir/vnd.android.btopp";

    /** MIME type for an individual share */
    private static final String SHARE_TYPE = "vnd.android.cursor.item/vnd.android.btopp";

    /** URI matcher used to recognize URIs sent by applications */
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    /** URI matcher constant for the URI of the entire share list */
    private static final int SHARES = 1;

    /** URI matcher constant for the URI of an individual share */
    private static final int SHARES_ID = 2;

    /** URI matcher constant for the URI of live folder */
    private static final int LIVE_FOLDER_RECEIVED_FILES = 3;
    static {
        sURIMatcher.addURI("com.android.bluetooth.opp", "btopp", SHARES);
        sURIMatcher.addURI("com.android.bluetooth.opp", "btopp/#", SHARES_ID);
        sURIMatcher.addURI("com.android.bluetooth.opp", "live_folders/received",
                LIVE_FOLDER_RECEIVED_FILES);
    }

    private static final HashMap<String, String> LIVE_FOLDER_PROJECTION_MAP;
    static {
        LIVE_FOLDER_PROJECTION_MAP = new HashMap<String, String>();
        LIVE_FOLDER_PROJECTION_MAP.put(LiveFolders._ID, BluetoothShare._ID + " AS "
                + LiveFolders._ID);
        LIVE_FOLDER_PROJECTION_MAP.put(LiveFolders.NAME, BluetoothShare.FILENAME_HINT + " AS "
                + LiveFolders.NAME);
    }

    /** The database that lies underneath this content provider */
    private SQLiteOpenHelper mOpenHelper = null;

    /**
     * Creates and updated database on demand when opening it. Helper class to
     * create database the first time the provider is initialized and upgrade it
     * when a new version of the provider needs an updated version of the
     * database.
     */
    private final class DatabaseHelper extends SQLiteOpenHelper {

        public DatabaseHelper(final Context context) {
            super(context, DB_NAME, null, DB_VERSION);
        }

        /**
         * Creates database the first time we try to open it.
         */
        @Override
        public void onCreate(final SQLiteDatabase db) {
            if (V) Log.v(TAG, "populating new database");
            createTable(db);
        }

        //TODO: use this function to check garbage transfer left in db, for example,
        // a crash incoming file
        /*
         * (not a javadoc comment) Checks data integrity when opening the
         * database.
         */
        /*
         * @Override public void onOpen(final SQLiteDatabase db) {
         * super.onOpen(db); }
         */

        /**
         * Updates the database format when a content provider is used with a
         * database that was created with a different format.
         */
        // Note: technically, this could also be a downgrade, so if we want
        // to gracefully handle upgrades we should be careful about
        // what to do on downgrades.
        @Override
        public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
            if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
                if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op
                    // upgrade.
                    return;
                }
                // NOP_FROM and NOP_TO are identical, just in different
                // codelines. Upgrading
                // from NOP_FROM is the same as upgrading from NOP_TO.
                oldV = DB_VERSION_NOP_UPGRADE_TO;
            }
            Log.i(TAG, "Upgrading downloads database from version " + oldV + " to "
                    + newV + ", which will destroy all old data");
            dropTable(db);
            createTable(db);
        }

    }

    private void createTable(SQLiteDatabase db) {
        try {
            db.execSQL("CREATE TABLE " + DB_TABLE + "(" + BluetoothShare._ID
                    + " INTEGER PRIMARY KEY AUTOINCREMENT," + BluetoothShare.URI + " TEXT, "
                    + BluetoothShare.FILENAME_HINT + " TEXT, " + BluetoothShare._DATA + " TEXT, "
                    + BluetoothShare.MIMETYPE + " TEXT, " + BluetoothShare.DIRECTION + " INTEGER, "
                    + BluetoothShare.DESTINATION + " TEXT, " + BluetoothShare.VISIBILITY
                    + " INTEGER, " + BluetoothShare.USER_CONFIRMATION + " INTEGER, "
                    + BluetoothShare.STATUS + " INTEGER, " + BluetoothShare.TOTAL_BYTES
                    + " INTEGER, " + BluetoothShare.CURRENT_BYTES + " INTEGER, "
                    + BluetoothShare.TIMESTAMP + " INTEGER," + Constants.MEDIA_SCANNED
                    + " INTEGER); ");
        } catch (SQLException ex) {
            Log.e(TAG, "couldn't create table in downloads database");
            throw ex;
        }
    }

    private void dropTable(SQLiteDatabase db) {
        try {
            db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
        } catch (SQLException ex) {
            Log.e(TAG, "couldn't drop table in downloads database");
            throw ex;
        }
    }

    @Override
    public String getType(Uri uri) {
        int match = sURIMatcher.match(uri);
        switch (match) {
            case SHARES: {
                return SHARE_LIST_TYPE;
            }
            case SHARES_ID: {
                return SHARE_TYPE;
            }
            default: {
                if (D) Log.d(TAG, "calling getType on an unknown URI: " + uri);
                throw new IllegalArgumentException("Unknown URI: " + uri);
            }
        }
    }

    private static final void copyString(String key, ContentValues from, ContentValues to) {
        String s = from.getAsString(key);
        if (s != null) {
            to.put(key, s);
        }
    }

    private static final void copyInteger(String key, ContentValues from, ContentValues to) {
        Integer i = from.getAsInteger(key);
        if (i != null) {
            to.put(key, i);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();

        if (sURIMatcher.match(uri) != SHARES) {
            if (D) Log.d(TAG, "calling insert on an unknown/invalid URI: " + uri);
            throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
        }

        ContentValues filteredValues = new ContentValues();

        copyString(BluetoothShare.URI, values, filteredValues);
        copyString(BluetoothShare.FILENAME_HINT, values, filteredValues);
        copyString(BluetoothShare.MIMETYPE, values, filteredValues);
        copyString(BluetoothShare.DESTINATION, values, filteredValues);

        copyInteger(BluetoothShare.VISIBILITY, values, filteredValues);
        copyInteger(BluetoothShare.TOTAL_BYTES, values, filteredValues);

        if (values.getAsInteger(BluetoothShare.VISIBILITY) == null) {
            filteredValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_VISIBLE);
        }
        Integer dir = values.getAsInteger(BluetoothShare.DIRECTION);
        Integer con = values.getAsInteger(BluetoothShare.USER_CONFIRMATION);
        String address = values.getAsString(BluetoothShare.DESTINATION);

        if (values.getAsInteger(BluetoothShare.DIRECTION) == null) {
            dir = BluetoothShare.DIRECTION_OUTBOUND;
        }
        if (dir == BluetoothShare.DIRECTION_OUTBOUND && con == null) {
            con = BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED;
        }
        if (dir == BluetoothShare.DIRECTION_INBOUND && con == null) {
            con = BluetoothShare.USER_CONFIRMATION_PENDING;
        }
        filteredValues.put(BluetoothShare.USER_CONFIRMATION, con);
        filteredValues.put(BluetoothShare.DIRECTION, dir);

        filteredValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_PENDING);
        filteredValues.put(Constants.MEDIA_SCANNED, 0);

        Long ts = values.getAsLong(BluetoothShare.TIMESTAMP);
        if (ts == null) {
            ts = System.currentTimeMillis();
        }
        filteredValues.put(BluetoothShare.TIMESTAMP, ts);

        Context context = getContext();
        context.startService(new Intent(context, BluetoothOppService.class));

        long rowID = db.insert(DB_TABLE, null, filteredValues);

        Uri ret = null;

        if (rowID != -1) {
            context.startService(new Intent(context, BluetoothOppService.class));
            ret = Uri.parse(BluetoothShare.CONTENT_URI + "/" + rowID);
            context.getContentResolver().notifyChange(uri, null);
        } else {
            if (D) Log.d(TAG, "couldn't insert into btopp database");
            }

        return ret;
    }

    @Override
    public boolean onCreate() {
        mOpenHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();

        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

        int match = sURIMatcher.match(uri);
        switch (match) {
            case SHARES: {
                qb.setTables(DB_TABLE);
                break;
            }
            case SHARES_ID: {
                qb.setTables(DB_TABLE);
                qb.appendWhere(BluetoothShare._ID + "=");
                qb.appendWhere(uri.getPathSegments().get(1));
                break;
            }
            case LIVE_FOLDER_RECEIVED_FILES: {
                qb.setTables(DB_TABLE);
                qb.setProjectionMap(LIVE_FOLDER_PROJECTION_MAP);
                qb.appendWhere(BluetoothShare.DIRECTION + "=" + BluetoothShare.DIRECTION_INBOUND
                        + " AND " + BluetoothShare.STATUS + "=" + BluetoothShare.STATUS_SUCCESS);
                sortOrder = "_id DESC, " + sortOrder;
                break;
            }
            default: {
                if (D) Log.d(TAG, "querying unknown URI: " + uri);
                throw new IllegalArgumentException("Unknown URI: " + uri);
            }
        }

        if (V) {
            java.lang.StringBuilder sb = new java.lang.StringBuilder();
            sb.append("starting query, database is ");
            if (db != null) {
                sb.append("not ");
            }
            sb.append("null; ");
            if (projection == null) {
                sb.append("projection is null; ");
            } else if (projection.length == 0) {
                sb.append("projection is empty; ");
            } else {
                for (int i = 0; i < projection.length; ++i) {
                    sb.append("projection[");
                    sb.append(i);
                    sb.append("] is ");
                    sb.append(projection[i]);
                    sb.append("; ");
                }
            }
            sb.append("selection is ");
            sb.append(selection);
            sb.append("; ");
            if (selectionArgs == null) {
                sb.append("selectionArgs is null; ");
            } else if (selectionArgs.length == 0) {
                sb.append("selectionArgs is empty; ");
            } else {
                for (int i = 0; i < selectionArgs.length; ++i) {
                    sb.append("selectionArgs[");
                    sb.append(i);
                    sb.append("] is ");
                    sb.append(selectionArgs[i]);
                    sb.append("; ");
                }
            }
            sb.append("sort is ");
            sb.append(sortOrder);
            sb.append(".");
            Log.v(TAG, sb.toString());
        }

        Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);

        if (ret != null) {
            ret.setNotificationUri(getContext().getContentResolver(), uri);
            if (V) Log.v(TAG, "created cursor " + ret + " on behalf of ");// +
        } else {
            if (D) Log.d(TAG, "query failed in downloads database");
            }

        return ret;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();

        int count;
        long rowId = 0;

        int match = sURIMatcher.match(uri);
        switch (match) {
            case SHARES:
            case SHARES_ID: {
                String myWhere;
                if (selection != null) {
                    if (match == SHARES) {
                        myWhere = "( " + selection + " )";
                    } else {
                        myWhere = "( " + selection + " ) AND ";
                    }
                } else {
                    myWhere = "";
                }
                if (match == SHARES_ID) {
                    String segment = uri.getPathSegments().get(1);
                    rowId = Long.parseLong(segment);
                    myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) ";
                }

                if (values.size() > 0) {
                    count = db.update(DB_TABLE, values, myWhere, selectionArgs);
                } else {
                    count = 0;
                }
                break;
            }
            default: {
                if (D) Log.d(TAG, "updating unknown/invalid URI: " + uri);
                throw new UnsupportedOperationException("Cannot update URI: " + uri);
            }
        }
        getContext().getContentResolver().notifyChange(uri, null);

        return count;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count;
        int match = sURIMatcher.match(uri);
        switch (match) {
            case SHARES:
            case SHARES_ID: {
                String myWhere;
                if (selection != null) {
                    if (match == SHARES) {
                        myWhere = "( " + selection + " )";
                    } else {
                        myWhere = "( " + selection + " ) AND ";
                    }
                } else {
                    myWhere = "";
                }
                if (match == SHARES_ID) {
                    String segment = uri.getPathSegments().get(1);
                    long rowId = Long.parseLong(segment);
                    myWhere += " ( " + BluetoothShare._ID + " = " + rowId + " ) ";
                }

                count = db.delete(DB_TABLE, myWhere, selectionArgs);
                break;
            }
            default: {
                if (D) Log.d(TAG, "deleting unknown/invalid URI: " + uri);
                throw new UnsupportedOperationException("Cannot delete URI: " + uri);
            }
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }
}
