/*
 * Copyright (c) 2014,KJFrameForAndroid Open Source Project,张涛.
 *
 * 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.kymjs.kjframe;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import org.kymjs.kjframe.database.CursorHelper;
import org.kymjs.kjframe.database.DaoConfig;
import org.kymjs.kjframe.database.DbModel;
import org.kymjs.kjframe.database.OneToManyLazyLoader;
import org.kymjs.kjframe.database.SqlBuilder;
import org.kymjs.kjframe.database.SqlInfo;
import org.kymjs.kjframe.database.utils.KeyValue;
import org.kymjs.kjframe.database.utils.ManyToOne;
import org.kymjs.kjframe.database.utils.OneToMany;
import org.kymjs.kjframe.database.utils.TableInfo;
import org.kymjs.kjframe.ui.KJActivityStack;
import org.kymjs.kjframe.utils.KJLoger;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * The DBLibrary's core classes<br>
 * 
 * <b>创建时间</b> 2014-8-15<br>
 * 
 * @author kymjs (https://github.com/kymjs)
 * @version 1.1
 */
public class KJDB {

    /** 用于保存所有数据库 */
    private static HashMap<String, KJDB> daoMap = new HashMap<String, KJDB>();

    private SQLiteDatabase db;
    private final DaoConfig config;

    private KJDB(DaoConfig config) {
        if (config == null) {
            throw new RuntimeException("daoConfig is null");
        }
        if (config.getContext() == null) {
            throw new RuntimeException("android context is null");
        }
        if (config.getTargetDirectory() != null
                && config.getTargetDirectory().trim().length() > 0) {
            this.db = createDbFileOnSDCard(config.getTargetDirectory(),
                    config.getDbName());
        } else {
            this.db = new SqliteDbHelper(config.getContext()
                    .getApplicationContext(), config.getDbName(),
                    config.getDbVersion(), config.getDbUpdateListener())
                    .getWritableDatabase();
        }
        this.config = config;
    }

    private synchronized static KJDB getInstance(DaoConfig daoConfig) {
        KJDB dao = daoMap.get(daoConfig.getDbName());
        if (dao == null) {
            dao = new KJDB(daoConfig);
            daoMap.put(daoConfig.getDbName(), dao);
        }
        return dao;
    }

    /**
     * 创建DBLibrary
     */
    public static KJDB create() {
        return create(false);
    }

    /**
     * 创建DBLibrary
     * 
     * @param isDebug
     *            是否是debug模式(debug模式进行数据库操作的时候将会打印sql语句)
     */
    public static KJDB create(boolean isDebug) {
        Context cxt = KJActivityStack.create().topActivity();
        if (cxt == null) {
            throw new NullPointerException("you have to extends KJActivity");
        }
        return create(cxt, isDebug);
    }

