/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.weex.appfram.storage;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteFullException;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.Nullable;

import org.apache.weex.common.WXThread;
import org.apache.weex.utils.WXLogUtils;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DefaultWXStorage implements IWXStorageAdapter {

    private WXSQLiteOpenHelper mDatabaseSupplier;

    private ExecutorService mExecutorService;

    private void execute(@Nullable final Runnable runnable) {
        if (mExecutorService == null) {
            mExecutorService = Executors.newSingleThreadExecutor();
        }

        if(runnable != null && !mExecutorService.isShutdown() && !mExecutorService.isTerminated()) {
            mExecutorService.execute(WXThread.secure(runnable));
        }
    }

    public DefaultWXStorage(Context context) {
        this.mDatabaseSupplier = new WXSQLiteOpenHelper(context);
    }


    @Override
    public void setItem(final String key, final String value, final OnResultReceivedListener listener) {
        execute(new Runnable() {
            @Override
            public void run() {
                Map<String, Object> data = StorageResultHandler.setItemResult(performSetItem(key, value, false, true));
                if (listener == null) {
                    return;
                }
                listener.onReceived(data);
            }
        });
    }

    @Override
    public void getItem(final String key, final OnResultReceivedListener listener) {
        execute(new Runnable() {
            @Override
            public void run() {
                Map<String, Object> data = StorageResultHandler.getItemResult(performGetItem(key));
                if (listener == null) {
                    return;
                }
                listener.onReceived(data);
            }
        });
    }

    @Override
    public void removeItem(final String key, final OnResultReceivedListener listener) {
        execute(new Runnable() {
            @Override
            public void run() {
                Map<String, Object> data = StorageResultHandler.removeItemResult(performRemoveItem(key));
                if (listener == null) {
                    return;
                }
                listener.onReceived(data);
            }
        });
    }

    @Override
    public void length(final OnResultReceivedListener listener) {
        execute(new Runnable() {
            @Override
            public void run() {
                Map<String, Object> data = StorageResultHandler.getLengthResult(performGetLength());
                if (listener == null) {
                    return;
                }
                listener.onReceived(data);
            }
        });
    }

    @Override
    public void getAllKeys(final OnResultReceivedListener listener) {
        execute(new Runnable() {
            @Override
            public void run() {
                Map<String, Object> data = StorageResultHandler.getAllkeysResult(performGetAllKeys());
                if (listener == null) {
                    return;
                }
                listener.onReceived(data);
            }
        });
    }

    @Override
    public void setItemPersistent(final String key, final String value, final OnResultReceivedListener listener) {
        execute(new Runnable() {
            @Override
            public void run() {
                Map<String, Object> data = StorageResultHandler.setItemResult(performSetItem(key, value, true, true));
                if (listener == null) {
                    return;
                }
                listener.onReceived(data);
            }
        });
    }

    @Override
    public void close() {
        final ExecutorService needCloseService = mExecutorService;
        execute(new Runnable() {
            @Override
            public void run() {
                try {
                    mDatabaseSupplier.closeDatabase();
                    if (needCloseService != null) {
                        needCloseService.shutdown();
                    }
                } catch (Exception e) {
                    WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, e.getMessage());
                }
            }
        });
        mExecutorService = null;
    }

    private boolean performSetItem(String key, String value, boolean isPersistent, boolean allowRetryWhenFull) {
        SQLiteDatabase database = mDatabaseSupplier.getDatabase();
        if (database == null) {
            return false;
        }

        WXLogUtils.d(WXSQLiteOpenHelper.TAG_STORAGE, "set k-v to storage(key:" + key + ",value:" + value + ",isPersistent:" + isPersistent + ",allowRetry:" + allowRetryWhenFull + ")");
        String sql = "INSERT OR REPLACE INTO " + WXSQLiteOpenHelper.TABLE_STORAGE + " VALUES (?,?,?,?);";
        SQLiteStatement statement = null;
        String timeStamp = WXSQLiteOpenHelper.sDateFormatter.format(new Date());
        try {
            statement = database.compileStatement(sql);
            statement.clearBindings();
            statement.bindString(1, key);
            statement.bindString(2, value);
            statement.bindString(3, timeStamp);
            statement.bindLong(4, isPersistent ? 1 : 0);
            statement.execute();
            return true;
        } catch (Exception e) {
            WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute setItem :" + e.getMessage());
            if (e instanceof SQLiteFullException) {
                if (allowRetryWhenFull && trimToSize()) {
                    //try again
                    //setItem/setItemPersistent method only allow try once when occurred a sqliteFullException.
                    WXLogUtils.d(WXSQLiteOpenHelper.TAG_STORAGE, "retry set k-v to storage(key:" + key + ",value:" + value + ")");
                    return performSetItem(key, value, isPersistent, false);
                }
            }

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

    /**
     * remove 10% of total record(at most) ordered by timestamp.
     * */
    private boolean trimToSize() {
        SQLiteDatabase database = mDatabaseSupplier.getDatabase();
        if (database == null) {
            return false;
        }

        List<String> toEvict = new ArrayList<>();
        int num = 0;

        Cursor c = database.query(WXSQLiteOpenHelper.TABLE_STORAGE, new String[]{WXSQLiteOpenHelper.COLUMN_KEY, WXSQLiteOpenHelper.COLUMN_PERSISTENT}, null, null, null, null, WXSQLiteOpenHelper.COLUMN_TIMESTAMP + " ASC");
        try {
            int evictSize = c.getCount() / 10;
            while (c.moveToNext()) {
                String key = c.getString(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_KEY));
                boolean persistent = c.getInt(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_PERSISTENT)) == 1;
                if (!persistent && key != null) {
                    num++;
                    toEvict.add(key);
                    if (num == evictSize) {
                        break;
                    }
                }
            }
        } catch (Exception e) {
            WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute trimToSize:" + e.getMessage());
        } finally {
            c.close();
        }

        if (num <= 0) {
            return false;
        }

        for (String key : toEvict) {
            performRemoveItem(key);
        }
        WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "remove " + num + " items by lru");
        return true;
    }

    private String performGetItem(String key) {
        SQLiteDatabase database = mDatabaseSupplier.getDatabase();
        if (database == null) {
            return null;
        }

        Cursor c = database.query(WXSQLiteOpenHelper.TABLE_STORAGE,
                new String[]{WXSQLiteOpenHelper.COLUMN_VALUE},
                WXSQLiteOpenHelper.COLUMN_KEY + "=?",
                new String[]{key},
                null, null, null);
        try {
            if (c.moveToNext()) {
                ContentValues values = new ContentValues();
                //update timestamp
                values.put(WXSQLiteOpenHelper.COLUMN_TIMESTAMP, WXSQLiteOpenHelper.sDateFormatter.format(new Date()));
                int updateResult = mDatabaseSupplier.getDatabase().update(WXSQLiteOpenHelper.TABLE_STORAGE, values, WXSQLiteOpenHelper.COLUMN_KEY + "= ?", new String[]{key});

                WXLogUtils.d(WXSQLiteOpenHelper.TAG_STORAGE, "update timestamp " + (updateResult == 1 ? "success" : "failed") + " for operation [getItem(key = " + key + ")]");
                return c.getString(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_VALUE));
            } else {
                return null;
            }
        } catch (Exception e) {
            WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute getItem:" + e.getMessage());
            return null;
        } finally {
            c.close();
        }
    }

    private boolean performRemoveItem(String key) {
        SQLiteDatabase database = mDatabaseSupplier.getDatabase();
        if (database == null) {
            return false;
        }

        int count = 0;
        try {
            count = database.delete(WXSQLiteOpenHelper.TABLE_STORAGE,
                    WXSQLiteOpenHelper.COLUMN_KEY + "=?",
                    new String[]{key});
        } catch (Exception e) {
            WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute removeItem:" + e.getMessage());
            return false;
        }
        return count == 1;
    }

    private long performGetLength() {
        SQLiteDatabase database = mDatabaseSupplier.getDatabase();
        if (database == null) {
            return 0;
        }

        String sql = "SELECT count(" + WXSQLiteOpenHelper.COLUMN_KEY + ") FROM " + WXSQLiteOpenHelper.TABLE_STORAGE;
        SQLiteStatement statement = null;
        try {
            statement = database.compileStatement(sql);
            return statement.simpleQueryForLong();
        } catch (Exception e) {
            WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute getLength:" + e.getMessage());
            return 0;
        } finally {
            if(statement != null) {
                statement.close();
            }
        }
    }

    private List<String> performGetAllKeys() {
        SQLiteDatabase database = mDatabaseSupplier.getDatabase();
        if (database == null) {
            return null;
        }

        List<String> result = new ArrayList<>();
        Cursor c = database.query(WXSQLiteOpenHelper.TABLE_STORAGE, new String[]{WXSQLiteOpenHelper.COLUMN_KEY}, null, null, null, null, null);
        try {
            while (c.moveToNext()) {
                result.add(c.getString(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_KEY)));
            }
            return result;
        } catch (Exception e) {
            WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute getAllKeys:" + e.getMessage());
            return result;
        } finally {
            c.close();
        }
    }

}
