package com.hikapro.backpack.model.dao;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.hikapro.backpack.App;
import com.hikapro.backpack.R;
import com.hikapro.backpack.model.Api;
import com.hikapro.backpack.model.Model;
import com.hikapro.backpack.model.NetworkUtil;
import com.hikapro.backpack.model.RestClient;
import com.hikapro.backpack.model.SetModel;
import com.hikapro.backpack.model.entities.Category;
import com.hikapro.backpack.model.entities.Item;
import com.hikapro.backpack.model.entities.Set;
import com.hikapro.backpack.model.entities.SharingPicture;
import com.hikapro.backpack.model.entities.Timestamp;
import com.hikapro.backpack.model.entities.UpdateLog;
import com.hikapro.backpack.presenter.adapters.helper.AppRater;

import retrofit2.Response;

/**
 * Created by tariel on 20/04/16.
 */
public class DAO {
    //region Constants
    private static final int CORE_POOL_SIZE = 1;
    private static final int CORE_MAX_POOL_SIZE = 1;

    private static final int KEEP_ALIVE_TIME = 1;
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
    //endregion

    private static DAO instance;

    private final ThreadPoolExecutor threadPool;
    private final Handler   handler;
    private final Api       api;
    private Context         context;
    private DbHelper        helper;
    private Map<String, Model.Base> observers;

    private DAO() {
        this.context    = App.getAppContext();
        this.helper     = new DbHelper(this.context);
        this.api        = RestClient.getApi();
        final BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();
        this.threadPool = initPool(taskQueue);
        this.observers  = Collections.synchronizedMap(new HashMap<String, Model.Base>());
        this.handler    = initHandler();
    }

    static {
        instance = new DAO();
    }

    public static DAO getInstance() {
        return instance;
    }

    public void registerObserver(Model.Base o) {
        observers.put(o.getClass().getName(), o);
    }

    public void unregisterObserver(Model.Base o) {
        observers.remove(o.getClass().getName());
    }

    private ThreadPoolExecutor initPool (BlockingQueue<Runnable> taskQueue) {
        ThreadPoolExecutor ret = new ThreadPoolExecutor(
                CORE_POOL_SIZE
                ,CORE_MAX_POOL_SIZE
                ,KEEP_ALIVE_TIME
                ,KEEP_ALIVE_TIME_UNIT
                ,taskQueue
        );
        return ret;
    }