    /**
     * 创建DBLibrary
     * 
     * @param context
     */
    public static KJDB create(Context context) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        return create(config);
    }

    /**
     * 创建DBLibrary
     * 
     * @param context
     * @param isDebug
     *            是否是debug模式(debug模式进行数据库操作的时候将会打印sql语句)
     */
    public static KJDB create(Context context, boolean isDebug) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        config.setDebug(isDebug);
        return create(config);
    }

    /**
     * 创建DBLibrary
     * 
     * @param context
     * @param dbName
     *            数据库名称
     */
    public static KJDB create(Context context, String dbName) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        config.setDbName(dbName);
        return create(config);
    }

    /**
     * 创建 DBLibrary
     * 
     * @param context
     * @param dbName
     *            数据库名称
     * @param isDebug
     *            是否为debug模式(debug模式进行数据库操作的时候将会打印sql语句)
     */
    public static KJDB create(Context context, String dbName, boolean isDebug) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        config.setDbName(dbName);
        config.setDebug(isDebug);
        return create(config);
    }

    /**
     * 创建DBLibrary
     * 
     * @param context
     * @param dbName
     *            数据库名称
     */
    public static KJDB create(Context context, String targetDirectory,
            String dbName) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        config.setDbName(dbName);
        config.setTargetDirectory(targetDirectory);
        return create(config);
    }

    /**
     * 创建 DBLibrary
     * 
     * @param context
     * @param dbName
     *            数据库名称
     * @param isDebug
     *            是否为debug模式(debug模式进行数据库操作的时候将会打印sql语句)
     */
    public static KJDB create(Context context, String targetDirectory,
            String dbName, boolean isDebug) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        config.setTargetDirectory(targetDirectory);
        config.setDbName(dbName);
        config.setDebug(isDebug);
        return create(config);
    }

    /**
     * 创建 DBLibrary
     * 
     * @param context
     *            上下文
     * @param dbName
     *            数据库名字
     * @param isDebug
     *            是否是调试模式:调试模式会log出sql信息
     * @param dbVersion
     *            数据库版本信息
     * @param dbUpdateListener
     *            数据库升级监听器:如果监听器为null,升级的时候将会清空所所有的数据
     * @return
     */
    public static KJDB create(Context context, String dbName, boolean isDebug,
            int dbVersion, DbUpdateListener dbUpdateListener) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        config.setDbName(dbName);
        config.setDebug(isDebug);
        config.setDbVersion(dbVersion);
        config.setDbUpdateListener(dbUpdateListener);
        return create(config);
    }

    /**
     * 标准创建器,创建DBLibrary
     * 
     * @param context
     *            上下文
     * @param targetDirectory
     *            db文件路径,可以配置为sdcard的路径
     * @param dbName
     *            数据库名字
     * @param isDebug
     *            是否是调试模式,调试模式会log出sql信息
     * @param dbVersion
     *            数据库版本信息
     * @param dbUpdateListener
     *            数据库升级监听器,如果监听器为null,升级的时候将会清空所所有的数据
     * @return
     */
    public static KJDB create(Context context, String targetDirectory,
            String dbName, boolean isDebug, int dbVersion,
            DbUpdateListener dbUpdateListener) {
        DaoConfig config = new DaoConfig();
        config.setContext(context);
        config.setTargetDirectory(targetDirectory);
        config.setDbName(dbName);
        config.setDebug(isDebug);
        config.setDbVersion(dbVersion);
        config.setDbUpdateListener(dbUpdateListener);
        return create(config);
    }

    /**
     * 创建 DBLibrary
     * 
     * @param daoConfig
     * @return
     */
    public static KJDB create(DaoConfig daoConfig) {
        return getInstance(daoConfig);
    }

    /**
     * 保存入数据库
     * 
     * @param entity
     */
    public void save(Object entity) {
        checkTableExist(entity.getClass());
        exeSqlInfo(SqlBuilder.buildInsertSql(entity));
    }

    /**
     * 保存入数据库
     * 
     * @param entities
     */
    public void save(List<? extends Object> entities) {
        if (entities != null) {
            for (Object t : entities) {
                save(t);
            }
        }
    }

    /**
     * 保存数据到数据库<br>
     * <b>注意:</b> <br>
     * 保存成功后,entity的主键将被赋值(或更新)为数据库的主键, 只针对自增长的id有效
     * 
     * @param entity
     *            要保存的数据
     * @return ture: 保存成功 false:保存失败
     */
    public boolean saveBindId(Object entity) {
        checkTableExist(entity.getClass());
        List<KeyValue> entityKvList = SqlBuilder
                .getSaveKeyValueListByEntity(entity);
        if (entityKvList != null && entityKvList.size() > 0) {
            TableInfo tf = TableInfo.get(entity.getClass());
            ContentValues cv = new ContentValues();
            insertContentValues(entityKvList, cv);
            Long id = db.insert(tf.getTableName(), null, cv);
            if (id == -1)
                return false;
            tf.getId().setValue(entity, id);
            return true;
        }
        return false;
    }

    /**
     * 把List<KeyValue>数据存储到ContentValues
     * 
     * @param list
     * @param cv
     */
    private void insertContentValues(List<KeyValue> list, ContentValues cv) {
        if (list != null && cv != null) {
            for (KeyValue kv : list) {
                cv.put(kv.getKey(), kv.getValue().toString());
            }
        } else {
            KJLoger.debug(getClass().getName()
                    + "insertContentValues: List<KeyValue> is empty or ContentValues is empty!");
        }

    }

    /**
     * 更新数据 (主键ID必须不能为空)
     * 
     * @param entity
     */
    public void update(Object entity) {
        checkTableExist(entity.getClass());
        exeSqlInfo(SqlBuilder.getUpdateSqlAsSqlInfo(entity));
    }

    /**
     * 根据条件更新数据
     * 
     * @param entity
     * @param strWhere
     *            :strWhere表示sql语句update xxx from xxx where后的语句,
     *            条件为空的时候,将会更新所有的数据
     */
    public void update(Object entity, String strWhere) {
        checkTableExist(entity.getClass());
        exeSqlInfo(SqlBuilder.getUpdateSqlAsSqlInfo(entity, strWhere));
    }

    /**
     * 删除数据
     * 
     * @param entity
     *            entity的主键不能为空
     */
    public void delete(Object entity) {
        checkTableExist(entity.getClass());
        exeSqlInfo(SqlBuilder.buildDeleteSql(entity));
    }

    /**
     * 根据主键删除数据
     * 
     * @param clazz
     *            要删除的实体类
     * @param id
     *            主键值
     */
    public void deleteById(Class<?> clazz, Object id) {
        checkTableExist(clazz);
        exeSqlInfo(SqlBuilder.buildDeleteSql(clazz, id));
    }

    /**
     * 根据条件删除数据
     * 
     * @param clazz
     * @param strWhere
     *            :strWhere表示sql语句后delete xxx from xxx where的语句,条件为空的时候
     *            将会删除所有的数据
     */
    public void deleteByWhere(Class<?> clazz, String strWhere) {
        checkTableExist(clazz);
        String sql = SqlBuilder.buildDeleteSql(clazz, strWhere);
        debugSql(sql);
        db.execSQL(sql);
    }

    /**
     * 删除所有数据表
     */
    public void dropDb() {
        Cursor cursor = db.rawQuery(
                "SELECT name FROM sqlite_master WHERE type ='table'", null);
        if (cursor != null) {
            while (cursor.moveToNext()) {
                // 添加异常捕获.忽略删除所有表时出现的异常:
                // table sqlite_sequence may not be dropped
                try {
                    db.execSQL("DROP TABLE " + cursor.getString(0));
                } catch (SQLException e) {
                    KJLoger.debug(getClass().getName() + e.getMessage());
                }
            }
        }
        if (cursor != null) {
            cursor.close();
            cursor = null;
        }
    }

    private void exeSqlInfo(SqlInfo sqlInfo) {
        if (sqlInfo != null) {
            debugSql(sqlInfo.getSql());
            db.execSQL(sqlInfo.getSql(), sqlInfo.getBindArgsAsArray());
        } else {
            KJLoger.debug(getClass().getName() + "sava error:sqlInfo is null");
        }
    }

    /**
     * 根据主键查找数据(默认不查询多对一或者一对多的关联数据)
     * 
     * @param id
     * @param clazz
     */
    public <T> T findById(Object id, Class<T> clazz) {
        checkTableExist(clazz);
        SqlInfo sqlInfo = SqlBuilder.getSelectSqlAsSqlInfo(clazz, id);
        if (sqlInfo != null) {
            debugSql(sqlInfo.getSql());
            Cursor cursor = db.rawQuery(sqlInfo.getSql(),
                    sqlInfo.getBindArgsAsStringArray());
            try {
                if (cursor.moveToNext()) {
                    return CursorHelper.getEntity(cursor, clazz, this);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                cursor.close();
            }
        }
        return null;
    }

    /**
     * 根据主键查找,同时查找“多对一”的数据(如果有多个“多对一”属性,则查找所有的“多对一”属性)
     * 
     * @param id
     * @param clazz
     */
    public <T> T findWithManyToOneById(Object id, Class<T> clazz) {
        checkTableExist(clazz);
        String sql = SqlBuilder.getSelectSQL(clazz, id);
        debugSql(sql);
        DbModel dbModel = findDbModelBySQL(sql);
        if (dbModel != null) {
            T entity = CursorHelper.dbModel2Entity(dbModel, clazz);
            return loadManyToOne(entity, clazz);
        }
        return null;
    }

    /**
     * 根据条件查找,同时查找“多对一”的数据(只查找findClass中的类的数据)
     * 
     * @param id
     * @param clazz
     * @param findClass
     *            要查找的类
     */
    public <T> T findWithManyToOneById(Object id, Class<T> clazz,
            Class<?>... findClass) {
        checkTableExist(clazz);
        String sql = SqlBuilder.getSelectSQL(clazz, id);
        debugSql(sql);
        DbModel dbModel = findDbModelBySQL(sql);
        if (dbModel != null) {
            T entity = CursorHelper.dbModel2Entity(dbModel, clazz);
            return loadManyToOne(entity, clazz, findClass);
        }
        return null;
    }

    /**
     * 将entity中的“多对一”的数据填充满
     * 
     * @param clazz
     * @param entity
     * @param <T>
     * @return
     */
    public <T> T loadManyToOne(T entity, Class<T> clazz, Class<?>... findClass) {
        if (entity != null) {
            try {
                Collection<ManyToOne> manys = TableInfo.get(clazz).manyToOneMap
                        .values();
                for (ManyToOne many : manys) {
                    Object id = many.getValue(entity);
                    if (id != null) {
                        boolean isFind = false;
                        if (findClass == null || findClass.length == 0) {
                            isFind = true;
                        }
                        for (Class<?> mClass : findClass) {
                            if (many.getManyClass() == mClass) {
                                isFind = true;
                                break;
                            }
                        }
                        if (isFind) {
                            @SuppressWarnings("unchecked")
                            T manyEntity = (T) findById(
                                    Integer.valueOf(id.toString()),
                                    many.getDataType());
                            if (manyEntity != null) {
                                many.setValue(entity, manyEntity);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return entity;
    }

    /**
     * 根据主键查找,同时查找“一对多”的数据(如果有多个“一对多”属性,则查找所有的一对多”属性)
     * 
     * @param id
     * @param clazz
     */
    public <T> T findWithOneToManyById(Object id, Class<T> clazz) {
        checkTableExist(clazz);
        String sql = SqlBuilder.getSelectSQL(clazz, id);
        debugSql(sql);
        DbModel dbModel = findDbModelBySQL(sql);
        if (dbModel != null) {
            T entity = CursorHelper.dbModel2Entity(dbModel, clazz);
            return loadOneToMany(entity, clazz);
        }

        return null;
    }

    /**
     * 根据主键查找,同时查找“一对多”的数据(只查找findClass中的“一对多”)
     * 
     * @param id
     * @param clazz
     * @param findClass
     */
    public <T> T findWithOneToManyById(Object id, Class<T> clazz,
            Class<?>... findClass) {
        checkTableExist(clazz);
        String sql = SqlBuilder.getSelectSQL(clazz, id);
        debugSql(sql);
        DbModel dbModel = findDbModelBySQL(sql);
        if (dbModel != null) {
            T entity = CursorHelper.dbModel2Entity(dbModel, clazz);
            return loadOneToMany(entity, clazz, findClass);
        }

        return null;
    }

    /**
     * 将entity中的“一对多”的数据填充满
     * 
     * @param entity
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> T loadOneToMany(T entity, Class<T> clazz, Class<?>... findClass) {
        if (entity != null) {
            try {
                Collection<OneToMany> ones = TableInfo.get(clazz).oneToManyMap
                        .values();
                Object id = TableInfo.get(clazz).getId().getValue(entity);
                for (OneToMany one : ones) {
                    boolean isFind = false;
                    if (findClass == null || findClass.length == 0) {
                        isFind = true;
                    }
                    for (Class<?> mClass : findClass) {
                        if (one.getOneClass() == mClass) {
                            isFind = true;
                            break;
                        }
                    }

                    if (isFind) {
                        List<?> list = findAllByWhere(one.getOneClass(),
                                one.getColumn() + "=" + id);
                        if (list != null) {
                            /* 如果是OneToManyLazyLoader泛型,则执行灌入懒加载数据 */
                            if (one.getDataType() == OneToManyLazyLoader.class) {
                                OneToManyLazyLoader oneToManyLazyLoader = one
                                        .getValue(entity);
                                oneToManyLazyLoader.setList(list);
                            } else {
                                one.setValue(entity, list);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return entity;
    }

    /**
     * 查找所有的数据
     * 
     * @param clazz
     */
    public <T> List<T> findAll(Class<T> clazz) {
        checkTableExist(clazz);
        return findAllBySql(clazz, SqlBuilder.getSelectSQL(clazz));
    }

    /**
     * 查找所有数据
     * 
     * @param clazz
     * @param orderBy
     *            排序的字段
     */
    public <T> List<T> findAll(Class<T> clazz, String orderBy) {
        checkTableExist(clazz);
        return findAllBySql(clazz, SqlBuilder.getSelectSQL(clazz)
                + " ORDER BY " + orderBy);
    }

    /**
     * 根据条件查找所有数据
     * 
     * @param clazz
     * @param strWhere
     *            :strWhere表示sql语句select xxx from xxx where后的语句, 条件为空的时候查找所有数据
     */
    public <T> List<T> findAllByWhere(Class<T> clazz, String strWhere) {
        checkTableExist(clazz);
        return findAllBySql(clazz,
                SqlBuilder.getSelectSQLByWhere(clazz, strWhere));
    }

    /**
     * 根据条件查找所有数据
     * 
     * @param clazz
     * @param strWhere
     *            :strWhere表示sql语句select xxx from xxx where后的语句,条件为空的时候查找所有数据
     * @param orderBy
     *            排序字段
     */
    public <T> List<T> findAllByWhere(Class<T> clazz, String strWhere,
            String orderBy) {
        checkTableExist(clazz);
        return findAllBySql(clazz,
                SqlBuilder.getSelectSQLByWhere(clazz, strWhere) + " ORDER BY "
                        + orderBy);
    }

    /**
     * 根据条件查找所有数据
     * 
     * @param clazz
     * @param strSQL
     */
    private <T> List<T> findAllBySql(Class<T> clazz, String strSQL) {
        checkTableExist(clazz);
        debugSql(strSQL);
        Cursor cursor = db.rawQuery(strSQL, null);
        try {
            List<T> list = new ArrayList<T>();
            while (cursor.moveToNext()) {
                T t = CursorHelper.getEntity(cursor, clazz, this);
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null)
                cursor.close();
            cursor = null;
        }
        return null;
    }

    /**
     * 根据sql语句查找数据,这个一般用于数据统计
     * 
     * @param strSQL
     */
    public DbModel findDbModelBySQL(String strSQL) {
        debugSql(strSQL);
        Cursor cursor = db.rawQuery(strSQL, null);
        try {
            if (cursor.moveToNext()) {
                return CursorHelper.getDbModel(cursor);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            cursor.close();
        }
        return null;
    }

    public List<DbModel> findDbModelListBySQL(String strSQL) {
        debugSql(strSQL);
        Cursor cursor = db.rawQuery(strSQL, null);
        List<DbModel> dbModelList = new ArrayList<DbModel>();
        try {
            while (cursor.moveToNext()) {
                dbModelList.add(CursorHelper.getDbModel(cursor));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            cursor.close();
        }
        return dbModelList;
    }

    private void checkTableExist(Class<?> clazz) {
        if (!tableIsExist(TableInfo.get(clazz))) {
            String sql = SqlBuilder.getCreatTableSQL(clazz);
            debugSql(sql);
            db.execSQL(sql);
        }
    }

    private boolean tableIsExist(TableInfo table) {
        if (table.isCheckDatabese())
            return true;

        Cursor cursor = null;
        try {
            String sql = "SELECT COUNT(*) AS c FROM sqlite_master WHERE type ='table' AND name ='"
                    + table.getTableName() + "' ";
            debugSql(sql);
            cursor = db.rawQuery(sql, null);
            if (cursor != null && cursor.moveToNext()) {
                int count = cursor.getInt(0);
                if (count > 0) {
                    table.setCheckDatabese(true);
                    return true;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null)
                cursor.close();
            cursor = null;
        }

        return false;
    }

    private void debugSql(String sql) {
        if (config != null && config.isDebug())
            android.util.Log.d("Debug SQL", ">>>>>>  " + sql);
    }

    /**
     * 在SD卡的指定目录上创建文件
     * 
     * @param sdcardPath
     * @param dbfilename
     * @return
     */
    private SQLiteDatabase createDbFileOnSDCard(String sdcardPath,
            String dbfilename) {
        File dbf = new File(sdcardPath, dbfilename);
        if (!dbf.exists()) {
            try {
                if (dbf.createNewFile()) {
                    return SQLiteDatabase.openOrCreateDatabase(dbf, null);
                }
            } catch (IOException ioex) {
                throw new RuntimeException("数据库文件创建失败", ioex);
            }
        } else {
            return SQLiteDatabase.openOrCreateDatabase(dbf, null);
        }
        return null;
    }

    private class SqliteDbHelper extends SQLiteOpenHelper {

        private final DbUpdateListener mDbUpdateListener;

        public SqliteDbHelper(Context context, String name, int version,
                DbUpdateListener dbUpdateListener) {
            super(context, name, null, version);
            this.mDbUpdateListener = dbUpdateListener;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {}

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            if (mDbUpdateListener != null) {
                mDbUpdateListener.onUpgrade(db, oldVersion, newVersion);
            } else { // 清空所有的数据信息
                dropDb();
            }
        }

    }

    /**
     * 数据库升级监听器
     */
    public interface DbUpdateListener {
        /**
         * @param db
         *            数据库
         * @param oldVersion
         *            旧版本
         * @param newVersion
         *            新版本
         */
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
    }

}