package org.fdroid.fdroid.data;

import android.annotation.TargetApi;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;

import org.fdroid.fdroid.Utils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public abstract class FDroidProvider extends ContentProvider {

    private static final String TAG = "FDroidProvider";

    static final String AUTHORITY = "org.fdroid.fdroid.data";

    static final int CODE_LIST   = 1;
    static final int CODE_SINGLE = 2;

    private static DBHelper dbHelper;

    private boolean isApplyingBatch;

    protected abstract String getTableName();

    protected abstract String getProviderName();

    /**
     * Should always be the same as the provider:name in the AndroidManifest
     */
    public final String getName() {
        return AUTHORITY + "." + getProviderName();
    }

    /**
     * Tells us if we are in the middle of a batch of operations. Allows us to
     * decide not to notify the content resolver of changes,
     * every single time we do something during many operations.
     * Based on http://stackoverflow.com/a/15886915.
     */
    protected final boolean isApplyingBatch() {
        return this.isApplyingBatch;
    }

    @NonNull
    @Override
    public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations)
            throws OperationApplicationException {
        ContentProviderResult[] result = null;
        isApplyingBatch = true;
        final SQLiteDatabase db = db();
        db.beginTransaction();
        try {
            result = super.applyBatch(operations);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
            isApplyingBatch = false;
        }
        return result;
    }

    /**
     * Only used for testing. Not quite sure how to mock a singleton variable like this.
     */
    public static void clearDbHelperSingleton() {
        dbHelper = null;
    }

    private static synchronized DBHelper getOrCreateDb(Context context) {
        if (dbHelper == null) {
            Utils.debugLog(TAG, "First time accessing database, creating new helper");
            dbHelper = new DBHelper(context);
        }
        return dbHelper;
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    protected final synchronized SQLiteDatabase db() {
        return getOrCreateDb(getContext().getApplicationContext()).getWritableDatabase();
    }

    @Override
    public String getType(@NonNull Uri uri) {
        String type;
        switch (getMatcher().match(uri)) {
            case CODE_LIST:
                type = "dir";
                break;
            case CODE_SINGLE:
            default:
                type = "item";
                break;
        }
        return "vnd.android.cursor." + type + "/vnd." + AUTHORITY + "." + getProviderName();
    }

    protected abstract UriMatcher getMatcher();

    protected static String generateQuestionMarksForInClause(int num) {
        StringBuilder sb = new StringBuilder(num * 2);
        for (int i = 0; i < num; i++) {
            if (i != 0) {
                sb.append(',');
            }
            sb.append('?');
        }
        return sb.toString();
    }

    @TargetApi(11)
    private Set<String> getKeySet(ContentValues values) {

        if (Build.VERSION.SDK_INT >= 11) {
            return values.keySet();
        }

        Set<String> keySet = new HashSet<>();
        for (Map.Entry<String, Object> item : values.valueSet()) {
            String key = item.getKey();
            keySet.add(key);
        }
        return keySet;

    }

    protected void validateFields(String[] validFields, ContentValues values)
            throws IllegalArgumentException {
        for (final String key : getKeySet(values)) {
            boolean isValid = false;
            for (final String validKey : validFields) {
                if (validKey.equals(key)) {
                    isValid = true;
                    break;
                }
            }

            if (!isValid) {
                throw new IllegalArgumentException(
                    "Cannot save field '" + key + "' to provider " + getProviderName());
            }
        }
    }
}
