package io.github.notsyncing.lightfur.entity

import com.alibaba.fastjson.annotation.JSONField
import io.github.notsyncing.lightfur.DataSession
import io.github.notsyncing.lightfur.entity.dsl.*
import io.github.notsyncing.lightfur.entity.events.CanFireEvents
import io.github.notsyncing.lightfur.entity.events.EntityEvent
import io.github.notsyncing.lightfur.entity.events.EntityEventDispatcher
import io.github.notsyncing.lightfur.sql.base.ExpressionBuilder
import io.github.notsyncing.lightfur.sql.models.TableModel
import kotlinx.coroutines.experimental.future.await
import kotlinx.coroutines.experimental.future.future
import java.io.InvalidObjectException
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0

abstract class EntityModel(@field:JSONField(serialize = false, deserialize = false) val database: String? = null,
                           @field:JSONField(serialize = false, deserialize = false) val schema: String? = null,
                           @field:JSONField(serialize = false, deserialize = false) val table: String) :
        CanFireEvents {
    companion object {
        @JSONField(serialize = false, deserialize = false)
        val primaryKeyFieldCache = ConcurrentHashMap<Class<EntityModel>, MutableSet<KProperty<*>>>()

        fun getPrimaryKeyFieldsFromCache(modelClass: Class<EntityModel>): MutableSet<KProperty<*>> {
            var l = primaryKeyFieldCache[modelClass]

            if (l == null) {
                l = mutableSetOf()
                primaryKeyFieldCache[modelClass] = l
            }

            return l
        }
    }

    @JSONField(serialize = false, deserialize = false)
    override val events = mutableListOf<EntityEvent>()

    @JSONField(serialize = false, deserialize = false)
    lateinit var primaryKeyFields: Set<KProperty<*>>

    @JSONField(serialize = false, deserialize = false)
    val fieldMap = ConcurrentHashMap<String, EntityField<*>>()

    @JSONField(serialize = false, deserialize = false)
    val referenceMap = ConcurrentHashMap<String, EntityReference<*>>()

    @JSONField(serialize = false, deserialize = false)
    val primaryKeyFieldInfos = ArrayList<EntityField<*>>()

    @JSONField(serialize = false, deserialize = false)
    var skipTableName = false

    @JSONField(serialize = false, deserialize = false)
    var skipTableAlias = false

    @JSONField(serialize = false, deserialize = false)
    var modelAliasBeforeColumnName = false

    init {
        if (!EntityGlobal.tableModels.containsKey(this::class.java)) {
            val t = TableModel()
            t.name = table
            t.database = database
            t.schema = schema

            EntityGlobal.tableModels[this::class.java as Class<EntityModel>] = t
        }
    }

    protected inline fun <reified T> field(column: String? = null, type: String? = null,
                                           length: Int = 0, nullable: Boolean = false, defaultValue: T? = null,
                                           defaultValueDefinedInDatabase: Boolean = false,
                                           primaryKey: Boolean = false, autoGenerated: Boolean = false): EntityField<T> {
        return EntityField(T::class.java, column, type, length, nullable,
                defaultValue, defaultValueDefinedInDatabase, primaryKey, autoGenerated)
    }

    protected inline fun <reified T: EntityModel?> reference(refField: KProperty<*>,
                                                             keyField: KProperty0<*>? = null): EntityReference<T> {
        return EntityReference(T::class.java, null, refField.name, keyField?.name)
    }

    protected inline fun <reified I: EntityModel, reified T: MutableCollection<I>> referenceMany(refField: KProperty<*>,
                                                                                                 keyField: KProperty0<*>? = null): EntityReference<T> {
        return EntityReference(T::class.java, I::class.java, refField.name, keyField?.name)
    }

    infix fun F(property: KProperty0<*>): EntityField<*> = fieldMap[property.name]!!

    infix fun R(property: KProperty0<*>): EntityReference<*> = referenceMap[property.name]!!

    fun assumeNoChange() {
        fieldMap.forEach { it.value.changed = false }
    }

    fun assumeAllChanged() {
        fieldMap.forEach { it.value.changed = true }
    }

    fun assumeChanged(property: KProperty<*>) {
        fieldMap[property.name]!!.changed = true
    }

    fun assumeNoChange(property: KProperty<*>) {
        fieldMap[property.name]!!.changed = false
    }

    fun hasChanged() = fieldMap.any { it.value.changed }

    fun copyFieldsTo(obj: Any) {
        if (obj is EntityModel) {
            fieldMap.forEach {
                obj.fieldMap[it.key]?.data = it.value.data
            }
        } else {
            fieldMap.forEach {
                obj.javaClass.getField(it.key)?.set(obj, it.value.data)
            }
        }
    }

    fun copyFieldsFrom(obj: Any) {
        if (obj is EntityModel) {
            fieldMap.forEach {
                it.value.data = obj.fieldMap[it.key]?.data
            }
        } else {
            fieldMap.forEach {
                val field = obj.javaClass.getField(it.key)

                if (field != null) {
                    it.value.data = field.get(obj)
                }
            }
        }
    }

    private fun getReferenceKeyField(ref: EntityReference<*>): EntityField<*> {
        val keyFieldName = if (ref.keyFieldName == null) {
            if (primaryKeyFields.size != 1) {
                throw InvalidObjectException("To use reference, the model $this must contain only one primary key " +
                        "field, or specify a key field explicitly, while this model has " +
                        "${primaryKeyFields.size} primary key column(s), neither specified a key field!")
            }

            primaryKeyFieldInfos[0].name
        } else {
            ref.keyFieldName
        }

        return fieldMap[keyFieldName]!!
    }

    private fun getReferenceKeyFieldValue(ref: EntityReference<*>): Any? {
        return getReferenceKeyField(ref).data
    }

    class LoadResult(val _intermediateModel: EntityModel,
                     val data: Any?)

    fun load(db: DataSession<*, *, *>, ref: EntityReference<*>) = future {
        val key = getReferenceKeyFieldValue(ref)
        val targetModel: EntityModel

        try {
            targetModel = (ref.componentClass ?: ref.targetClass).newInstance()
        } catch (e: Exception) {
            val ex = Exception("Target model ${ref.componentClass ?: ref.targetClass} cannot be " +
                    "instantiated, please make sure it contains a no-arg constructor!", e)

            throw ex
        }

        val query = targetModel.select()
                .where { targetModel.fieldMap[ref.refFieldName]!! eq key }

        val data: Any? = if (ref.componentClass == null) {
            query.executeFirst(db)
                    .await()
        } else {
            query.execute(db)
                    .thenApply { (l, _) -> l }
                    .await()
        }

        val refField = referenceMap[ref.name]!! as EntityReference<Any>
        refField.data = data
        refField.changed = false

        LoadResult(targetModel, data)
    }

    fun load(db: DataSession<*, *, *>, refField: KProperty0<*>): CompletableFuture<LoadResult> {
        return load(db, R(refField))
    }

    fun loadAll(db: DataSession<*, *, *>, recursive: Boolean = true): CompletableFuture<Unit> = future {
        for ((_, refField) in referenceMap) {
            load(db, refField).await()
        }

        if (recursive) {
            for ((_, refField) in referenceMap) {
                val data = refField.data

                if (data is EntityModel) {
                    data.loadAll(db, recursive).await()
                } else if (data is MutableCollection<*>) {
                    for (m in data) {
                        if (m !is EntityModel) {
                            continue
                        }

                        m.loadAll(db, recursive).await()
                    }
                }
            }
        }
    }

    class SaveResult(val _intermediateModel: EntityModel?,
                     val list: List<EntityModel>,
                     val count: Int)

    fun save(db: DataSession<*, *, *>): CompletableFuture<Pair<List<EntityModel>, Int>> = future {
        val r = insert()
                .updateWhenExists(primaryKeyFieldInfos) {
                    it.set(skipPrimaryKeys = true)
                            .where(null)
                }
                .execute(db)
                .await()

        dispatchEvents(db).await()

        r
    }

    fun save(db: DataSession<*, *, *>, refField: EntityReference<*>): CompletableFuture<SaveResult> = future {
        save(db).await()

        val keyField = getReferenceKeyField(refField)
        val field = referenceMap[refField.name]!!

        if (refField.componentClass == null) {
            var refModel = field.data as? EntityModel

            val r = if (field.changed) {
                if (refModel == null) {
                    refModel = refField.targetClass.newInstance()
                    refModel!!.fieldMap[refField.refFieldName]!!.data = keyField.data

                    refModel.delete()
                            .execute(db)
                            .await()
                } else {
                    refModel.fieldMap[refField.refFieldName]!!.data = keyField.data

                    refModel.save(db)
                            .await()
                }
            } else {
                Pair(emptyList(), 0)
            }

            refModel?.dispatchEvents(db)?.await()

            SaveResult(refModel, r.first, r.second)
        } else {
            val refModelList = field.data as MutableCollection<EntityModel>
            val refModelPrimaryKeyList = mutableListOf<MutableList<Any?>>()

            for (m in refModelList) {
                m.fieldMap[refField.refFieldName]!!.data = keyField.data

                m.save(db).await()

                val pkValues = mutableListOf<Any?>()

                for (pkField in m.primaryKeyFieldInfos) {
                    pkValues.add(m.fieldMap[pkField.name]!!.data)
                }

                refModelPrimaryKeyList.add(pkValues)
            }

            val refModel = refField.componentClass!!.newInstance()

            val notInCurrentKeys = if (refModelPrimaryKeyList.isNotEmpty()) {
                ExpressionBuilder()
                        .beginGroup()
                        .apply {
                            for (pkField in refModel.primaryKeyFieldInfos) {
                                column(EntityBaseDSL.getColumnModelFromEntityField(pkField))
                                        .separator()
                            }
                        }
                        .trimLastSeparator()
                        .endGroup()
                        .not()
                        .`in`()
                        .beginGroup()
                        .apply {
                            for (pkValues in refModelPrimaryKeyList) {
                                beginGroup()

                                for (v in pkValues) {
                                    literal(v)
                                    separator()
                                }

                                trimLastSeparator()
                                endGroup()
                                separator()
                            }
                        }
                        .trimLastSeparator()
                        .endGroup()
            } else {
                ExpressionBuilder()
                        .literal(true)
            }

            val r = refModel.delete()
                    .where {
                        (refModel.fieldMap[refField.refFieldName]!! eq keyField.data) and notInCurrentKeys
                    }
                    .execute(db)
                    .await()

            refModel.dispatchEvents(db).await()

            SaveResult(refModel, r.first, r.second)
        }
    }

    fun save(db: DataSession<*, *, *>, refField: KProperty0<*>): CompletableFuture<SaveResult> {
        return save(db, R(refField))
    }

    fun saveAll(db: DataSession<*, *, *>, recursive: Boolean = true): CompletableFuture<Unit> = future {
        for ((_, refField) in referenceMap) {
            save(db, refField).await()
        }

        if (recursive) {
            for ((_, refField) in referenceMap) {
                val data = refField.data

                if (data is EntityModel) {
                    data.saveAll(db, recursive).await()
                } else if (data is MutableCollection<*>) {
                    for (m in data) {
                        if (m !is EntityModel) {
                            continue
                        }

                        m.saveAll(db, recursive).await()
                    }
                }
            }
        }
    }

    fun dispatchEvents(db: DataSession<*, *, *>, recursive: Boolean = false): CompletableFuture<Unit> = future {
        try {
            EntityEventDispatcher.dispatch(db, events).await()
        } catch (e: Exception) {
            throw Exception("Failed to dispatch events in $this", e)
        } finally {
            events.clear()
        }

        if (recursive) {
            for ((_, refField) in referenceMap) {
                val data = refField.data

                if (data is EntityModel) {
                    data.dispatchEvents(db).await()
                } else if (data is MutableCollection<*>) {
                    for (m in data) {
                        if (m !is EntityModel) {
                            continue
                        }

                        m.dispatchEvents(db).await()
                    }
                }
            }
        }
    }
}

fun <T: EntityModel> T.select(): EntitySelectDSL<T> {
    return EntityDSL.select(this).from()
}

fun <T: EntityModel> T.insert(): EntityInsertDSL<T> {
    this.assumeAllChanged()

    for ((i, v) in primaryKeyFieldInfos.withIndex()) {
        if (v.dbAutoGenerated == true) {
            fieldMap[v.name]!!.changed = false
        }
    }

    return EntityDSL.insert(this).values()
}

fun <T: EntityModel> T.update(): EntityUpdateDSL<T> {
    return EntityDSL.update(this).set()
}

fun <T: EntityModel> T.delete(): EntityDeleteDSL<T> {
    return EntityDSL.delete(this)
}