/**
 * JBoss, Home of Professional Open Source
 * Copyright Red Hat, Inc., and individual contributors.
 *
 * 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 org.jboss.aerogear.android.impl.datamanager;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.jboss.aerogear.android.Callback;
import org.jboss.aerogear.android.ReadFilter;
import org.jboss.aerogear.android.datamanager.IdGenerator;
import org.jboss.aerogear.android.datamanager.Store;
import org.jboss.aerogear.android.datamanager.StoreType;
import org.jboss.aerogear.android.impl.reflection.Property;
import org.jboss.aerogear.android.impl.reflection.Scan;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.AsyncTask;
import android.util.Log;
import android.util.Pair;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;

public class SQLStore<T> extends SQLiteOpenHelper implements Store<T> {

    private static final String TAG = SQLStore.class.getSimpleName();
    private final Class<T> klass;
    private final String className;
    private final static String CREATE_PROPERTIES_TABLE = "create table if not exists %s_property "
            + " ( _ID integer primary key autoincrement,"
            + "  PARENT_ID text not null,"
            + "  PROPERTY_NAME text not null,"
            + "  PROPERTY_VALUE text )";
    private final static String CREATE_PROPERTIES_INDEXES = "create index  if not exists %s_property_name_index "
            + " ON %s_property (PROPERTY_NAME);"
            + "create index if not exists %s_property_name_value_index "
            + " ON %s_property (PROPERTY_NAME, PROPERTY_VALUE) ;"
            + "create index  if not exists %s_property_parent_index "
            + " ON %s_property (PARENT_ID);";
    private SQLiteDatabase database;
    private final Gson gson;
    private final IdGenerator generator;

    public SQLStore(Class<T> klass, Context context) {
        super(context, klass.getSimpleName(), null, 1);
        this.klass = klass;
        this.className = klass.getSimpleName();
        this.gson = new Gson();
        this.generator = new DefaultIdGenerator();
    }

    public SQLStore(Class<T> klass, Context context, GsonBuilder builder, IdGenerator generator) {
        super(context, klass.getSimpleName(), null, 1);
        this.klass = klass;
        this.className = klass.getSimpleName();
        this.gson = builder.create();
        this.generator = generator;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public StoreType getType() {
        return StoreTypes.SQL;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public Collection<T> readAll() {
        String sql = String.format("Select PROPERTY_NAME, PROPERTY_VALUE,PARENT_ID from %s_property", className);
        Cursor cursor = database.rawQuery(sql, new String[0]);
        HashMap<Integer, JsonObject> objects = new HashMap<Integer, JsonObject>(cursor.getCount());
        try {
            while (cursor.moveToNext()) {
                Integer id = cursor.getInt(2);
                JsonObject object = objects.get(id);
                if (object == null) {
                    object = new JsonObject();
                    objects.put(id, object);
                }
                add(object, cursor.getString(0), cursor.getString(1));
            }
        } finally {
            cursor.close();
        }
        ArrayList<T> data = new ArrayList<T>(cursor.getCount());
        for (JsonObject object : objects.values()) {
            data.add(gson.fromJson(object, klass));
        }

        return data;

    }

    /**
     * {@inheritDoc }
     */
    @Override
    public T read(Serializable id) {
        String sql = String.format("Select PROPERTY_NAME, PROPERTY_VALUE from %s_property where PARENT_ID = ?", className);
        String[] bindArgs = new String[1];
        bindArgs[0] = id.toString();
        JsonObject result = new JsonObject();
        Cursor cursor = database.rawQuery(sql, bindArgs);

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

        try {
            while (cursor.moveToNext()) {
                add(result, cursor.getString(0), cursor.getString(1));
            }
        } finally {
            cursor.close();
        }

        return gson.fromJson(result, klass);

    }

    /**
     * {@inheritDoc }
     */
    @Override
    public List<T> readWithFilter(ReadFilter filter) {
        if (filter == null) {
            filter = new ReadFilter();
        }
        String sql = String.format("select PARENT_ID from %s_property where PROPERTY_NAME = ? and PROPERTY_VALUE = ?", className);
        JsonObject where = (JsonObject) new JsonParser().parse(filter.getWhere().toString());
        List<Pair<String, String>> queryList = new ArrayList<Pair<String, String>>();
        Map<String, AtomicInteger> resultCount = new HashMap<String, AtomicInteger>();
        buildKeyValuePairs(where, queryList, "");

        if (queryList.isEmpty()) {//there is no query
            return new ArrayList<T>(readAll());
        } else {
            for (Pair<String, String> kv : queryList) {
                String[] bindArgs = new String[] { kv.first, kv.second };
                Cursor cursor = database.rawQuery(sql, bindArgs);
                while (cursor.moveToNext()) {
                    String id = cursor.getString(0);
                    AtomicInteger count = resultCount.get(id);
                    if (count == null) {
                        count = new AtomicInteger(0);
                        resultCount.put(id, count);
                    }
                    count.incrementAndGet();
                }
            }
        }
        List<T> results = new ArrayList<T>();

        for (String id : resultCount.keySet()) {
            if (resultCount.get(id).get() == queryList.size()) {//There are as many objects as queries which meant a result was returned for every query
                results.add(read(id));
            }
        }

        return results;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void save(T item) {
        String recordIdFieldName = Scan.recordIdFieldNameIn(item.getClass());
        Property property = new Property(item.getClass(), recordIdFieldName);
        Serializable idValue = (Serializable) property.getValue(item);

        if (idValue == null) {
            idValue = generator.generate();
            property.setValue(item, idValue);
        }

        JsonObject serialized = (JsonObject) gson.toJsonTree(item, klass);
        saveElement(serialized, "", idValue);
    }

    private void saveElement(JsonObject serialized, String path, Serializable id) {
        String sql = String.format("insert into %s_property (PROPERTY_NAME, PROPERTY_VALUE, PARENT_ID) values (?,?,?)", className);
        Set<Entry<String, JsonElement>> members = serialized.entrySet();
        String pathVar = path.isEmpty() ? "" : ".";
        for (Entry<String, JsonElement> member : members) {
            JsonElement jsonValue = member.getValue();
            String propertyName = member.getKey();
            if (jsonValue.isJsonObject()) {
                saveElement((JsonObject) jsonValue, path + pathVar + propertyName, id);
            } else {
                if (jsonValue.isJsonPrimitive()) {
                    JsonPrimitive primitive = jsonValue.getAsJsonPrimitive();
                    if (primitive.isBoolean()) {
                        Integer value = primitive.getAsBoolean() ? 1 : 0;
                        database.execSQL(sql, new Object[] { path + pathVar + propertyName, value, id });
                    } else if (primitive.isNumber()) {
                        Number value = primitive.getAsNumber();
                        database.execSQL(sql, new Object[] { path + pathVar + propertyName, value, id });
                    } else if (primitive.isString()) {
                        String value = primitive.getAsString();
                        database.execSQL(sql, new Object[] { path + pathVar + propertyName, value, id });
                    } else {
                        throw new IllegalArgumentException(jsonValue + " isn't a number, boolean, or string");
                    }

                } else {
                    throw new IllegalArgumentException(jsonValue + " isn't a JsonPrimitive");
                }

            }
        }
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void reset() {
        String sql = String.format("Delete from %s_property", className);
        database.execSQL(sql);
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void remove(Serializable id) {
        String sql = String.format("Delete from %s_property where PARENT_ID = ?", className);
        Object[] bindArgs = new Object[1];
        bindArgs[0] = id;
        database.execSQL(sql, bindArgs);

    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void onCreate(SQLiteDatabase db) {

        db.execSQL(String.format(CREATE_PROPERTIES_TABLE, className));
        db.execSQL(String.format(CREATE_PROPERTIES_INDEXES, className, className, className, className, className, className));
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }

    public void open(final Callback onReady) {
        new AsyncTask<Void, Void, Void>() {
            private Exception exception;

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    SQLStore.this.database = getWritableDatabase();
                    onReady.onSuccess(this);
                } catch (Exception e) {
                    this.exception = e;
                    Log.e(TAG, "There was an error loading the database", e);
                }
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
                if (exception != null) {
                    onReady.onFailure(exception);
                } else {
                    onReady.onSuccess(SQLStore.this);
                }
            }
        }.execute((Void) null);
    }

    @Override
    public void close() {
        this.database.close();
    }

    private void add(JsonObject result, String propertyName, String propertyValue) {
        if (!propertyName.contains(".")) {
            result.addProperty(propertyName, propertyValue);
        } else {
            String[] names = propertyName.split("\\.", 2);
            JsonObject subObject = (JsonObject) result.get(names[0]);
            if (subObject == null) {
                subObject = new JsonObject();
                result.add(names[0], subObject);
            }
            add(subObject, names[1], propertyValue);

        }
    }

    private void buildKeyValuePairs(JsonObject where, List<Pair<String, String>> keyValues, String parentPath) {
        Set<Entry<String, JsonElement>> keys = where.entrySet();
        String pathVar = parentPath.isEmpty() ? "" : ".";//Set a dot if parent path is not empty
        for (Entry<String, JsonElement> entry : keys) {
            String key = entry.getKey();
            String path = parentPath + pathVar + key;
            JsonElement jsonValue = entry.getValue();
            if (jsonValue.isJsonObject()) {
                buildKeyValuePairs((JsonObject) jsonValue, keyValues, path);
            } else {
                if (jsonValue.isJsonPrimitive()) {
                    JsonPrimitive primitive = jsonValue.getAsJsonPrimitive();
                    if (primitive.isBoolean()) {
                        Integer value = primitive.getAsBoolean() ? 1 : 0;
                        keyValues.add(new Pair<String, String>(path, value.toString()));
                    } else if (primitive.isNumber()) {
                        Number value = primitive.getAsNumber();
                        keyValues.add(new Pair<String, String>(path, value.toString()));
                    } else if (primitive.isString()) {
                        String value = primitive.getAsString();
                        keyValues.add(new Pair<String, String>(path, value));
                    } else {
                        throw new IllegalArgumentException(jsonValue + " isn't a number, boolean, or string");
                    }

                } else {
                    throw new IllegalArgumentException(jsonValue + " isn't a JsonPrimitive");
                }

            }
        }
    }
}
