/**
 * Copyright (C) 2013 The CyanogenMod Project
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.whispersystems.whisperpush.database;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;

import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
import org.whispersystems.textsecure.crypto.MasterCipher;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
import org.whispersystems.textsecure.util.Base64;

import java.io.IOException;

public class IdentityDatabase {

    private static final Uri CHANGE_URI = Uri.parse("content://whisperpush/identities");

    private static final String TABLE_NAME   = "identities";
    private static final String ID           = "_id";
    public  static final String ADDRESS      = "address";
    public  static final String IDENTITY_KEY = "key";
    public  static final String MAC          = "mac";

    private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
            " (" + ID + " INTEGER PRIMARY KEY, " +
            ADDRESS + " INTEGER, " +
            IDENTITY_KEY + " TEXT, " +
            MAC + " TEXT);";

    private static final String CREATE_INDEX[]  = {"CREATE INDEX IF NOT EXISTS address_index ON " +
            TABLE_NAME + " (" + ADDRESS + ");",
            "CREATE UNIQUE INDEX IF NOT EXISTS address_key_index ON " +
                    TABLE_NAME + " (" + ADDRESS + ", " + IDENTITY_KEY + ");"};

    private final Context context;
    private final SQLiteOpenHelper databaseHelper;

    public IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
        this.context        = context;
        this.databaseHelper = databaseHelper;
    }

    public boolean isFreshIdentity(CanonicalRecipientAddress address) {
        SQLiteDatabase database  = databaseHelper.getReadableDatabase();
        long           addressId = address.getCanonicalAddress(context);
        Cursor         cursor    = null;

        try {
            cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?",
                    new String[] {String.valueOf(addressId)}, null, null, null);

            return cursor == null || cursor.isAfterLast() || cursor.getCount() == 0;
        } finally {
            if (cursor != null)
                cursor.close();
        }
    }

    public boolean isValidIdentity(MasterSecret masterSecret,
                                   CanonicalRecipientAddress address,
                                   IdentityKey theirIdentity)
    {
        SQLiteDatabase database     = databaseHelper.getReadableDatabase();
        MasterCipher   masterCipher = new MasterCipher(masterSecret);
        long           addressId    = address.getCanonicalAddress(context);
        Cursor         cursor       = null;

        try {
            cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?",
                    new String[] {String.valueOf(addressId)}, null, null,null);

            if (cursor == null || cursor.isAfterLast() || cursor.getCount() == 0) {
                return true;
            }

            while (cursor.moveToNext()) {
                try {
                    String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
                    String mac                = cursor.getString(cursor.getColumnIndexOrThrow(MAC));

                    if (!masterCipher.verifyMacFor(addressId + serializedIdentity, Base64.decode(mac))) {
                        Log.w("IdentityDatabase", "MAC failed");
                        continue;
                    }

                    IdentityKey ourIdentity = new IdentityKey(Base64.decode(serializedIdentity), 0);

                    if (ourIdentity.equals(theirIdentity)) {
                        return true;
                    }
                } catch (IOException e) {
                    Log.w("IdentityDatabase", e);
                    return false;
                } catch (InvalidKeyException e) {
                    Log.w("IdentityDatabase", e);
                    return false;
                }
            }

            return false;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    public void saveIdentity(MasterSecret masterSecret,
                             CanonicalRecipientAddress address,
                             IdentityKey identityKey)
    {
        SQLiteDatabase database          = databaseHelper.getWritableDatabase();
        long           addressId         = address.getCanonicalAddress(context);
        MasterCipher   masterCipher      = new MasterCipher(masterSecret);
        String         identityKeyString = Base64.encodeBytes(identityKey.serialize());
        String         macString         = Base64.encodeBytes(masterCipher.getMacFor(addressId +
                identityKeyString  ));

        ContentValues contentValues = new ContentValues();
        contentValues.put(ADDRESS, addressId);
        contentValues.put(IDENTITY_KEY, identityKeyString);
        contentValues.put(MAC, macString);

        database.replace(TABLE_NAME, null, contentValues);
        context.getContentResolver().notifyChange(CHANGE_URI, null);
    }

    public Cursor getIdentities() {
        SQLiteDatabase database = databaseHelper.getReadableDatabase();
        Cursor cursor           = database.query(TABLE_NAME, null, null, null, null, null, ADDRESS);

        if (cursor != null)
            cursor.setNotificationUri(context.getContentResolver(), CHANGE_URI);

        return cursor;
    }

    public Reader readerFor(MasterSecret masterSecret, Cursor cursor) {
        return new Reader(masterSecret, cursor);
    }

    public class Reader {

        private final Cursor cursor;
        private final MasterCipher cipher;

        public Reader(MasterSecret masterSecret, Cursor cursor) {
            this.cursor = cursor;
            this.cipher = new MasterCipher(masterSecret);
        }

        public Pair<String, IdentityKey> getCurrent() {
            String number = null;

            try {
                long canonicalAddress = cursor.getLong(cursor.getColumnIndexOrThrow(ADDRESS));
                number                = DatabaseFactory.getAddressDatabase(context)
                        .getNumberFromCanonicalAddress(canonicalAddress);

                String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
                String mac               = cursor.getString(cursor.getColumnIndexOrThrow(MAC));

                if (!cipher.verifyMacFor(canonicalAddress + identityKeyString, Base64.decode(mac))) {
                    return new Pair<String, IdentityKey>(number, null);
                }

                IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyString), 0);
                return new Pair<String, IdentityKey>(number, identityKey);
            } catch (IOException e) {
                Log.w("IdentityDatabase", e);
                return new Pair<String, IdentityKey>(number, null);
            } catch (InvalidKeyException e) {
                Log.w("IdentityDatabase", e);
                return new Pair<String, IdentityKey>(number, null);
            } catch (NoSuchAddressException e) {
                Log.w("IdentityDatabase", e);
                return new Pair<String, IdentityKey>(null, null);
            }
        }
    }

    public static void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE);
        for (String index : CREATE_INDEX) {
            db.execSQL(index);
        }
    }
}
