/*
 * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 */

package com.etesync.syncadapter

import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Application
import android.content.*
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.StrictMode
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.core.content.ContextCompat
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.*
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.ui.AccountsActivity
import com.etesync.syncadapter.utils.HintManager
import com.etesync.syncadapter.utils.LanguageUtils
import com.etesync.syncadapter.utils.NotificationUtils
import com.etesync.syncadapter.utils.TaskProviderHandling
import io.requery.Persistable
import io.requery.android.sqlite.DatabaseSource
import io.requery.meta.EntityModel
import io.requery.sql.EntityDataStore
import org.acra.ACRA
import org.jetbrains.anko.doAsync
import java.util.*


class App : Application() {
    /**
     * @return [EntityDataStore] single instance for the application.
     *
     *
     * Note if you're using Dagger you can make this part of your application level module returning
     * `@Provides @Singleton`.
     */
    // override onUpgrade to handle migrating to a new version
    val data: MyEntityDataStore
        get() = initDataStore()

    fun initDataStore(): MyEntityDataStore {
        val source = MyDatabaseSource(this, Models.DEFAULT, 6)
        val configuration = source.configuration
        return MyEntityDataStore(configuration)
    }

    @SuppressLint("HardwareIds")
    override fun onCreate() {
        super.onCreate()
        reinitLogger()
        StrictMode.enableDefaults()
        initPrefVersion()

        NotificationUtils.createChannels(this)

        appName = getString(R.string.app_name)
        accountType = getString(R.string.account_type)
        addressBookAccountType = getString(R.string.account_type_address_book)
        addressBooksAuthority = getString(R.string.address_books_authority)

        loadLanguage()

        // don't block UI for some background checks
        doAsync {
            // watch installed/removed apps
            val tasksFilter = IntentFilter()
            tasksFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
            tasksFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
            tasksFilter.addDataScheme("package")
            registerReceiver(PackageChangedReceiver(), tasksFilter)

            OPENTASK_PROVIDERS.forEach {
                // check whether a tasks app is currently installed
                TaskProviderHandling.updateTaskSync(this@App, it)
            }
        }
    }

    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)

        if (!BuildConfig.DEBUG) {
            // The following line triggers the initialization of ACRA
            ACRA.init(this, AcraConfiguration.getConfig(this))
            val pm = base.packageManager
            val installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID)
            ACRA.getErrorReporter().putCustomData("installedFrom", installedFrom);
        }
    }

    private fun loadLanguage() {
        val serviceDB = ServiceDB.OpenHelper(this)
        val lang = Settings(serviceDB.readableDatabase).getString(App.FORCE_LANGUAGE, null)
        if (lang != null && lang != DEFAULT_LANGUAGE) {
            LanguageUtils.setLanguage(this, lang)
        }

        serviceDB.close()
    }

    fun reinitLogger() {
        Logger.initialize(this)
    }

    private class MyDatabaseSource internal constructor(context: Context, entityModel: EntityModel, version: Int) : DatabaseSource(context, entityModel, version) {

        override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            super.onUpgrade(db, oldVersion, newVersion)

            if (oldVersion < 3) {
                db.execSQL("PRAGMA foreign_keys=OFF;")

                db.execSQL("CREATE TABLE new_Journal (id integer primary key autoincrement not null, deleted boolean not null, encryptedKey varbinary(255), info varchar(255), owner varchar(255), service integer, serviceModel integer, uid varchar(64) not null, readOnly boolean default false, foreign key (serviceModel) references Service (id) on delete cascade);")
                db.execSQL("CREATE TABLE new_Entry (id integer primary key autoincrement not null, content varchar(255), journal integer, uid varchar(64) not null, foreign key (journal) references new_Journal (id) on delete cascade);")

                db.execSQL("INSERT INTO new_Journal SELECT id, deleted, encryptedKey, info, owner, service, serviceModel, uid, 0 from Journal;")
                db.execSQL("INSERT INTO new_Entry SELECT id, content, journal, uid from Entry;")

                db.execSQL("DROP TABLE Journal;")
                db.execSQL("DROP TABLE Entry;")
                db.execSQL("ALTER TABLE new_Journal RENAME TO Journal;")
                db.execSQL("ALTER TABLE new_Entry RENAME TO Entry;")
                // Add back indexes
                db.execSQL("CREATE UNIQUE INDEX journal_unique_together on Journal (serviceModel, uid);")
                db.execSQL("CREATE UNIQUE INDEX entry_unique_together on Entry (journal, uid);")
                db.execSQL("PRAGMA foreign_keys=ON;")
            }
        }
    }

    /** Init the preferences version of the app.
     * This is used to initialise the first version if not alrady set.  */
    private fun initPrefVersion() {
        val prefs = getSharedPreferences("app", Context.MODE_PRIVATE)
        if (prefs.getInt(PREF_VERSION, 0) == 0) {
            prefs.edit().putInt(PREF_VERSION, BuildConfig.VERSION_CODE).apply()
        }
    }

    private fun update(fromVersion: Int) {
        Logger.log.info("Updating from version " + fromVersion + " to " + BuildConfig.VERSION_CODE)

        if (fromVersion < 7) {
            /* Fix all of the etags to be non-null */
            val am = AccountManager.get(this)
            for (account in am.getAccountsByType(App.accountType)) {
                try {
                    // Generate account settings to make sure account is migrated.
                    AccountSettings(this, account)

                    val calendars = AndroidCalendar.find(account, this.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
                            LocalCalendar.Factory, null, null)
                    for (calendar in calendars) {
                        calendar.fixEtags()
                    }
                } catch (e: CalendarStorageException) {
                    e.printStackTrace()
                } catch (e: InvalidAccountException) {
                    e.printStackTrace()
                }

            }

            for (account in am.getAccountsByType(App.addressBookAccountType)) {
                val addressBook = LocalAddressBook(this, account, this.contentResolver.acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI))
                try {
                    addressBook.fixEtags()
                } catch (e: ContactsStorageException) {
                    e.printStackTrace()
                }

            }
        }

        if (fromVersion < 10) {
            HintManager.setHintSeen(this, AccountsActivity.HINT_ACCOUNT_ADD, true)
        }

        if (fromVersion < 11) {
            val dbHelper = ServiceDB.OpenHelper(this)

            migrateServices(dbHelper)

            dbHelper.close()
        }
    }

    class AppUpdatedReceiver : BroadcastReceiver() {

        @SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
        override fun onReceive(context: Context, intent: Intent) {
            Logger.log.info("EteSync was updated, checking for app version")

            val app = context.applicationContext as App
            val prefs = app.getSharedPreferences("app", Context.MODE_PRIVATE)
            val fromVersion = prefs.getInt(PREF_VERSION, 1)
            app.update(fromVersion)
            prefs.edit().putInt(PREF_VERSION, BuildConfig.VERSION_CODE).apply()
        }

    }

    fun migrateServices(dbHelper: ServiceDB.OpenHelper) {
        val db = dbHelper.readableDatabase
        val data = this.data
        val cursor = db.query(ServiceDB.Services._TABLE, null, null, null, null, null, null)
        while (cursor.moveToNext()) {
            val values = ContentValues()
            DatabaseUtils.cursorRowToContentValues(cursor, values)
            val service = ServiceEntity()
            service.account = values.getAsString(ServiceDB.Services.ACCOUNT_NAME)
            service.type = CollectionInfo.Type.valueOf(values.getAsString(ServiceDB.Services.SERVICE))
            data.insert(service)

            for (journalEntity in data.select(JournalEntity::class.java).where(JournalEntity.SERVICE.eq(values.getAsLong(ServiceDB.Services.ID))).get()) {
                journalEntity.serviceModel = service
                data.update(journalEntity)
            }
        }

        db.delete(ServiceDB.Services._TABLE, null, null)
        db.close()
        cursor.close()
    }

    companion object {
        val DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts"
        val LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage"
        val OVERRIDE_PROXY = "overrideProxy"
        val OVERRIDE_PROXY_HOST = "overrideProxyHost"
        val OVERRIDE_PROXY_PORT = "overrideProxyPort"
        val PREFER_TASKSORG = "preferTasksOrg"
        val FORCE_LANGUAGE = "forceLanguage"
        val CHANGE_NOTIFICATION = "show_change_notification"

        val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
        val OVERRIDE_PROXY_PORT_DEFAULT = 8118

        val DEFAULT_LANGUAGE = "default"
        var sDefaultLocacle = Locale.getDefault()

        var appName: String = "EteSync"

        lateinit var accountType: String
            private set
        lateinit var addressBookAccountType: String
            private set
        lateinit var addressBooksAuthority: String
            private set

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        fun getLauncherBitmap(context: Context): Bitmap? {
            var bitmapLogo: Bitmap? = null
            val drawableLogo = ContextCompat.getDrawable(context, R.mipmap.ic_launcher)

            if (drawableLogo is BitmapDrawable)
                bitmapLogo = drawableLogo.bitmap
            return bitmapLogo
        }

        // update from previous account settings

        private val PREF_VERSION = "version"
    }
}
