/*
 * Copyright (c) 2018 Alexander Yaburov
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package me.impa.knockonports.database

import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import android.content.Context
import android.util.Base64
import me.impa.knockonports.data.AppData
import me.impa.knockonports.data.IcmpType
import me.impa.knockonports.database.converter.DataConverters
import me.impa.knockonports.database.dao.LogEntryDao
import me.impa.knockonports.database.dao.SequenceDao
import me.impa.knockonports.database.entity.LogEntry
import me.impa.knockonports.database.entity.Sequence

@Database(
        entities = [Sequence::class, LogEntry::class],
        version = 16
)
@TypeConverters(DataConverters::class)
abstract class KnocksDatabase : RoomDatabase() {
    abstract fun sequenceDao(): SequenceDao
    abstract fun logEntryDao(): LogEntryDao

    class Migration1To2: Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_delay` INTEGER")
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_udp_content` TEXT")
        }
    }

    class Migration2To3: Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_application` TEXT")
        }

    }

    class Migration3To4: Migration(3, 4) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_base64` INTEGER")
        }
    }

    class Migration4To5: Migration(4, 5) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_port_string` TEXT")
            val seqCur = database.query("SELECT `_id` FROM `tbSequence`")
            val seqList = mutableListOf<Long>()
            if (seqCur.moveToFirst()) {
                do {
                    seqList.add(seqCur.getLong(0))
                } while (seqCur.moveToNext())
            } else return
            seqList.forEach {
                val portCur = database.query("SELECT `_number`, `_type` FROM `tbPort` WHERE `_sequence_id`=? ORDER BY `_id`", arrayOf(it))
                val portList = mutableListOf<String>()
                if (portCur.moveToFirst()) {
                    do {
                        if (!portCur.isNull(0) && !portCur.isNull(1)) {
                            portList.add("${portCur.getInt(0)}:${if (portCur.getInt(1) == 0) {
                                "UDP"
                            } else {
                                "TCP"
                            }}")
                        }
                    } while (portCur.moveToNext())
                }
                if (portList.size > 0) {
                    database.execSQL("UPDATE `tbSequence` SET `_port_string`=? WHERE `_id`=?", arrayOf(portList.joinToString(", "), it))
                }
            }
        }
    }

    class Migration5To6: Migration(5, 6) {
        override fun migrate(database: SupportSQLiteDatabase) {
            val portMap = mutableMapOf<Long, String>()
            val portCur = database.query("SELECT `_sequence_id`, `_number`, `_type` FROM `tbPort` ORDER BY `_id`")
            if (portCur.moveToFirst()) {
                do {
                    if (portCur.isNull(0))
                        continue
                    val seqId = portCur.getLong(0)
                    val str = if (portCur.isNull(1)) {
                        ""
                    } else {
                        portCur.getInt(1).toString()
                    } + DataConverters.VALUE_SEPARATOR +
                            if (portCur.isNull(2)) {
                                ""
                            } else {
                                portCur.getInt(2).toString()
                            }
                    portMap[seqId] = if (portMap.containsKey(seqId)) {
                        portMap[seqId] + DataConverters.ENTRY_SEPARATOR
                    } else {
                        ""
                    } + str
                } while (portCur.moveToNext())
                portMap.forEach {
                    database.execSQL("UPDATE `tbSequence` SET `_port_string`=? WHERE `_id`=?", arrayOf(it.value, it.key))
                }
            }
            database.execSQL("DROP TABLE `tbPort`")
        }
    }

    class Migration6To7(val context: Context): Migration(6, 7) {
        override fun migrate(database: SupportSQLiteDatabase) {
            val apps by lazy { AppData.loadInstalledApps(context) }
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_application_name` TEXT")
            val appMap = mutableMapOf<Long, String?>()
            val appCur = database.query("SELECT `_id`, `_application` FROM `tbSequence`")
            if (appCur.moveToFirst()) {
                do {
                    if (appCur.isNull(1) || appCur.isNull(0))
                        continue
                    val app = appCur.getString(1)
                    if (app.isEmpty())
                        continue
                    val seqId = appCur.getLong(0)
                    val appName = apps.firstOrNull { it.app == app }?.name
                    appMap[seqId] = appName
                } while (appCur.moveToNext())
                appMap.forEach {
                    database.execSQL("UPDATE `tbSequence` SET `_application_name`=? WHERE `_id`=?", arrayOf(it.value, it.key))
                }
            }
        }
    }

    class Migration7To8: Migration(7, 8) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_type` INTEGER")
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_icmp_string` TEXT")
            database.execSQL("UPDATE `tbSequence` SET `_type`=0")
        }
    }

    class Migration8To9: Migration(8, 9) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_icmp_type` INTEGER")
            database.execSQL("UPDATE `tbSequence` SET `_icmp_type`=1")
        }
    }

    class Migration9To10: Migration(9, 10) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("CREATE TABLE `tbSequence_new` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "`_name` TEXT, `_host` TEXT, `_order` INTEGER, `_delay` INTEGER, `_udp_content` TEXT, " +
                    "`_application` TEXT, `_base64` INTEGER, `_port_string` TEXT, `_application_name` TEXT, " +
                    "`_type` INTEGER, `_icmp_string` TEXT, `_icmp_type` INTEGER)")
            database.execSQL("INSERT INTO `tbSequence_new` (`_id`, " +
                    "`_name`, `_host`, `_order`, `_delay`, `_udp_content`, " +
                    "`_application`, `_base64`, `_port_string`, `_application_name`, " +
                    "`_type`, `_icmp_string`, `_icmp_type`) SELECT `_id`, " +
                    "`_name`, `_host`, `_order`, `_delay`, `_udp_content`, " +
                    "`_application`, `_base64`, `_port_string`, `_application_name`, " +
                    "`_type`, `_icmp_string`, `_icmp_type` from `tbSequence`")
            database.execSQL("DROP TABLE `tbSequence`")
            database.execSQL("ALTER TABLE `tbSequence_new` RENAME TO `tbSequence`")
        }
    }

    class Migration10To11: Migration(10, 11) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("CREATE TABLE `tbSequence_new` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "`_name` TEXT, `_host` TEXT, `_order` INTEGER, `_delay` INTEGER, `_application` TEXT, " +
                    "`_application_name` TEXT, `_icmp_type` INTEGER, `_steps` TEXT)")
            database.execSQL("INSERT INTO `tbSequence_new` (`_id`, `_name`, `_host`, `_order`, `_delay`, " +
                    "`_application`, `_application_name`, `_icmp_type`) SELECT " +
                    "`_id`, `_name`, `_host`, `_order`, `_delay`, `_application`, `_application_name`, " +
                    "`_icmp_type` from `tbSequence`")

            val seqMap = mutableMapOf<Long, String?>()
            val seqCur = database.query("SELECT `_id`, `_type`, `_udp_content`, `_base64`, `_port_string`, `_icmp_string` FROM `tbSequence`")
            if (seqCur.moveToFirst()) {
                do {
                    if (seqCur.isNull(0) || seqCur.isNull(1))
                        continue
                    val id = seqCur.getLong(0)
                    val type = seqCur.getLong(1)
                    if (type == 0L) {
                        val portString = if (seqCur.isNull(4)) "" else seqCur.getString(4)
                        if (portString.isBlank())
                            continue
                        val content = if (seqCur.isNull(2)) "" else Base64.encodeToString(seqCur.getString(2).toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP)
                        val base64 = if (seqCur.isNull(3)) 0 else seqCur.getLong(3)
                        seqMap[id] = portString.split('|').joinToString("|") {
                            val portData = it.split(":")
                            "${portData[1]}:${portData[0]}:::$content:${if (base64 == 0L || base64 == 1L) base64 else 0}"
                        }
                    } else if (type == 1L) {
                        val icmpString = if (seqCur.isNull(5)) "" else seqCur.getString(5)
                        if (icmpString.isBlank())
                            continue
                        seqMap[id] = icmpString.split("|").joinToString("|") {
                            val icmpData = it.split(":")
                            "2::${icmpData[0]}:${icmpData[1]}:${icmpData[3]}:${icmpData[2]}"
                        }
                    }
                } while (seqCur.moveToNext())
                seqMap.forEach{
                    database.execSQL("UPDATE `tbSequence_new` SET `_steps`=? WHERE `_id`=?", arrayOf(it.value, it.key))
                }
            }
            database.execSQL("DROP TABLE `tbSequence`")
            database.execSQL("ALTER TABLE `tbSequence_new` RENAME TO `tbSequence`")
        }
    }

    class Migration11To12: Migration(11, 12) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_descriptionType` INTEGER")
            database.execSQL("UPDATE `tbSequence` SET `_descriptionType`=0")
        }
    }

    class Migration12To13: Migration(12, 13) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_pin` TEXT")
        }
    }

    class Migration13To14: Migration(13, 14) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("CREATE TABLE `tbLog` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    "`_dt` INTEGER, `_event` INTEGER, `_data` TEXT)")
        }
    }

    class Migration14To15: Migration(14, 15) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE `tbSequence` ADD COLUMN `_ipv` INTEGER")
            database.execSQL("UPDATE `tbSequence` SET `_ipv`=0")
        }
    }

    class Migration15To16: Migration(15, 16) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("UPDATE `tbSequence` SET `_icmp_type`=${IcmpType.WITH_ICMP_HEADER.ordinal} WHERE `_icmp_type`=${IcmpType.WITH_IP_AND_ICMP_HEADERS.ordinal}")
        }

    }

    companion object {
        private var INSTANCE: KnocksDatabase? = null

        fun getInstance(context: Context): KnocksDatabase? {
            if (INSTANCE == null) {
                synchronized(KnocksDatabase::class) {
                    INSTANCE = Room.databaseBuilder(
                        context.applicationContext,
                        KnocksDatabase::class.java,
                        "knocksdb"
                    )
                        .addMigrations(
                            Migration1To2(),
                            Migration2To3(),
                            Migration3To4(),
                            Migration4To5(),
                            Migration5To6(),
                            Migration6To7(context),
                            Migration7To8(),
                            Migration8To9(),
                            Migration9To10(),
                            Migration10To11(),
                            Migration11To12(),
                            Migration12To13(),
                            Migration13To14(),
                            Migration14To15(),
                            Migration15To16()
                        )
                        .build()
                }
            }
            return INSTANCE
        }

        @Suppress("unused")
        fun destroyInstance() {
            synchronized(KnocksDatabase::class) {
                INSTANCE?.close()
                INSTANCE = null
            }
        }
    }
}