    private Handler initHandler() {
        Handler ret = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                for (Map.Entry<String, Model.Base> entry : observers.entrySet()) {
                    entry.getValue().onEvent(msg);
                }
            }
        };
        return ret;
    }

    public void executeCommand(Message command) {

        SetTask     setTask;
        ItemTask    itemTask;
        ImageProviderTask imageProviderTask;

        if (command != null) {
            switch (command.what) {

                case Command.SET_GET_ALL:
                    setTask = new SetTask(Command.SET_GET_ALL,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    threadPool.execute(setTask);
                    break;

                case Command.SET_REORDER:
                    setTask = new SetTask(Command.SET_REORDER,
                            Process.THREAD_PRIORITY_BACKGROUND);
                    setTask.setsToUpdate = (List<Set>) command.obj;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_GET_ITEMS:
                    setTask = new SetTask(Command.SET_GET_ITEMS,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    setTask.setId = command.arg1;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_GET_ITEMS_LIKE:
                    setTask = new SetTask(Command.SET_GET_ITEMS_LIKE,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    setTask.setId = command.arg1;
                    setTask.query = (String) command.obj;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_GET_PACKED_ITEMS :
                    setTask = new SetTask(Command.SET_GET_PACKED_ITEMS,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    setTask.setId = command.arg1;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_GET_ITEMS_LIKE_PACKED:
                    setTask = new SetTask(Command.SET_GET_ITEMS_LIKE_PACKED,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    setTask.setId = command.arg1;
                    setTask.query = (String) command.obj;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_UNPACK_ITEMS:
                    setTask = new SetTask(Command.SET_UNPACK_ITEMS,
                            Process.THREAD_PRIORITY_DEFAULT);
                    setTask.setId = command.arg1;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_RESTORE_DEFAULT:
                    setTask = new SetTask(Command.SET_RESTORE_DEFAULT,
                            Process.THREAD_PRIORITY_DEFAULT);
                    setTask.setId = command.arg1;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_GET_STAT:
                    setTask = new SetTask(Command.SET_GET_STAT,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    setTask.setId = command.arg1;
                    threadPool.execute(setTask);
                    break;

                case Command.SET_CLEAN_PACKED:
                    setTask = new SetTask(Command.SET_CLEAN_PACKED,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    setTask.setId = command.arg1;
                    threadPool.execute(setTask);
                    break;

                case Command.ITEM_GET_CATEGORIES:
                    itemTask = new ItemTask(Command.ITEM_GET_CATEGORIES,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_INSERT:
                    itemTask = new ItemTask(Command.ITEM_INSERT,
                            Process.THREAD_PRIORITY_BACKGROUND);
                    itemTask.item   = (Item) command.obj;
                    itemTask.setId  = command.arg1;
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_READ:
                    itemTask = new ItemTask(Command.ITEM_READ,
                            Process.THREAD_PRIORITY_DEFAULT);
                    itemTask.setId  = command.arg1;
                    itemTask.itemId = command.arg2;
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_DELETE_FROM_SET:
                    itemTask = new ItemTask(Command.ITEM_DELETE_FROM_SET,
                            Process.THREAD_PRIORITY_BACKGROUND);
                    itemTask.setId  = command.arg1;
                    itemTask.itemId = command.arg2;
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_PENDING_REMOVAL:
                    itemTask = new ItemTask(Command.ITEM_PENDING_REMOVAL,
                            Process.THREAD_PRIORITY_BACKGROUND);
                    itemTask.setId  = command.arg1;
                    itemTask.itemId = command.arg2;
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_REMOVAL_CANCEL:
                    itemTask = new ItemTask(Command.ITEM_REMOVAL_CANCEL,
                            Process.THREAD_PRIORITY_BACKGROUND);
                    itemTask.setId  = command.arg1;
                    itemTask.itemId = command.arg2;
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_PACK :
                    itemTask = new ItemTask(Command.ITEM_PACK,
                            Process.THREAD_PRIORITY_BACKGROUND);
                    itemTask.setId  = command.arg1;
                    itemTask.itemId = command.arg2;
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_UNPACK :
                    itemTask = new ItemTask(Command.ITEM_UNPACK,
                            Process.THREAD_PRIORITY_BACKGROUND);
                    itemTask.setId  = command.arg1;
                    itemTask.itemId = command.arg2;
                    threadPool.execute(itemTask);
                    break;

                case Command.ITEM_GET_IMAGE :
                    Item item = findItem(command.arg1);
                    if (item != null) {
                        imageProviderTask = new ImageProviderTask(Command.ITEM_GET_IMAGE,
                                Process.THREAD_PRIORITY_DEFAULT);
                        imageProviderTask.item = item;
                        threadPool.execute(imageProviderTask);
                    }
                    break;

                case Command.ITEM_GET_LIKE :
                    itemTask = new ItemTask(Command.ITEM_GET_LIKE,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    itemTask.query = (String) command.obj;
                    itemTask.setId = command.arg1;
                    threadPool.execute(itemTask);
                    break;

                case Command.SHARING_GET_PIC:
                    imageProviderTask = new ImageProviderTask(Command.SHARING_GET_PIC,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE);
                    threadPool.execute(imageProviderTask);
                    break;

                case Command.SYNC:
                    threadPool.execute(new SyncTask(Command.SYNC,
                            Process.THREAD_PRIORITY_BACKGROUND));
                    break;

                case Command.SYNC_READ_FROM_FILE:
                    threadPool.execute(new SyncTask(Command.SYNC_READ_FROM_FILE,
                            Process.THREAD_PRIORITY_BACKGROUND));
                    break;

                case Command.SYNC_IF_NOT_EXISTS:
                    threadPool.execute(new SyncTask(Command.SYNC_IF_NOT_EXISTS,
                            Process.THREAD_PRIORITY_MORE_FAVORABLE));
                    break;
            }
        }
    }

    ///////////////////////  DATABASE  /////////////////////

    //region Database
    private SQLiteDatabase getReadDB(){
        return helper.getReadableDatabase();
    }

    private SQLiteDatabase getWriteDB(){
        return helper.getWritableDatabase();
    }

    // inserts
    private void insertTimestamp(Timestamp timestamp) {
        if (timestamp != null && timestamp.timestamp > 0) {
            ContentValues values;
            SQLiteDatabase db = getWriteDB();
            try {
                db.beginTransaction();
                values = Db.LogTable.toContentValues(timestamp);
                db.insert(Db.LogTable.TABLE_NAME, null, values);
                db.setTransactionSuccessful();
            } catch (Exception ex) {
                Log.e("Insert Timestamp", ex.getMessage());
            } finally {
                db.endTransaction();
                db.close();
            }
        }
    }

    private void insertSharingPic(SharingPicture sharingPicture) {
        if (sharingPicture != null) {
            ContentValues values;
            SQLiteDatabase db = getWriteDB();
            try {
                db.beginTransaction();
                values = Db.SharingPicsTable.toContentValues(sharingPicture);
                db.insert(Db.SharingPicsTable.TABLE_NAME, null, values);
                db.setTransactionSuccessful();
            } catch (Exception ex) {
                Log.e("Inserting sharing photo", ex.getMessage());
            } finally {
                db.endTransaction();
                db.close();
            }
        }
    }

    private void insertItems(List<Item> items) {
        if (items != null && !items.isEmpty()) {
            ContentValues values;
            SQLiteDatabase db = getWriteDB();
            try {
                db.beginTransaction();
                for (Item item : items) {
                    values = Db.ItemsTable.toContentValues(item);
                    db.insert(Db.ItemsTable.TABLE_NAME, null, values);
                }
                db.setTransactionSuccessful();
            } catch (Exception ex) {
                Log.e("Insert Items", ex.getMessage());
            } finally {
                db.endTransaction();
                db.close();
            }
        }
    }

    private long insertItem(Item item) {
        long ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        Cursor cursor = null;
        if (item != null) {
            try {
                if (item.getId() < 0) {
                    db = getWriteDB();
                    db.beginTransaction();
                    String query = String.format("SELECT %s FROM %s WHERE %s = 1 ORDER BY %s DESC LIMIT 1",
                            Db.ItemsTable.COLUMN_ID,
                            Db.ItemsTable.TABLE_NAME,
                            Db.ItemsTable.COLUMN_USER_DEFINED,
                            Db.ItemsTable.COLUMN_ID);
                    cursor = db.rawQuery(query, null);

                    if (cursor.moveToNext()) {
                        int id = cursor.getInt(0);
                        item.setId(id + 1);
                    } else {
                        query = String.format("SELECT max(%s) FROM %s",
                                Db.ItemsTable.COLUMN_ID,
                                Db.ItemsTable.TABLE_NAME);
                        cursor = db.rawQuery(query, null);
                        if (cursor.moveToNext()) {
                            int maxId = cursor.getInt(0);
                            item.setId(maxId + 0x400);
                        }
                    }
                    values = Db.ItemsTable.toContentValues(item);
                    ret = db.insert(Db.ItemsTable.TABLE_NAME, null, values);
                    db.setTransactionSuccessful();
                } else {
                    ret = item.getId();
                }
            } catch (Exception ex) {
                Log.e("Insert Item", ex.getMessage());
            } finally {
                if (cursor != null)
                    cursor.close();
                if (db != null) {
                    db.endTransaction();
                    db.close();
                }
            }
        }
        return ret;
    }

    private void insertCategories(List<Category> categories) {

        if (categories != null && !categories.isEmpty()) {
            ContentValues values;
            SQLiteDatabase db = getWriteDB();
            try {
                db.beginTransaction();
                for (Category category : categories) {
                    values = Db.CategoriesTable.toContentValues(category);
                    db.insert(Db.CategoriesTable.TABLE_NAME, null, values);
                }
                db.setTransactionSuccessful();
            } catch (Exception ex) {
                Log.e("Insert Categories", ex.getMessage());
            } finally {
                db.endTransaction();
                db.close();
            }
        }
    }

    private void insertSets(List<Set> sets) {

        if (sets != null && !sets.isEmpty()) {
            ContentValues values;
            int i = 0;
            SQLiteDatabase db = getWriteDB();
            try {
                db.beginTransaction();
                for (Set set : sets) {
                    values = Db.SetsTable.toContentValues(set, i);
                    db.insert(Db.SetsTable.TABLE_NAME, null, values);
                    insertSetItems(set, db);
                    ++i;
                }
                db.setTransactionSuccessful();
            } catch (Exception ex) {
                Log.e("Insert Sets", ex.getMessage());
            } finally {
                db.endTransaction();
                db.close();
            }
        }
    }

    private void insertSetItems(Set set, SQLiteDatabase db) {
        if (set != null && db != null) {
            if (!set.getItems().isEmpty()) {
                ContentValues values;
                int setId = set.getId();
                try {
                    db.beginTransaction();
                    for (Integer itemid : set.getItems()) {
                        values = Db.SetItemsTable.toContentValues(setId, itemid);
                        db.insert(Db.SetItemsTable.TABLE_NAME, null, values);
                    }
                    db.setTransactionSuccessful();
                } catch (Exception ex) {
                    Log.e("Insert Set Items", ex.getMessage());
                } finally {
                    db.endTransaction();
                }
            }
        }
    }

    private long insertSetItem(int setId, int itemId, boolean userDefined) {
        long ret = 0;
        ContentValues values;
        SQLiteDatabase db = getWriteDB();
        try {
            db.beginTransaction();
            values = Db.SetItemsTable.toContentValues(setId, itemId, userDefined);
            ret = db.insert(Db.SetItemsTable.TABLE_NAME, null, values);
            db.setTransactionSuccessful();
        } catch (Exception ex) {
            Log.e("Insert Set Item", ex.getMessage());
        } finally {
            db.endTransaction();
            db.close();
        }
        return ret;
    }

    // reads
    private boolean logExist() {
        boolean ret;
        SQLiteDatabase db = getReadDB();
        Cursor cursor = db.query(Db.LogTable.TABLE_NAME,
                new String[] {Db.LogTable.COLUMN_ID}, null, null, null, null, null, "1");
        ret = cursor.moveToNext();
        cursor.close();
        db.close();
        return ret;
    }

    private UpdateLog readLastLog() {
        UpdateLog ret = null;
        SQLiteDatabase db = null;
        Cursor cursor = null;
        try {
            db = getReadDB();
            String q = String.format("SELECT * FROM %s ORDER BY %s DESC LIMIT 1",
                    Db.LogTable.TABLE_NAME, Db.LogTable.COLUMN_ID);
            cursor = db.rawQuery(q,null);
            if (cursor.moveToNext())
                ret = Db.LogTable.parseCursor(cursor);
        } catch (Exception ex) {
            Log.e("Update Log", ex.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private SharingPicture readLastSharingPic() {
        SharingPicture ret = null;
        SQLiteDatabase db = null;
        Cursor cursor = null;
        try {
            db = getReadDB();
            String q = String.format("SELECT * FROM %s ORDER BY %s DESC LIMIT 1",
                    Db.SharingPicsTable.TABLE_NAME, Db.SharingPicsTable.COLUMN_ID);
            cursor = db.rawQuery(q,null);
            if (cursor.moveToNext())
                ret = Db.SharingPicsTable.parseCursor(cursor);
        } catch (Exception e) {
            Log.e("Reading sharing pic", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private Item findItem(int id) {
        Item ret = null;
        Cursor cursor = null;
        SQLiteDatabase db = null;
        try {
            db = getReadDB();
            String query = String.format("SELECT * FROM %s a WHERE a.%s = %d LIMIT 1",
                    Db.ItemsTable.TABLE_NAME, Db.ItemsTable.COLUMN_ID, id);
            cursor = db.rawQuery(query, null);
            if (cursor.moveToNext()) {
                ret = Db.ItemsTable.parseCursor(cursor);
            }
        } catch (Exception e) {
            Log.e("Find item", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private Item readItem(SQLiteDatabase db, int setId, int itemId) {
        Item ret = null;
        Cursor cursor = null;
        String query = String.format(
                "SELECT a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, %s, %s FROM %s a INNER JOIN %s b ON a.%s = b.%s WHERE b.%s = ? AND b.%s = ? LIMIT 1",

                Db.ItemsTable.COLUMN_ID,
                Db.ItemsTable.COLUMN_NAME,
                Db.ItemsTable.COLUMN_CATEGORY,
                Db.ItemsTable.COLUMN_DESCRIPTION,
                Db.ItemsTable.COLUMN_BUY_URLS,
                Db.ItemsTable.COLUMN_PHOTO_URL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_URL,
                Db.ItemsTable.COLUMN_PHOTO_LOCAL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_LOCAL,
                Db.ItemsTable.COLUMN_USER_DEFINED,
                Db.SetItemsTable.COLUMN_PENDING_REMOVAL,
                Db.SetItemsTable.COLUMN_PACKED,

                Db.ItemsTable.TABLE_NAME,
                Db.SetItemsTable.TABLE_NAME,
                Db.ItemsTable.COLUMN_ID,
                Db.SetItemsTable.COLUMN_ITEM,
                Db.SetItemsTable.COLUMN_SET,
                Db.SetItemsTable.COLUMN_ITEM);

        try {
            cursor = db.rawQuery(query, new String[]{String.valueOf(setId), String.valueOf(itemId)});
            if (cursor.moveToNext()) {
                ret = Db.ItemsTable.parseCursor(cursor);
            }
        } catch (Exception e) {
            Log.e("Read item", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return ret;
    }

    private Item readItem(int setId, int itemId) {
        Item ret = null;
        Cursor cursor = null;
        SQLiteDatabase db = null;
        String query = String.format(
                "SELECT a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, %s, %s FROM %s a INNER JOIN %s b ON a.%s = b.%s WHERE b.%s = ? AND b.%s = ? LIMIT 1",

                Db.ItemsTable.COLUMN_ID,
                Db.ItemsTable.COLUMN_NAME,
                Db.ItemsTable.COLUMN_CATEGORY,
                Db.ItemsTable.COLUMN_DESCRIPTION,
                Db.ItemsTable.COLUMN_BUY_URLS,
                Db.ItemsTable.COLUMN_PHOTO_URL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_URL,
                Db.ItemsTable.COLUMN_PHOTO_LOCAL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_LOCAL,
                Db.ItemsTable.COLUMN_USER_DEFINED,
                Db.SetItemsTable.COLUMN_PENDING_REMOVAL,
                Db.SetItemsTable.COLUMN_PACKED,

                Db.ItemsTable.TABLE_NAME,
                Db.SetItemsTable.TABLE_NAME,
                Db.ItemsTable.COLUMN_ID,
                Db.SetItemsTable.COLUMN_ITEM,
                Db.SetItemsTable.COLUMN_SET,
                Db.SetItemsTable.COLUMN_ITEM);

        try {
            db = getReadDB();
            cursor = db.rawQuery(query, new String[]{String.valueOf(setId), String.valueOf(itemId)});
            if (cursor.moveToNext()) {
                ret = Db.ItemsTable.parseCursor(cursor);
            }
        } catch (Exception e) {
            Log.e("Read item", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private List<Item> readItems(int setId, boolean packed) {
        List<Item> ret = new ArrayList<>(256);
        Cursor cursor = null;
        SQLiteDatabase db = null;
        Item item;
        String query = String.format(
                "SELECT a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, %s, %s FROM %s a INNER JOIN %s b ON a.%s = b.%s WHERE b.%s = ? AND b.%s <> 1 AND b.%s %s 1",

                Db.ItemsTable.COLUMN_ID,
                Db.ItemsTable.COLUMN_NAME,
                Db.ItemsTable.COLUMN_CATEGORY,
                Db.ItemsTable.COLUMN_DESCRIPTION,
                Db.ItemsTable.COLUMN_BUY_URLS,
                Db.ItemsTable.COLUMN_PHOTO_URL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_URL,
                Db.ItemsTable.COLUMN_PHOTO_LOCAL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_LOCAL,
                Db.ItemsTable.COLUMN_USER_DEFINED,
                Db.SetItemsTable.COLUMN_PENDING_REMOVAL,
                Db.SetItemsTable.COLUMN_PACKED,

                Db.ItemsTable.TABLE_NAME,
                Db.SetItemsTable.TABLE_NAME,
                Db.ItemsTable.COLUMN_ID,
                Db.SetItemsTable.COLUMN_ITEM,
                Db.SetItemsTable.COLUMN_SET,
                Db.SetItemsTable.COLUMN_DELETED,
                Db.SetItemsTable.COLUMN_PACKED,
                packed ? "=" : "<>");
        try {
            db = getReadDB();
            cursor = db.rawQuery(query, new String[]{String.valueOf(setId)});
            while (cursor.moveToNext()) {
                item = Db.ItemsTable.parseCursor(cursor);
                ret.add(item);
            }
        } catch (Exception e) {
            Log.e("Read items", e.getMessage());
            ret = null;
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private List<Item> readItemsLike(String like, int setId, boolean packed) {
        List<Item> ret = new ArrayList<>(256);
        Cursor cursor = null;
        SQLiteDatabase db = null;
        Item item;
        String query = String.format(
                "SELECT a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, a.%s, %s, %s FROM %s a INNER JOIN %s b ON a.%s = b.%s WHERE b.%s = ? AND a.%s LIKE %s AND b.%s <> 1 AND b.%s %s 1",

                Db.ItemsTable.COLUMN_ID,
                Db.ItemsTable.COLUMN_NAME,
                Db.ItemsTable.COLUMN_CATEGORY,
                Db.ItemsTable.COLUMN_DESCRIPTION,
                Db.ItemsTable.COLUMN_BUY_URLS,
                Db.ItemsTable.COLUMN_PHOTO_URL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_URL,
                Db.ItemsTable.COLUMN_PHOTO_LOCAL,
                Db.ItemsTable.COLUMN_PHOTO_THUMB_LOCAL,
                Db.ItemsTable.COLUMN_USER_DEFINED,
                Db.SetItemsTable.COLUMN_PENDING_REMOVAL,
                Db.SetItemsTable.COLUMN_PACKED,

                Db.ItemsTable.TABLE_NAME,
                Db.SetItemsTable.TABLE_NAME,
                Db.ItemsTable.COLUMN_ID,
                Db.SetItemsTable.COLUMN_ITEM,
                Db.SetItemsTable.COLUMN_SET,
                Db.ItemsTable.COLUMN_NAME,
                '\''+like+'%'+'\'',
                Db.SetItemsTable.COLUMN_DELETED,
                Db.SetItemsTable.COLUMN_PACKED,
                packed ? "=" : "<>");
        try {
            db = getReadDB();
            cursor = db.rawQuery(query, new String[]{String.valueOf(setId)});
            while (cursor.moveToNext()) {
                item = Db.ItemsTable.parseCursor(cursor);
                ret.add(item);
            }
        } catch (Exception e) {
            Log.e("Read items like", e.getMessage());
            ret = null;
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private List<Item> readItemsLike(String like, int checkThisSet) {
        List<Item> ret = new ArrayList<>(256);
        Cursor cursor = null;
        Cursor cursor2 = null;
        SQLiteDatabase db = null;
        Item item;
        String query = String.format("SELECT * FROM %s WHERE %s LIKE %s",
                Db.ItemsTable.TABLE_NAME,
                Db.ItemsTable.COLUMN_NAME,
                '\''+like+'%'+'\'');
        try {
            db = getReadDB();
            cursor = db.rawQuery(query, null);
            while (cursor.moveToNext()) {
                item = Db.ItemsTable.parseCursor(cursor);
                query = String.format("SELECT * FROM %s WHERE %s = %d AND %s = %d LIMIT 1",
                        Db.SetItemsTable.TABLE_NAME,
                        Db.SetItemsTable.COLUMN_ITEM,
                        item.getId(),
                        Db.SetItemsTable.COLUMN_SET,
                        checkThisSet
                        );
                cursor2 = db.rawQuery(query, null);
                if (cursor2.moveToNext())
                    item.InList = true;
                ret.add(item);
            }
        } catch (Exception e) {
            Log.e("Read items like", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (cursor2 != null)
                cursor2.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private Hashtable<Integer, Category> readCategories() {
        Hashtable<Integer, Category> ret = new Hashtable<>(20, 0.9f);
        Cursor cursor = null;
        SQLiteDatabase db = null;
        Category category;
        try {
            db = getReadDB();
            cursor = db.query(Db.CategoriesTable.TABLE_NAME,
                    new String[]{Db.CategoriesTable.COLUMN_ID,
                            Db.CategoriesTable.COLUMN_NAME},
                    null,null,null,null,null);
            while (cursor.moveToNext()) {
                category = Db.CategoriesTable.parseCursor(cursor);
                ret.put(category.getId(), category);
            }
        } catch (Exception e) {
            Log.e("Read categories", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private Set readSet(SQLiteDatabase db, int setId) {
        Set ret = null;
        Cursor cursor;
        String query = String.format("SELECT * FROM %s a WHERE a.%s = %d LIMIT 1",
                Db.SetsTable.TABLE_NAME, Db.SetsTable.COLUMN_ID, setId);
        cursor = db.rawQuery(query, null);
        if (cursor.moveToNext())
            ret = Db.SetsTable.parseCursor(cursor);
        if (cursor != null)
            cursor.close();
        return ret;
    }

    private Set readSet(int setId) {
        Set ret = null;
        SQLiteDatabase db = null;
        Cursor cursor = null;
        try {
            db = getReadDB();
            String query = String.format("SELECT * FROM %s a WHERE a.%s = %d LIMIT 1",
                    Db.SetsTable.TABLE_NAME, Db.SetsTable.COLUMN_ID, setId);
            cursor = db.rawQuery(query, null);
            if (cursor.moveToNext())
                ret = Db.SetsTable.parseCursor(cursor);
        } catch (Exception e) {
            Log.e("Read set", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    private List<Set> readSets() {
        List<Set> ret = new ArrayList<>(12);
        Cursor cursor = null;
        SQLiteDatabase db = null;
        Set set;
        try {
            db = getReadDB();
            String query = String.format("SELECT * FROM %s", Db.SetsTable.TABLE_NAME);
            cursor = db.rawQuery(query, null);
            while (cursor.moveToNext()) {
                set = Db.SetsTable.parseCursor(cursor);
                ret.add(set);
            }
            Collections.sort(ret);
        } catch (Exception e) {
            Log.e("Read sets", e.getMessage());
        } finally {
            if (cursor != null)
                cursor.close();
            if (db != null)
                db.close();
        }
        return ret;
    }

    // updates
    private int updateItemLocalPic(int id, String path) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        try {
            db = getWriteDB();
            db.beginTransaction();
            values = new ContentValues();
            values.put(Db.ItemsTable.COLUMN_PHOTO_LOCAL, path);
            ret = db.update(Db.ItemsTable.TABLE_NAME, values, "_id = ?",
                    new String[]{String.valueOf(id)});
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update local pic", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSharingPicLocal(int id, String picPath, String picThumbPath) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        if (picPath == null && picThumbPath == null)
            return ret;
        try {
            db = getWriteDB();
            db.beginTransaction();
            values = new ContentValues();
            if (picPath != null)
                values.put(Db.SharingPicsTable.COLUMN_PHOTO_LOCAL, picPath);
            if (picThumbPath != null)
                values.put(Db.SharingPicsTable.COLUMN_PHOTO_THUMB_LOCAL, picThumbPath);
            ret = db.update(Db.SharingPicsTable.TABLE_NAME, values, "_id = ?",
                    new String[]{String.valueOf(id)});
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update sharing pic path", e.getMessage());
            ret = -1;
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetsOrder(List<Set> reorderedSet) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        try {

            db = getWriteDB();
            db.beginTransaction();
            for (Set set : reorderedSet) {
                values = new ContentValues();
                values.put(Db.SetsTable.COLUMN_LINE_NUMBER, set.getLineNumber());
                ret += db.update(Db.SetsTable.TABLE_NAME, values, "_id = ?",
                        new String[]{String.valueOf(set.getId())});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update sets order", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetItemPendingRemoval(int setId, int itemId, boolean remove) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        Set set;
        Item item;
        try {
            db = getWriteDB();
            db.beginTransaction();
            item = readItem(db, setId, itemId);
            values = new ContentValues();
            values.put(Db.SetItemsTable.COLUMN_PENDING_REMOVAL, remove);
            ret = db.update(Db.SetItemsTable.TABLE_NAME, values, String.format("%s = ? AND %s = ?",
                    Db.SetItemsTable.COLUMN_SET, Db.SetItemsTable.COLUMN_ITEM ),
                    new String[]{String.valueOf(setId), String.valueOf(itemId)});

            set = readSet(db, setId);
            if (set != null) {
                values = new ContentValues();
                values.put(Db.SetsTable.COLUMN_ACTIVE_QTY, remove ? set.getActiveQty()-1 : set.getActiveQty()+1);
                if (item.isPacked() ) {
                    if (remove)
                        values.put(Db.SetsTable.COLUMN_PACKED_QTY, set.getPackedQty()-1);
                    else
                        values.put(Db.SetsTable.COLUMN_PACKED_QTY, set.getPackedQty()+1);
                }
                ret += db.update(Db.SetsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.SetsTable.COLUMN_ID),
                        new String[]{String.valueOf(setId)});
            }
            db.setTransactionSuccessful();

        } catch (Exception e) {
            Log.e("Update pending removal", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetItemDeleted(int setId, int itemId, boolean del) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        Set set;
        Item item;
        try {
            db = getWriteDB();
            db.beginTransaction();
            item = readItem(db, setId, itemId);
            values = new ContentValues();
            values.put(Db.SetItemsTable.COLUMN_DELETED, del);
            values.put(Db.SetItemsTable.COLUMN_PENDING_REMOVAL, false);
            values.put(Db.SetItemsTable.COLUMN_PACKED, false);
            ret = db.update(Db.SetItemsTable.TABLE_NAME, values, String.format("%s = ? AND %s = ?",
                    Db.SetItemsTable.COLUMN_SET, Db.SetItemsTable.COLUMN_ITEM ),
                    new String[]{String.valueOf(setId), String.valueOf(itemId)});

            set = readSet(db, setId);
            if (set != null) {
                values = new ContentValues();
                if (del) {
                    if (!item.isPendingRemoval())
                        values.put(Db.SetsTable.COLUMN_ACTIVE_QTY, set.getActiveQty()-1);
                } else
                    values.put(Db.SetsTable.COLUMN_ACTIVE_QTY, set.getActiveQty()+1);

                values.put(Db.SetsTable.COLUMN_PACKED_QTY, item.isPacked() ? set.getPackedQty()-1 :set.getPackedQty());
                ret += db.update(Db.SetsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.SetsTable.COLUMN_ID),
                        new String[]{String.valueOf(setId)});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update item deleted", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetActiveQty(int setId, int updateOn) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        Set set;
        try {
            db = getWriteDB();
            db.beginTransaction();
            set = readSet(db, setId);
            if (set != null) {
                values = new ContentValues();

                values.put(Db.SetsTable.COLUMN_ACTIVE_QTY, set.getActiveQty()+updateOn);

                ret += db.update(Db.SetsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.SetsTable.COLUMN_ID),
                        new String[]{String.valueOf(setId)});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update set active", e.getMessage());
            ret = -1;
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetItemsCleanPacked(int setId) {
        int  ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        try {
            db = getWriteDB();
            db.beginTransaction();
            values = new ContentValues();
            values.put(Db.SetItemsTable.COLUMN_PACKED, 0);
            ret = db.update(Db.SetItemsTable.TABLE_NAME, values, String.format("%s = ? AND %s = ?",
                    Db.SetItemsTable.COLUMN_SET,
                    Db.SetItemsTable.COLUMN_PENDING_REMOVAL),
                    new String[]{String.valueOf(setId), "1"});
            db.setTransactionSuccessful();
        } catch (Exception e) {
            ret = -1;
            Log.e("Clean packed", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetItemsPacked(int setId, boolean pack) {
        int  ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        Set set;
        try {
            db = getWriteDB();
            db.beginTransaction();
            values = new ContentValues();
            values.put(Db.SetItemsTable.COLUMN_PACKED, pack);
            ret = db.update(Db.SetItemsTable.TABLE_NAME, values, String.format("%s = ?",
                    Db.SetItemsTable.COLUMN_SET),
                    new String[]{String.valueOf(setId)});

            set = readSet(db, setId);
            if (set != null) {
                values = new ContentValues();
                values.put(Db.SetsTable.COLUMN_PACKED_QTY, pack ? set.getActiveQty() : 0);
                ret += db.update(Db.SetsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.SetsTable.COLUMN_ID),
                        new String[]{String.valueOf(setId)});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update item packed", e.getMessage());
            ret = -1;
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetItemPacked(int setId, int itemId, boolean pack) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        Set set;
        Item item;
        try {
            db = getWriteDB();
            db.beginTransaction();
            item = readItem(db, setId, itemId);
            values = new ContentValues();
            values.put(Db.SetItemsTable.COLUMN_PACKED, pack);
            values.put(Db.SetItemsTable.COLUMN_PENDING_REMOVAL, 0);
            ret = db.update(Db.SetItemsTable.TABLE_NAME, values, String.format("%s = ? AND %s = ?",
                    Db.SetItemsTable.COLUMN_SET, Db.SetItemsTable.COLUMN_ITEM ),
                    new String[]{String.valueOf(setId), String.valueOf(itemId)});

            set = readSet(db, setId);
            if (set != null) {
                values = new ContentValues();
                if (pack) {
                    values.put(Db.SetsTable.COLUMN_PACKED_QTY, set.getPackedQty() + 1);
                    if (item.isPendingRemoval())
                        values.put(Db.SetsTable.COLUMN_ACTIVE_QTY, set.getActiveQty()+1 );
                } else {
                    if (!item.isPendingRemoval()) {
                        values.put(Db.SetsTable.COLUMN_PACKED_QTY, set.getPackedQty() - 1);
                    } else {
                        values.put(Db.SetsTable.COLUMN_ACTIVE_QTY, set.getActiveQty()+1);
                    }
                }

                ret += db.update(Db.SetsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.SetsTable.COLUMN_ID),
                        new String[]{String.valueOf(setId)});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update item packed", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetRestoreDefault(int setId) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;
        Set set;
        try {
            db = getWriteDB();
            db.beginTransaction();
            values = new ContentValues();
            values.put(Db.SetItemsTable.COLUMN_PACKED, false);
            values.put(Db.SetItemsTable.COLUMN_DELETED, false);
            values.put(Db.SetItemsTable.COLUMN_PENDING_REMOVAL, false);
            ret = db.update(Db.SetItemsTable.TABLE_NAME,
                    values,
                    String.format("%s = ?",
                    Db.SetItemsTable.COLUMN_SET),
                    new String[]{String.valueOf(setId)});

            set = readSet(db, setId);
            if (set != null) {
                values = new ContentValues();
                values.put(Db.SetsTable.COLUMN_PACKED_QTY, 0);
                values.put(Db.SetsTable.COLUMN_ACTIVE_QTY, set.getItems().size());
                ret += db.update(Db.SetsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.SetsTable.COLUMN_ID),
                        new String[]{String.valueOf(setId)});
            }
            ret += db.delete(Db.SetItemsTable.TABLE_NAME,
                    String.format("%s = ? AND %s = ?", Db.SetItemsTable.COLUMN_SET, Db.SetItemsTable.COLUMN_USER_DEFINED),
                    new String[]{String.valueOf(setId), String.valueOf(1)});
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Restore defaults", e.getMessage());
            ret = -1;
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateSetNames(List<Set> sets) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;

        try {
            db = getWriteDB();
            db.beginTransaction();
            for (Set set : sets) {
                values = new ContentValues();
                values.put(Db.SetsTable.COLUMN_NAME, set.getName());
                ret += db.update(Db.SetsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.SetsTable.COLUMN_ID),
                        new String[]{String.valueOf(set.getId())});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update set names", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateCategoryNames(List<Category> categories) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;

        try {
            db = getWriteDB();
            db.beginTransaction();
            for (Category category : categories) {
                values = new ContentValues();
                values.put(Db.CategoriesTable.COLUMN_NAME, category.getName());
                ret += db.update(Db.CategoriesTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.CategoriesTable.COLUMN_ID),
                        new String[]{String.valueOf(category.getId())});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update category names", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateItemNames(List<Item> items) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;

        try {
            db = getWriteDB();
            db.beginTransaction();
            for (Item item : items) {
                values = new ContentValues();
                values.put(Db.ItemsTable.COLUMN_NAME, item.getName());
                values.put(Db.ItemsTable.COLUMN_DESCRIPTION, item.getDescription());
                ret += db.update(Db.ItemsTable.TABLE_NAME, values, String.format("%s = ?",
                        Db.ItemsTable.COLUMN_ID),
                        new String[]{String.valueOf(item.getId())});
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update item names", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }

    private int updateLogLocale(int id, String newLocale) {
        int ret = 0;
        SQLiteDatabase db = null;
        ContentValues values;

        try {
            db = getWriteDB();
            db.beginTransaction();

            values = new ContentValues();
            values.put(Db.LogTable.COLUMN_LOCALE, newLocale);

            ret = db.update(Db.LogTable.TABLE_NAME, values, String.format("%s = ?",
                    Db.ItemsTable.COLUMN_ID),
                    new String[]{String.valueOf(id)});

            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e("Update log locale", e.getMessage());
        } finally {
            if (db != null) {
                db.endTransaction();
                db.close();
            }
        }
        return ret;
    }
    //endregion

    ///////////////////////  TASK CLASSES //////////////////

    //region Task classes

    // ITEM CLASS
    private class ItemTask implements Runnable {
        int currentCommand;
        int priority;
        int setId;
        int itemId;
        Item item;
        String query;

        public ItemTask(int command, int priority) {
            this.currentCommand = command;
            this.setId          = -1;
            this.itemId         = -1;
            this.priority       = priority;
        }

        @Override
        public void run() {
            android.os.Process.setThreadPriority(priority);
            Message message = Message.obtain();
            switch (currentCommand) {

                case Command.ITEM_READ:
                    message.obj = readItem(setId, itemId);
                    if (message.obj != null)
                        message.what = Event.ITEM_READ_COMPLETED;
                    else
                        message.what = Event.ITEM_READ_ERROR;
                    message.arg1 = setId;
                    message.arg2 = itemId;
                    break;

                case Command.ITEM_DELETE_FROM_SET :
                    message.arg1 = updateSetItemDeleted(setId, itemId, true);
                    if (message.arg1 > 0)
                        message.what = Event.ITEM_FROM_SET_DELETED;
                    else
                        message.what = Event.ITEM_FROM_SET_ERROR;
                    break;

                case Command.ITEM_PENDING_REMOVAL:
                    message.arg1 = updateSetItemPendingRemoval(setId, itemId, true);
                    if (message.arg1 > 0)
                        message.what = Event.ITEM_PENDING_REMOVAL_COMPLETED;
                    else
                        message.what = Event.ITEM_PENDING_REMOVAL_ERROR;
                    message.arg1 = setId;
                    message.arg2 = itemId;
                    break;

                case Command.ITEM_REMOVAL_CANCEL:
                    message.arg1 = updateSetItemPendingRemoval(setId, itemId, false);
                    if (message.arg1 > 0)
                        message.what = Event.ITEM_REMOVAL_CANCELED;
                    else
                        message.what = Event.ITEM_REMOVAL_CANCEL_ERROR;
                    message.arg1 = setId;
                    message.arg2 = itemId;
                    break;

                case Command.ITEM_INSERT :
                    if (insertItem(item) > 0) {
                        if (insertSetItem(setId, item.getId(), item.isUserDefined()) > 0) {
                            updateSetActiveQty(setId, 1);
                            message.what = Event.ITEM_INSERTED;
                            message.arg1 = setId;
                            message.arg2 = item.getId();
                            AppRater.updateSharedPreferences(1);
                            if (AppRater.checkRatePrompt()) {
                                Message rateEvent = Message.obtain();
                                rateEvent.what = Event.NEED_TO_RATE_NOW;
                                handler.sendMessage(rateEvent);
                            }
                        }
                    } else {
                        message.what = Event.ITEM_INSERT_ERROR;
                    }
                    break;

                case Command.ITEM_PACK :
                    message.arg1 = updateSetItemPacked(setId, itemId, true);
                    if (message.arg1 > 0)
                        message.what = Event.ITEM_PACKED;
                    else
                        message.what = Event.ITEM_PACK_ERROR;
                    message.arg1 = setId;
                    message.arg2 = itemId;
                    break;

                case Command.ITEM_UNPACK :
                    message.arg1 = updateSetItemPacked(setId, itemId, false);
                    if (message.arg1 > 0)
                        message.what = Event.ITEM_UNPACKED;
                    else
                        message.what = Event.ITEM_UNPACK_ERROR;
                    message.arg1 = setId;
                    message.arg2 = itemId;
                    break;

                case Command.ITEM_GET_CATEGORIES :
                    Hashtable<Integer, Category> res = readCategories();
                    if (res.isEmpty())
                        message.what = Event.ITEM_CATEGORY_LOAD_ERROR;
                    else {
                        message.what = Event.ITEM_CATEGORY_LOAD_COMPLETED;
                        message.obj = res;
                    }
                    break;

                case Command.ITEM_GET_LIKE :
                    List<Item> itemsLike = readItemsLike(query, setId);
                    if (itemsLike.isEmpty()) {
                        message.what = Event.ITEM_LIKE_LOAD_ERROR;
                    } else {
                        message.what = Event.ITEM_LIKE_LOAD_COMPLETED;
                        message.obj = itemsLike;
                    }
                    break;
            }
            handler.sendMessage(message);
        }
    }
    // SET CLASS
    private class SetTask implements Runnable {
        int currentCommand;
        int priority;
        int setId;
        String query;
        List<Set> setsToUpdate;
        List<Item> items;

        public SetTask(int command, int priority) {
            this.currentCommand = command;
            this.setId          = -1;
            this.setsToUpdate   = new ArrayList<>();
            this.priority       = priority;
        }

        @Override
        public void run() {
            android.os.Process.setThreadPriority(priority);
            Message message = Message.obtain();
            switch (currentCommand) {

                case Command.SET_GET_ALL:
                    List<Set> sets = readSets();
                    if (sets.isEmpty())
                        message.what = Event.SET_LOAD_ERROR;
                    else {
                        message.what = Event.SET_LOAD_COMPLETED;
                        message.obj = sets;
                    }
                    break;

                case Command.SET_GET_ITEMS:
                    items = readItems(setId, false);
                    if (items == null)
                        message.what = Event.SET_ITEMS_LOAD_ERROR;
                    else {
                        Collections.sort(items);
                        message.what = Event.SET_ITEMS_LOAD_COMPLETED;
                        message.obj = items;
                        message.arg1 = setId;
                    }
                    break;

                case Command.SET_GET_ITEMS_LIKE:
                    items = readItemsLike(query, setId, false);
                    if (items == null)
                        message.what = Event.SET_ITEMS_LIKE_ERROR;
                    else {
                        Collections.sort(items);
                        message.what = Event.SET_ITEMS_LIKE_COMPLETED;
                        message.obj = items;
                        message.arg1 = setId;
                    }
                    break;

                case Command.SET_REORDER:
                    message.arg1 = updateSetsOrder(setsToUpdate);
                    if (message.arg1 > 0)
                        message.what = Event.SET_REORDER_COMPLETED;
                    else
                        message.what = Event.SET_REORDER_ERROR;
                    break;

                case Command.SET_GET_PACKED_ITEMS :
                    items = readItems(setId, true);
                    if (items == null)
                        message.what = Event.SET_PACKED_LOAD_ERROR;
                    else {
                        Collections.sort(items);
                        message.what = Event.SET_PACKED_LOAD_COMPLETED;
                        message.obj = items;
                        message.arg1 = setId;
                    }
                    break;

                case Command.SET_GET_ITEMS_LIKE_PACKED:
                    items = readItemsLike(query, setId, true);
                    if (items == null)
                        message.what = Event.SET_ITEMS_LIKE_PACKED_ERROR;
                    else {
                        Collections.sort(items);
                        message.what = Event.SET_ITEMS_LIKE_PACKED_COMPLETED;
                        message.obj = items;
                        message.arg1 = setId;
                    }
                    break;

                case Command.SET_UNPACK_ITEMS :
                    message.arg1 = updateSetItemsPacked(setId, false);
                    message.arg2 = setId;
                    if (message.arg1 > 0)
                        message.what = Event.SET_UNPACK_COMPLETED;
                    else
                        message.what = Event.SET_UNPACK_ERROR;
                    break;

                case Command.SET_RESTORE_DEFAULT :
                    message.arg1 = updateSetRestoreDefault(setId);
                    if (message.arg1 > 0)
                        message.what = Event.SET_RESTORE_COMPLETED;
                    else
                        message.what = Event.SET_RESTORE_ERROR;
                    break;

                case Command.SET_GET_STAT:
                    Set set = readSet(setId);
                    message.arg1 = set.getActiveQty();
                    message.arg2 = set.getPackedQty();
                    message.what = Event.SET_GET_STAT_COMPLETED;
                    break;

                case Command.SET_CLEAN_PACKED:
                    message.arg1 = updateSetItemsCleanPacked(setId);
                    if (message.arg1 > -1)
                        message.what = Event.SET_PACKED_CLEANED;
                    else
                        message.what = Event.SET_CLEAN_PACKED_ERROR;
                    break;
            }
            handler.sendMessage(message);
        }
    }
    // SYNC CLASS
    private class SyncTask implements Runnable {
        int currentCommand;
        int priority;
        int statusCode;
        String locale;

        public SyncTask(int command, int priority) {
            this.currentCommand = command;
            this.priority       = priority;
            this.locale         = Locale.getDefault().getLanguage();
        }

        @Override
        public void run() {
            android.os.Process.setThreadPriority(priority);
            Message message = Message.obtain();

            switch (currentCommand) {
                case Command.SYNC:
                    try {
                        UpdateLog log = readLastLog();
                        if (log != null) {
                            if (log.getLocale().equals(locale))
                                message.what = Event.SYNC_COMPLETED;
                            else {
                                if (!NetworkUtil.isInternetAvailable()) {
                                    message.what = Event.SYNC_FAILED;
                                } else {
                                    Response<List<Set>> response0 = api.getSets(locale).execute();
                                    updateSetNames(response0.body());
                                    statusCode = response0.code();
                                    Response<List<Category>> response1 = api.getItemCategories(locale).execute();
                                    updateCategoryNames(response1.body());
                                    statusCode = response1.code();
                                    Response<List<Item>> response2 = api.getItems(locale).execute();
                                    updateItemNames(response2.body());
                                    statusCode = response2.code();
                                    updateLogLocale(log.getId(), locale);
                                    message.what = Event.SYNC_COMPLETED;
                                }
                            }

                        } else { // for the first time
                            if (!NetworkUtil.isInternetAvailable()) {
                                message.what = Event.SYNC_NO_CONNECTION;
                            } else {
                                Response<List<Set>> response0 = api.getSets(locale).execute();
                                insertSets(response0.body());
                                statusCode = response0.code();
                                Response<List<Category>> response1 = api.getItemCategories(locale).execute();
                                insertCategories(response1.body());
                                statusCode = response1.code();
                                Response<List<Item>> response2 = api.getItems(locale).execute();
                                insertItems(response2.body());
                                statusCode = response2.code();
                                Response<Timestamp> response3 = api.getTimestamp().execute();
                                insertTimestamp(response3.body());
                                statusCode = response3.code();
                                Response<List<SharingPicture>> response4 = api.getSharingPicture().execute();
                                insertSharingPic(response4.body().get(0));
                                statusCode = response4.code();

                                message.what = Event.SYNC_COMPLETED;
                            }
                        }

                    } catch (Exception e) {
                        message.what = Event.SYNC_FAILED;
                    }
                    finally {
                        message.arg1 = statusCode;
                        handler.sendMessage(message);
                    }
                    break;

                case Command.SYNC_READ_FROM_FILE:
                    try {
                        String sets = readSetsFromFile();
                        String categories = readCategoriesFromFile();
                        String items = readItemsFromFile();
                        String pic = readSharingPicFromFile();

                        if (sets != null && categories != null && items != null && pic != null) {
                            Gson gson = new Gson();
                            Type type = new TypeToken<List<Set>>() {
                            }.getType();
                            List<Set> setsFromJson = gson.fromJson(sets, type);
                            insertSets(setsFromJson);
                            type = new TypeToken<List<Category>>() {
                            }.getType();
                            List<Category> categoriesFromJson = gson.fromJson(categories, type);
                            insertCategories(categoriesFromJson);
                            type = new TypeToken<List<Item>>() {
                            }.getType();
                            List<Item> itemsFromJson = gson.fromJson(items, type);
                            insertItems(itemsFromJson);
                            insertTimestamp(new Timestamp(1456083374));
                            type = new TypeToken<List<SharingPicture>>() {
                            }.getType();
                            List<SharingPicture> picFromJson = gson.fromJson(pic, type);
                            if (picFromJson != null) {
                                for (SharingPicture sharingPicture : picFromJson) {
                                    insertSharingPic(sharingPicture);
                                }
                            }
                            message.what = Event.SYNC_COMPLETED;
                        } else {
                            message.what = Event.SYNC_FAILED;
                        }
                    } catch (Exception ex) {
                        Log.e("Read from file", ex.getMessage());
                        message.what = Event.SYNC_FAILED;
                    } finally {
                        message.arg1 = 200;
                        handler.sendMessage(message);
                    }
                    break;

                case Command.SYNC_IF_NOT_EXISTS:
                    if (logExist()) {
                        message.what = Event.SYNC_COMPLETED;
                    } else {
                        try {
                            if (!NetworkUtil.isInternetAvailable()) {
                                message.what = Event.SYNC_NO_CONNECTION;
                            } else {
                                Response<List<Set>> response0 = api.getSets(locale).execute();
                                insertSets(response0.body());
                                statusCode = response0.code();
                                Response<List<Category>> response1 = api.getItemCategories(locale).execute();
                                insertCategories(response1.body());
                                statusCode = response1.code();
                                Response<List<Item>> response2 = api.getItems(locale).execute();
                                insertItems(response2.body());
                                statusCode = response2.code();
                                Response<Timestamp> response3 = api.getTimestamp().execute();
                                insertTimestamp(response3.body());
                                statusCode = response3.code();
                                message.what = Event.SYNC_COMPLETED;
                            }
                        } catch (IOException e ){
                            message.what = Event.SYNC_FAILED;
                        } finally {
                            message.arg1 = statusCode;
                            handler.sendMessage(message);
                        }
                    }
                    break;
            }
        }
    }
    // IMAGE PROVIDER CLASS
    private class ImageProviderTask implements Runnable {
        int currentCommand;
        int priority;
        Item item;


        public ImageProviderTask(int command, int priority) {
            this.currentCommand = command;
            this.priority       = priority;
        }

        @Override
        public void run() {
            android.os.Process.setThreadPriority(priority);
            Message message = Message.obtain();
            switch (currentCommand) {

                case Command.ITEM_GET_IMAGE :
                    try {
                        Bitmap bitmap = loadItemImage(item);
                        if (bitmap != null) {
                            message.what = Event.ITEM_IMAGE_LOAD_COMPLETED;
                            message.obj = bitmap;
                        }
                        else
                            message.what = Event.ITEM_IMAGE_LOAD_ERROR;
                    } catch (Exception e) {
                        message.what = Event.ITEM_IMAGE_LOAD_ERROR;
                    }
                    break;

                case Command.SHARING_GET_PIC:
                    try {
                        SharingPicture sharingPicture = readLastSharingPic();
                        if (sharingPicture != null) {
                            Bitmap[] ret = null;
                            Bitmap bitmap1 = loadSharingImage(sharingPicture);
                            Bitmap bitmap2 = loadSharingThumbImage(sharingPicture);

                            if (bitmap1 != null || bitmap2 != null) {
                                ret = new Bitmap[2];
                                ret[0] = bitmap1;
                                ret[1] = bitmap2;
                                message.obj = ret;
                                message.what = Event.SHARING_PIC_LOADED;
                            } else
                                message.what = Event.SHARING_GET_PIC_ERROR;

                        } else
                            message.what = Event.SHARING_GET_PIC_ERROR;

                    } catch (Exception e) {
                        message.what = Event.SHARING_GET_PIC_ERROR;
                    }
                    break;
            }
            handler.sendMessage(message);
        }
    }
    //endregion

    private Bitmap loadItemImage(Item item) {
        ImageDownloadHelper downloadHelper = new ImageDownloadHelper();
        Bitmap bitmap   = null;
        String filename = null;

        if (item != null) {
            if (!TextUtils.isEmpty(item.getPhotoLocal()))
                bitmap = BitmapFactory.decodeFile(item.getPhotoLocal());
            // cannot retrieve, download and save then
            if (bitmap == null) { // return it
                bitmap = downloadHelper.loadImage(item.getPhotoUrl());
                if (bitmap != null) {
                    if (downloadHelper.isExternalStorageWritable()) {
                        filename = downloadHelper.saveImageExternal(
                                downloadHelper.generateFileName(item.getPhotoUrl()), bitmap);
                    } else {
                        filename = downloadHelper.saveImageInternal(
                                downloadHelper.generateFileName(item.getPhotoUrl()), bitmap);
                    }
                    updateItemLocalPic(item.getId(), filename);
                }
            }
        }
        return bitmap;
    }

    private Bitmap loadSharingImage(SharingPicture sharingPicture) {
        ImageDownloadHelper downloadHelper = new ImageDownloadHelper();
        Bitmap bitmap   = null;
        String filename = null;

        if (sharingPicture != null) {
            if (!TextUtils.isEmpty(sharingPicture.getPhotoLocal()))
                bitmap = BitmapFactory.decodeFile(sharingPicture.getPhotoLocal());
            // cannot retrieve, download and save then
            if (bitmap == null) { // return it
                bitmap = downloadHelper.loadImage(sharingPicture.getPhotoUrl());
                if (bitmap != null) {
                    if (downloadHelper.isExternalStorageWritable()) {
                        filename = downloadHelper.saveImageExternal("sharing_pic", bitmap);
                    } else {
                        filename = downloadHelper.saveImageInternal("sharing_pic", bitmap);
                    }
                    updateSharingPicLocal(sharingPicture.getId(), filename, null);
                }
            }
            if (bitmap == null) {
                bitmap = BitmapFactory.decodeResource(context.getResources(),
                        R.drawable.sharing_pic_default);
            }
        }
        return bitmap;
    }

    private Bitmap loadSharingThumbImage(SharingPicture sharingPicture) {
        ImageDownloadHelper downloadHelper = new ImageDownloadHelper();
        Bitmap bitmap   = null;
        String filename = null;

        if (sharingPicture != null) {
            if (!TextUtils.isEmpty(sharingPicture.getPhotoThumbLocal()))
                bitmap = BitmapFactory.decodeFile(sharingPicture.getPhotoThumbLocal());
            // cannot retrieve, download and save then
            if (bitmap == null) { // return it
                bitmap = downloadHelper.loadImage(sharingPicture.getPhotoThumbUrl());
                if (bitmap != null) {
                    if (downloadHelper.isExternalStorageWritable()) {
                        filename = downloadHelper.saveImageExternal("sharing_pic_thumb", bitmap);
                    } else {
                        filename = downloadHelper.saveImageInternal("sharing_pic_thumb", bitmap);
                    }
                    updateSharingPicLocal(sharingPicture.getId(), null, filename);
                }
            }
            if (bitmap == null) {
                bitmap = BitmapFactory.decodeResource(context.getResources(),
                        R.drawable.sharing_pic_thumb_default);
            }
        }
        return bitmap;
    }

    private String readSetsFromFile() {
        String sets = null;
        try {
            InputStream is = App.getAppContext().getAssets().open("jsons/sets.json");
            int size = is.available();
            byte[] buffer = new byte[size];
            is.read(buffer);
            is.close();
            sets = new String(buffer, "UTF-8");
        } catch (IOException ex) {
            Log.e("Reading json", ex.getMessage());
        }
        return sets;
    }

    private String readCategoriesFromFile() {
        String categories = null;
        try {
            InputStream is = App.getAppContext().getAssets().open("jsons/item_categories.json");
            int size = is.available();
            byte[] buffer = new byte[size];
            is.read(buffer);
            is.close();
            categories = new String(buffer, "UTF-8");
        } catch (IOException ex) {
            Log.e("Reading json", ex.getMessage());
        }
        return categories;
    }

    private String readItemsFromFile() {
        String items = null;
        try {
            InputStream is = App.getAppContext().getAssets().open("jsons/items.json");
            int size = is.available();
            byte[] buffer = new byte[size];
            is.read(buffer);
            is.close();
            items = new String(buffer, "UTF-8");
        } catch (IOException ex) {
            Log.e("Reading json", ex.getMessage());
        }
        return items;
    }

    private String readSharingPicFromFile() {
        String sharingPic = null;
        try {
            InputStream is = App.getAppContext().getAssets().open("jsons/sharing_photos.json");
            int size = is.available();
            byte[] buffer = new byte[size];
            is.read(buffer);
            is.close();
            sharingPic = new String(buffer, "UTF-8");
        } catch (IOException ex) {
            Log.e("Reading json", ex.getMessage());
        }
        return sharingPic;
    }
}
