package pl.szczodrzynski.edziennik

import android.app.ActivityManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.navigation.NavOptions
import com.danimahardhika.cafebar.CafeBar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jetradarmobile.snowfall.SnowfallView
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import com.mikepenz.materialdrawer.model.*
import com.mikepenz.materialdrawer.model.interfaces.*
import com.mikepenz.materialdrawer.model.utils.hiddenInMiniDrawer
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateStateEvent
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.ui.base.enums.NavTargetLocation
import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateProgressDialog
import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment
import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.*
import pl.szczodrzynski.navlib.SystemBarsUtil.Companion.COLOR_HALF_TRANSPARENT
import pl.szczodrzynski.navlib.bottomsheet.NavBottomSheet
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import pl.szczodrzynski.navlib.drawer.NavDrawer
import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem
import java.io.IOException
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt

class MainActivity : AppCompatActivity(), CoroutineScope {
    companion object {
        private const val TAG = "MainActivity"
    }

    private var job = Job()
    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main

    val b: ActivitySzkolnyBinding by lazy { ActivitySzkolnyBinding.inflate(layoutInflater) }
    val navView: NavView by lazy { b.navView }
    val drawer: NavDrawer by lazy { navView.drawer }
    val bottomSheet: NavBottomSheet by lazy { navView.bottomSheet }
    val mainSnackbar: MainSnackbar by lazy { MainSnackbar(this) }
    val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
    val requestHandler by lazy { MainActivityRequestHandler(this) }

    val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }

    var onBeforeNavigate: (() -> Boolean)? = null
    private var pausedNavigationData: PausedNavigationData? = null

    val app: App by lazy {
        applicationContext as App
    }

    private val fragmentManager by lazy { supportFragmentManager }
    lateinit var navTarget: NavTarget
        private set
    private var navArguments: Bundle? = null

    private val navBackStack = mutableListOf<Pair<NavTarget, Bundle?>>()
    private var navLoading = true

    /*     ____           _____                _
          / __ \         / ____|              | |
         | |  | |_ __   | |     _ __ ___  __ _| |_ ___
         | |  | | '_ \  | |    | '__/ _ \/ _` | __/ _ \
         | |__| | | | | | |____| | |  __/ (_| | ||  __/
          \____/|_| |_|  \_____|_|  \___|\__,_|\__\__*/
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        d(TAG, "Activity created")

        setTheme(Themes.appTheme)

        app.config.ui.language?.let {
            setLanguage(it)
        }

        app.buildManager.validateBuild(this)

        if (App.profileId == 0) {
            onProfileListEmptyEvent(ProfileListEmptyEvent())
            return
        }

        d(TAG, "Profile is valid, inflating views")

        setContentView(b.root)

        mainSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
        errorSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)

        val versionBadge = app.buildManager.versionBadge
        b.nightlyText.isVisible = versionBadge != null
        b.nightlyText.text = versionBadge
        if (versionBadge != null) {
            b.nightlyText.background.setTintColor(0xa0ff0000.toInt())
        }

        navLoading = true

        b.navView.apply {
            drawer.init(this@MainActivity)

            SystemBarsUtil(this@MainActivity).run {
                //paddingByKeyboard = b.navView
                appFullscreen = false
                statusBarColor = getColorFromAttr(context, android.R.attr.colorBackground)
                statusBarDarker = false
                statusBarFallbackLight = COLOR_HALF_TRANSPARENT
                statusBarFallbackGradient = COLOR_HALF_TRANSPARENT
                navigationBarTransparent = false

                b.navView.configSystemBarsUtil(this)

                // fix for setting status bar color to window color, outside of navlib
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    window.statusBarColor = statusBarColor
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                    && ColorUtils.calculateLuminance(statusBarColor) > 0.6
                ) {
                    @Suppress("deprecation")
                    window.decorView.systemUiVisibility =
                        window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
                }

                // TODO fix navlib navbar detection, orientation change issues, status bar color setting if not fullscreen

                commit()
            }

            toolbar.apply {
                subtitleFormat = R.string.toolbar_subtitle
                subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
            }

            bottomBar.apply {
                fabEnable = false
                fabExtendable = true
                fabExtended = false
                fabGravity = Gravity.CENTER
                if (Themes.isDark) {
                    setBackgroundColor(blendColors(
                        getColorFromAttr(context, R.attr.colorSurface),
                        getColorFromRes(R.color.colorSurface_4dp)
                    ))
                    elevation = dpToPx(4).toFloat()
                }
            }

            bottomSheet.apply {
                removeAllItems()
                toggleGroupEnabled = false
                textInputEnabled = false
                onCloseListener = {
                    if (!app.config.ui.bottomSheetOpened)
                        app.config.ui.bottomSheetOpened = true
                }
            }

            drawer.apply {
                setAccountHeaderBackground(app.config.ui.headerBackground)

                drawerProfileListEmptyListener = {
                    onProfileListEmptyEvent(ProfileListEmptyEvent())
                }
                drawerItemSelectedListener = { id, _, item ->
                    if (item is ExpandableDrawerItem)
                        false
                    else
                        navigate(navTarget = id.asNavTargetOrNull())
                }
                drawerProfileSelectedListener = { id, _, _, _ ->
                    // why is this negated -_-
                    !navigate(profileId = id)
                }
                drawerProfileLongClickListener = { _, profile, _, view ->
                    if (view != null && profile is ProfileDrawerItem) {
                        launch {
                            val appProfile = withContext(Dispatchers.IO) {
                                App.db.profileDao().getByIdNow(profile.identifier.toInt())
                            } ?: return@launch
                            drawer.close()
                            ProfileConfigDialog(this@MainActivity, appProfile).show()
                        }
                        true
                    } else {
                        false
                    }
                }
                drawerProfileImageLongClickListener = drawerProfileLongClickListener
                drawerProfileSettingClickListener = this@MainActivity.profileSettingClickListener

                miniDrawerVisibleLandscape = null
                miniDrawerVisiblePortrait = app.config.ui.miniMenuVisible
            }
        }

        navTarget = NavTarget.HOME

        if (savedInstanceState != null) {
            intent?.putExtras(savedInstanceState)
            savedInstanceState.clear()
        }

        app.db.profileDao().all.observe(this) { profiles ->
            val allArchived = profiles.all { it.archived }
            drawer.setProfileList(profiles.filter {
                it.id >= 0 && (!it.archived || allArchived)
            }.toMutableList())
            //prepend the archived profile if loaded
            if (app.profile.archived && !allArchived) {
                drawer.prependProfile(Profile(
                    id = app.profile.id,
                    loginStoreId = app.profile.loginStoreId,
                    loginStoreType = app.profile.loginStoreType,
                    name = app.profile.name,
                    subname = "Archiwum - ${app.profile.subname}"
                ).also {
                    it.archived = true
                })
            }
            drawer.currentProfile = App.profileId
        }

        setDrawerItems()

        handleIntent(intent?.extras)

        app.db.metadataDao().unreadCounts.observe(this) { unreadCounters ->
            drawer.setUnreadCounterList(unreadCounters)
        }

        b.swipeRefreshLayout.isEnabled = true
        b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } }
        b.swipeRefreshLayout.setColorSchemeResources(
            R.color.md_blue_500,
            R.color.md_amber_500,
            R.color.md_green_500
        )

        SyncWorker.scheduleNext(app)
        UpdateWorker.scheduleNext(app)

        // if loaded profile is archived, switch to the up-to-date version of it
        if (app.profile.archived) {
            launch {
                if (app.profile.archiveId != null) {
                    val profile = withContext(Dispatchers.IO) {
                        app.db.profileDao().getNotArchivedOf(app.profile.archiveId!!)
                    }
                    if (profile != null)
                        navigate(profile = profile)
                    else
                        navigate(profileId = 0)
                } else {
                    navigate(profileId = 0)
                }
            }
        }

        // APP BACKGROUND
        setAppBackground()

        // IT'S WINTER MY DUDES
        val today = Date.getToday()
        if ((today.month % 11 == 1) && app.config.ui.snowfall) {
            b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
        } else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
            val eggfall = layoutInflater.inflate(
                R.layout.eggfall,
                b.rootFrame,
                false
            ) as SnowfallView
            eggfall.setSnowflakeBitmaps(listOf(
                BitmapFactory.decodeResource(resources, R.drawable.egg1),
                BitmapFactory.decodeResource(resources, R.drawable.egg2),
                BitmapFactory.decodeResource(resources, R.drawable.egg3),
                BitmapFactory.decodeResource(resources, R.drawable.egg4),
                BitmapFactory.decodeResource(resources, R.drawable.egg5),
                BitmapFactory.decodeResource(resources, R.drawable.egg6)
            ))
            b.rootFrame.addView(eggfall)
        }

        // WHAT'S NEW DIALOG
        if (app.config.appVersion < BuildConfig.VERSION_CODE) {
            // force an AppSync after update
            app.config.sync.lastAppSync = 0L
            ChangelogDialog(this).show()
            if (app.config.appVersion < 170) {
                //Intent intent = new Intent(this, ChangelogIntroActivity.class);
                //startActivity(intent);
            } else {
                app.config.appVersion = BuildConfig.VERSION_CODE
            }
        }

        // RATE SNACKBAR
        if (app.config.appRateSnackbarTime != 0L && app.config.appRateSnackbarTime <= System.currentTimeMillis()) {
            navView.coordinator.postDelayed({
                CafeBar.builder(this)
                    .content(R.string.rate_snackbar_text)
                    .icon(IconicsDrawable(this).apply {
                        icon = CommunityMaterial.Icon3.cmd_star_outline
                        sizeDp = 24
                        colorInt = Themes.getPrimaryTextColor(this@MainActivity)
                    })
                    .positiveText(R.string.rate_snackbar_positive)
                    .positiveColor(-0xb350b0)
                    .negativeText(R.string.rate_snackbar_negative)
                    .negativeColor(0xff666666.toInt())
                    .neutralText(R.string.rate_snackbar_neutral)
                    .neutralColor(0xff666666.toInt())
                    .onPositive { cafeBar ->
                        Utils.openGooglePlay(this)
                        cafeBar.dismiss()
                        app.config.appRateSnackbarTime = 0
                    }
                    .onNegative { cafeBar ->
                        Toast.makeText(this,
                            R.string.rate_snackbar_negative_message,
                            Toast.LENGTH_LONG).show()
                        cafeBar.dismiss()
                        app.config.appRateSnackbarTime = 0
                    }
                    .onNeutral { cafeBar ->
                        Toast.makeText(this, R.string.ok, Toast.LENGTH_LONG).show()
                        cafeBar.dismiss()
                        app.config.appRateSnackbarTime =
                            System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000
                    }
                    .autoDismiss(false)
                    .swipeToDismiss(true)
                    .floating(true)
                    .show()
            }, 10000)
        }

        // CONTEXT MENU ITEMS
        bottomSheet.removeAllItems()
        bottomSheet.appendItems(
            BottomSheetPrimaryItem(false)
                .withTitle(R.string.menu_sync)
                .withIcon(CommunityMaterial.Icon.cmd_download_outline)
                .withOnClickListener {
                    bottomSheet.close()
                    SyncViewListDialog(this, navTarget).show()
                },
            BottomSheetSeparatorItem(false),
        )
        for (target in NavTarget.values()) {
            if (target.location != NavTargetLocation.BOTTOM_SHEET)
                continue
            if (target.devModeOnly && !App.devMode)
                continue
            bottomSheet += target.toBottomSheetItem(this)
        }
    }

    private var profileSettingClickListener = { itemId: Int, _: View? ->
        when (val item = itemId.asNavTarget()) {
            NavTarget.PROFILE_ADD -> {
                requestHandler.requestLogin()
            }
            NavTarget.PROFILE_SYNC_ALL -> {
                EdziennikTask.sync().enqueue(this)
            }
            NavTarget.PROFILE_MARK_AS_READ -> {
                launch {
                    withContext(Dispatchers.Default) {
                        app.db.profileDao().allNow.forEach { profile ->
                            if (!profile.getAppData().uiConfig.enableMarkAsReadAnnouncements)
                                app.db.metadataDao()
                                    .setAllSeenExceptMessagesAndAnnouncements(profile.id, true)
                            else
                                app.db.metadataDao().setAllSeenExceptMessages(profile.id, true)
                        }
                    }
                    Toast.makeText(this@MainActivity,
                        R.string.main_menu_mark_as_read_success,
                        Toast.LENGTH_SHORT).show()
                }
            }
            else -> {
                navigate(navTarget = item)
            }
        }
        false
    }

    /*     _____
          / ____|
         | (___  _   _ _ __   ___
          \___ \| | | | '_ \ / __|
          ____) | |_| | | | | (__
         |_____/ \__, |_| |_|\___|
                  __/ |
                 |__*/
    private suspend fun syncCurrentFeature() {
        if (app.profile.archived) {
            MaterialAlertDialogBuilder(this)
                .setTitle(R.string.profile_archived_title)
                .setMessage(
                    R.string.profile_archived_text,
                    app.profile.studentSchoolYearStart,
                    app.profile.studentSchoolYearStart + 1
                )
                .setPositiveButton(R.string.ok, null)
                .show()
            swipeRefreshLayout.isRefreshing = false
            return
        }
        if (app.profile.shouldArchive()) {
            MaterialAlertDialogBuilder(this)
                .setTitle(R.string.profile_archiving_title)
                .setMessage(
                    R.string.profile_archiving_format,
                    app.profile.dateYearEnd.formattedString
                )
                .setPositiveButton(R.string.ok, null)
                .show()
        }
        if (app.profile.isBeforeYear()) {
            MaterialAlertDialogBuilder(this)
                .setTitle(R.string.profile_year_not_started_title)
                .setMessage(
                    R.string.profile_year_not_started_format,
                    app.profile.dateSemester1Start.formattedString
                )
                .setPositiveButton(R.string.ok, null)
                .show()
            swipeRefreshLayout.isRefreshing = false
            return
        }

        val error = withContext(Dispatchers.IO) {
            app.availabilityManager.check(app.profile)
        }
        when (error?.type) {
            Type.NOT_AVAILABLE -> {
                swipeRefreshLayout.isRefreshing = false
                navigate(navTarget = NavTarget.HOME)
                RegisterUnavailableDialog(this, error.status!!).show()
                return
            }
            Type.API_ERROR -> {
                errorSnackbar.addError(error.apiError!!).show()
                return
            }
            Type.NO_API_ACCESS -> {
                Toast.makeText(this, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
            }
            else -> {}
        }

        swipeRefreshLayout.isRefreshing = true
        Toast.makeText(this, fragmentToSyncName(navTarget), Toast.LENGTH_SHORT).show()
        val featureType = when (navTarget) {
            NavTarget.MESSAGES -> when (MessagesFragment.pageSelection) {
                Message.TYPE_SENT -> FeatureType.MESSAGES_SENT
                else -> FeatureType.MESSAGES_INBOX
            }
            else -> navTarget.featureType
        }
        val arguments = when (navTarget) {
            NavTarget.TIMETABLE -> JsonObject("weekStart" to TimetableFragment.pageSelection?.weekStart?.stringY_m_d)
            else -> null
        }
        EdziennikTask.syncProfile(
            App.profileId,
            featureType?.let { setOf(it) },
            arguments = arguments
        ).enqueue(this)
    }

    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onUpdateEvent(event: Update) {
        EventBus.getDefault().removeStickyEvent(event)
        UpdateAvailableDialog(this, event).show()
    }

    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onUpdateStateEvent(event: UpdateStateEvent) {
        if (!event.running)
            return
        EventBus.getDefault().removeStickyEvent(event)
        UpdateProgressDialog(this, event.update ?: return, event.downloadId).show()
    }

    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
        EventBus.getDefault().removeStickyEvent(event)
        val error = app.availabilityManager.check(app.profile, cacheOnly = true)
        if (error != null) {
            RegisterUnavailableDialog(this, error.status!!).show()
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) {
        swipeRefreshLayout.isRefreshing = true
        if (event.profileId == App.profileId) {
            navView.toolbar.apply {
                subtitleFormat = null
                subtitleFormatWithUnread = null
                subtitle = getString(R.string.toolbar_subtitle_syncing)
            }
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onProfileListEmptyEvent(event: ProfileListEmptyEvent) {
        d(TAG, "Profile list is empty. Launch LoginActivity.")
        app.config.loginFinished = false
        startActivity(Intent(this, LoginActivity::class.java))
        finish()
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onApiTaskProgressEvent(event: ApiTaskProgressEvent) {
        if (event.profileId == App.profileId) {
            navView.toolbar.apply {
                subtitleFormat = null
                subtitleFormatWithUnread = null
                subtitle = if (event.progress < 0f)
                    event.progressText ?: ""
                else
                    getString(
                        R.string.toolbar_subtitle_syncing_format,
                        event.progress.roundToInt(),
                        event.progressText ?: "",
                    )

            }
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) {
        EventBus.getDefault().removeStickyEvent(event)
        if (event.profileId == App.profileId) {
            navView.toolbar.apply {
                subtitleFormat = R.string.toolbar_subtitle
                subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
                subtitle = "Gotowe"
            }
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) {
        EventBus.getDefault().removeStickyEvent(event)
        swipeRefreshLayout.isRefreshing = false
    }

    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) {
        EventBus.getDefault().removeStickyEvent(event)
        if (event.error.errorCode == ERROR_VULCAN_API_DEPRECATED) {
            if (event.error.profileId != App.profileId)
                return
            ErrorDetailsDialog(this, listOf(event.error)).show()
        }
        navView.toolbar.apply {
            subtitleFormat = R.string.toolbar_subtitle
            subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
            subtitle = "Gotowe"
        }
        mainSnackbar.dismiss()
        errorSnackbar.addError(event.error).show()
    }

    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    fun onAppManagerDetectedEvent(event: AppManagerDetectedEvent) {
        EventBus.getDefault().removeStickyEvent(event)
        if (app.config.sync.dontShowAppManagerDialog)
            return
        MaterialAlertDialogBuilder(this)
            .setTitle(R.string.app_manager_dialog_title)
            .setMessage(R.string.app_manager_dialog_text)
            .setPositiveButton(R.string.ok) { _, _ ->
                try {
                    for (intent in appManagerIntentList) {
                        if (packageManager.resolveActivity(intent,
                                PackageManager.MATCH_DEFAULT_ONLY) != null
                        ) {
                            startActivity(intent)
                        }
                    }
                } catch (e: Exception) {
                    try {
                        startActivity(Intent(Settings.ACTION_SETTINGS))
                    } catch (e: Exception) {
                        e.printStackTrace()
                        Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT)
                            .show()
                    }
                }
            }
            .setNeutralButton(R.string.dont_ask_again) { _, _ ->
                app.config.sync.dontShowAppManagerDialog = true
            }
            .setCancelable(false)
            .show()
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
        app.userActionManager.execute(this, event, UserActionManager.UserActionCallback())
    }

    private fun fragmentToSyncName(navTarget: NavTarget): Int {
        return when (navTarget) {
            NavTarget.TIMETABLE -> R.string.sync_feature_timetable
            NavTarget.AGENDA -> R.string.sync_feature_agenda
            NavTarget.GRADES -> R.string.sync_feature_grades
            NavTarget.HOMEWORK -> R.string.sync_feature_homework
            NavTarget.BEHAVIOUR -> R.string.sync_feature_notices
            NavTarget.ATTENDANCE -> R.string.sync_feature_attendance
            NavTarget.MESSAGES -> when (MessagesFragment.pageSelection) {
                Message.TYPE_SENT -> R.string.sync_feature_messages_outbox
                else -> R.string.sync_feature_messages_inbox
            }
            NavTarget.ANNOUNCEMENTS -> R.string.sync_feature_announcements
            else -> R.string.sync_feature_syncing_all
        }
    }

    /*    _____       _             _
         |_   _|     | |           | |
           | |  _ __ | |_ ___ _ __ | |_ ___
           | | | '_ \| __/ _ \ '_ \| __/ __|
          _| |_| | | | ||  __/ | | | |_\__ \
         |_____|_| |_|\__\___|_| |_|\__|__*/
    private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            handleIntent(intent?.extras)
        }
    }

    fun handleIntent(extras: Bundle?) {
        d(TAG, "handleIntent() {")
        extras?.keySet()?.forEach { key ->
            d(TAG, "    \"$key\": " + extras.get(key))
        }
        d(TAG, "}")

        val intentProfileId = extras.getIntOrNull("profileId").takePositive()
        var intentNavTarget = extras.getIntOrNull("fragmentId").asNavTargetOrNull()

        if (extras?.containsKey("action") == true) {
            val handled = when (extras.getString("action")) {
                "updateRequest" -> {
                    UpdateAvailableDialog(this, app.config.update).show()
                    true
                }
                "serverMessage" -> {
                    ServerMessageDialog(
                        this,
                        extras.getString("serverMessageTitle") ?: getString(R.string.app_name),
                        extras.getString("serverMessageText") ?: ""
                    ).show()
                    true
                }
                "feedbackMessage" -> {
                    intentNavTarget = NavTarget.FEEDBACK
                    false
                }
                "userActionRequired" -> {
                    val event = UserActionRequiredEvent(
                        profileId = extras.getInt("profileId"),
                        type = extras.getEnum<UserActionRequiredEvent.Type>("type") ?: return,
                        params = extras.getBundle("params") ?: return,
                        errorText = 0,
                    )
                    app.userActionManager.execute(this,
                        event,
                        UserActionManager.UserActionCallback())
                    true
                }
                "createManualEvent" -> {
                    val date = extras.getString("eventDate")
                        ?.let { Date.fromY_m_d(it) }
                        ?: Date.getToday()
                    EventManualDialog(
                        this,
                        App.profileId,
                        defaultDate = date
                    ).show()
                    true
                }
                else -> false
            }
            if (handled && !navLoading) {
                return
            }
        }

        if (extras?.containsKey("reloadProfileId") == true) {
            val reloadProfileId = extras.getIntOrNull("reloadProfileId").takePositive()
            if (reloadProfileId == null || app.profile.id == reloadProfileId) {
                reloadTarget()
                return
            }
        }

        extras?.remove("profileId")
        extras?.remove("fragmentId")
        extras?.remove("reloadProfileId")

        /*if (intentTargetId == -1 && navController.currentDestination?.id == R.id.loadingFragment) {
            intentTargetId = navTarget.id
        }*/

        if (navLoading)
            b.fragment.removeAllViews()

        when {
            app.profile.id == 0 -> navigate(
                profileId = intentProfileId ?: app.config.lastProfileId,
                navTarget = intentNavTarget,
                args = extras,
            )
            intentProfileId != null -> navigate(
                profileId = intentProfileId,
                navTarget = intentNavTarget,
                args = extras,
            )
            intentNavTarget != null -> navigate(
                navTarget = intentNavTarget,
                args = extras,
            )
            navLoading -> navigate()
            else -> drawer.currentProfile = app.profile.id
        }
        navLoading = false
    }

    override fun recreate() {
        recreate(navTarget)
    }

    fun recreate(navTarget: NavTarget) {
        recreate(navTarget, null)
    }

    fun recreate(navTarget: NavTarget? = null, arguments: Bundle? = null) {
        val intent = Intent(this, MainActivity::class.java)
        if (arguments != null)
            intent.putExtras(arguments)
        if (navTarget != null) {
            intent.putExtra("fragmentId", navTarget.id)
        }
        finish()
        overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
        startActivity(intent)
    }

    override fun onStart() {
        d(TAG, "Activity started")
        super.onStart()
    }

    override fun onStop() {
        d(TAG, "Activity stopped")
        super.onStop()
    }

    override fun onResume() {
        d(TAG, "Activity resumed")
        val filter = IntentFilter()
        filter.addAction(Intent.ACTION_MAIN)
        registerReceiver(intentReceiver, filter)
        EventBus.getDefault().register(this)
        super.onResume()
    }

    override fun onPause() {
        d(TAG, "Activity paused")
        unregisterReceiver(intentReceiver)
        EventBus.getDefault().unregister(this)
        super.onPause()
    }

    override fun onDestroy() {
        d(TAG, "Activity destroyed")
        super.onDestroy()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putExtras("fragmentId" to navTarget)
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        handleIntent(intent?.extras)
    }

    @Suppress("deprecation")
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        requestHandler.handleResult(requestCode, resultCode, data)
    }

    /*    _                     _                  _   _               _
         | |                   | |                | | | |             | |
         | |     ___   __ _  __| |  _ __ ___   ___| |_| |__   ___   __| |___
         | |    / _ \ / _` |/ _` | | '_ ` _ \ / _ \ __| '_ \ / _ \ / _` / __|
         | |___| (_) | (_| | (_| | | | | | | |  __/ |_| | | | (_) | (_| \__ \
         |______\___/ \__,_|\__,_| |_| |_| |_|\___|\__|_| |_|\___/ \__,_|__*/
    val navOptions = NavOptions.Builder()
        .setEnterAnim(R.anim.task_open_enter) // new fragment enter
        .setExitAnim(R.anim.task_open_exit) // old fragment exit
        .setPopEnterAnim(R.anim.task_close_enter) // old fragment enter back
        .setPopExitAnim(R.anim.task_close_exit) // new fragment exit
        .build()

    private fun canNavigate(): Boolean = onBeforeNavigate?.invoke() != false

    fun resumePausedNavigation(): Boolean {
        val data = pausedNavigationData ?: return false
        navigate(
            profileId = data.profileId,
            navTarget = data.navTarget,
            args = data.args,
            skipBeforeNavigate = true,
        )
        pausedNavigationData = null
        return true
    }

    fun navigate(
        profileId: Int? = null,
        profile: Profile? = null,
        navTarget: NavTarget? = null,
        args: Bundle? = null,
        skipBeforeNavigate: Boolean = false,
    ): Boolean {
        d(TAG, "navigate(profileId = ${profile?.id ?: profileId}, target = ${navTarget?.name}, args = $args)")
        if (!(skipBeforeNavigate || navTarget == this.navTarget) && !canNavigate()) {
            bottomSheet.close()
            drawer.close()
            // restore the previous profile if changing it with the drawer
            // well, it still does not change the toolbar profile image,
            // but that's now NavView's problem, not mine.
            drawer.currentProfile = App.profile.id
            pausedNavigationData = PausedNavigationData(profileId, navTarget, args)
            return false
        }

        val loadNavTarget = navTarget ?: this.navTarget
        if (profile != null && profile.id != App.profileId) {
            navigateImpl(profile, loadNavTarget, args, profileChanged = true)
            return true
        }
        if (profileId != null && profileId != App.profileId) {
            app.profileLoad(profileId) {
                navigateImpl(it, loadNavTarget, args, profileChanged = true)
            }
            return true
        }
        navigateImpl(App.profile, loadNavTarget, args, profileChanged = false)
        return true
    }

    private fun navigateImpl(
        profile: Profile,
        navTarget: NavTarget,
        args: Bundle?,
        profileChanged: Boolean,
    ) {
        d(TAG, "navigateImpl(profileId = ${profile.id}, target = ${navTarget.name}, args = $args)")

        if (navTarget.featureType != null && !profile.hasUIFeature(navTarget.featureType)) {
            navigateImpl(profile, NavTarget.HOME, args, profileChanged)
            return
        }

        if (profileChanged) {
            if (App.profileId != profile.id)
                app.profileLoad(profile)
            MessagesFragment.pageSelection = -1
            // set new drawer items for this profile
            setDrawerItems()

            val previousArchivedId = if (app.profile.archived) app.profile.id else null
            if (previousArchivedId != null) {
                // prevents accidentally removing the first item if the archived profile is not shown
                drawer.removeProfileById(previousArchivedId)
            }
            if (profile.archived) {
                // add the same profile but with a different name
                // (other fields are not needed by the drawer)
                drawer.prependProfile(Profile(
                    id = profile.id,
                    loginStoreId = profile.loginStoreId,
                    loginStoreType = profile.loginStoreType,
                    name = profile.name,
                    subname = "Archiwum - ${profile.subname}"
                ).also {
                    it.archived = true
                })
            }

            // the drawer profile is updated automatically when the drawer item is clicked
            // update it manually when switching profiles from other source
            //if (drawer.currentProfile != app.profile.id)
            drawer.currentProfile = App.profileId
        }

        val arguments = args
            ?: navBackStack.firstOrNull { it.first == navTarget }?.second
            ?: Bundle()
        bottomSheet.close()
        bottomSheet.removeAllContextual()
        bottomSheet.toggleGroupEnabled = false
        drawer.close()
        if (drawer.getSelection() != navTarget.id)
            drawer.setSelection(navTarget.id, fireOnClick = false)
        navView.toolbar.setTitle(navTarget.titleRes ?: navTarget.nameRes)
        navView.bottomBar.fabEnable = false
        navView.bottomBar.fabExtended = false
        navView.bottomBar.setFabOnClickListener(null)

        d("NavDebug", "Navigating from ${this.navTarget.name} to ${navTarget.name}")

        val fragment = navTarget.fragmentClass?.newInstance() ?: return
        fragment.arguments = arguments
        val transaction = fragmentManager.beginTransaction()

        if (navTarget == this.navTarget) {
            // just reload the current target
            transaction.setCustomAnimations(
                R.anim.fade_in,
                R.anim.fade_out
            )
        } else {
            navBackStack.keys().lastIndexOf(navTarget).let {
                if (it == -1)
                    return@let navTarget
                // pop the back stack up until that target
                transaction.setCustomAnimations(
                    R.anim.task_close_enter,
                    R.anim.task_close_exit
                )

                // navigating grades_add -> grades
                // navTarget == grades_add
                // navBackStack = [home, grades, grades_editor]
                // it == 1
                //
                // navTarget = target
                // remove 1
                // remove 2
                val popCount = navBackStack.size - it
                for (i in 0 until popCount) {
                    navBackStack.removeAt(navBackStack.lastIndex)
                }
                this.navTarget = navTarget
                this.navArguments = arguments

                return@let null
            }?.let {
                // target is neither current nor in the back stack
                // so navigate to it
                transaction.setCustomAnimations(
                    R.anim.task_open_enter,
                    R.anim.task_open_exit
                )
                navBackStack.add(this.navTarget to this.navArguments)
                this.navTarget = navTarget
                this.navArguments = arguments
            }
        }

        if (navTarget.popTo == NavTarget.HOME) {
            // if the current has popToHome, let only home be in the back stack
            // probably `if (navTarget.popToHome)` in popBackStack() is not needed now
            val popCount = navBackStack.size - 1
            for (i in 0 until popCount) {
                navBackStack.removeAt(navBackStack.lastIndex)
            }
        }

        d("NavDebug", "Current fragment ${navTarget.name}, back stack:")
        navBackStack.forEachIndexed { index, item ->
            d("NavDebug", " - $index: ${item.first.name}")
        }

        transaction.replace(R.id.fragment, fragment)
        transaction.commitAllowingStateLoss()

        // TASK DESCRIPTION
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)

            @Suppress("deprecation")
            val taskDesc = ActivityManager.TaskDescription(
                if (navTarget == NavTarget.HOME)
                    getString(R.string.app_name)
                else
                    getString(R.string.app_task_format, getString(navTarget.nameRes)),
                bm,
                getColorFromAttr(this, R.attr.colorSurface)
            )
            setTaskDescription(taskDesc)
        }
        return
    }

    fun reloadTarget() = navigate()

    private fun popBackStack(skipBeforeNavigate: Boolean = false): Boolean {
        if (navBackStack.size == 0) {
            return false
        }
        // TODO back stack argument support
        if (navTarget.popTo != null) {
            navigate(
                navTarget = navTarget.popTo,
                skipBeforeNavigate = skipBeforeNavigate,
            )
        } else {
            navBackStack.last().let {
                navigate(
                    navTarget = it.first,
                    args = it.second,
                    skipBeforeNavigate = skipBeforeNavigate,
                )
            }
        }
        return true
    }

    fun navigateUp(skipBeforeNavigate: Boolean = false) {
        if (!popBackStack(skipBeforeNavigate)) {
            super.onBackPressed()
        }
    }

    /**
     * Use the NavLib's menu button ripple to gain user attention
     * that something has changed in the bottom sheet.
     */
    fun gainAttention() {
        if (app.config.ui.bottomSheetOpened)
            return
        b.navView.postDelayed({
            navView.gainAttentionOnBottomBar()
        }, 2000)
    }

    fun gainAttentionFAB() {
        navView.bottomBar.fabExtended = false

        b.navView.postDelayed({
            navView.bottomBar.fabExtended = true
        }, 1000)

        b.navView.postDelayed({
            navView.bottomBar.fabExtended = false
        }, 3000)
    }

    fun setAppBackground() {
        try {
            b.root.background = app.config.ui.appBackground?.let {
                if (it.endsWith(".gif"))
                    GifDrawable(it)
                else
                    BitmapDrawable.createFromPath(it)
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    /*    _____                                _ _
         |  __ \                              (_) |
         | |  | |_ __ __ ___      _____ _ __   _| |_ ___ _ __ ___  ___
         | |  | | '__/ _` \ \ /\ / / _ \ '__| | | __/ _ \ '_ ` _ \/ __|
         | |__| | | | (_| |\ V  V /  __/ |    | | ||  __/ | | | | \__ \
         |_____/|_|  \__,_| \_/\_/ \___|_|    |_|\__\___|_| |_| |_|__*/
    private fun createDrawerItem(target: NavTarget, level: Int = 1): IDrawerItem<*> {
        val item = when {
            // target.subItems != null -> ExpandableDrawerItem()
            level > 1 -> SecondaryDrawerItem()
            else -> DrawerPrimaryItem()
        }

        item.also {
            it.identifier = target.id.toLong()
            it.nameRes = target.nameRes
            it.descriptionRes = target.descriptionRes ?: -1
            it.icon = target.icon?.toImageHolder()
            it.hiddenInMiniDrawer = !app.config.ui.miniMenuButtons.contains(target)
            if (it is DrawerPrimaryItem)
                it.appTitle = target.titleRes?.resolveString(this)
            if (/* it is ColorfulBadgeable && */ target.badgeType != null)
                it.badgeStyle = drawer.badgeStyle
            it.isSelectedBackgroundAnimated = false
            it.level = level
        }
        if (target.badgeType != null)
            drawer.addUnreadCounterType(target.badgeType.id, target.id)

        /* item.subItems = target.subItems?.map {
            createDrawerItem(it, level + 1)
        }?.toMutableList() ?: mutableListOf() */

        return item
    }

    fun setDrawerItems() {
        d("NavDebug", "setDrawerItems() app.profile = ${app.profile}")
        val drawerItems = arrayListOf<IDrawerItem<*>>()
        val drawerItemsMore = arrayListOf<IDrawerItem<*>>()
        val drawerItemsBottom = arrayListOf<IDrawerItem<*>>()
        val drawerProfiles = arrayListOf<ProfileSettingDrawerItem>()

        for (target in NavTarget.values()) {
            if (target.devModeOnly && !App.devMode)
                continue
            if (target.featureType != null && !app.profile.hasUIFeature(target.featureType))
                continue

            when (target.location) {
                NavTargetLocation.DRAWER -> {
                    drawerItems += createDrawerItem(target, level = 1)
                }
                NavTargetLocation.DRAWER_MORE -> {
                    drawerItemsMore += createDrawerItem(target, level = 2)
                }
                NavTargetLocation.DRAWER_BOTTOM -> {
                    drawerItemsBottom += createDrawerItem(target, level = 1)
                }
                NavTargetLocation.PROFILE_LIST -> {
                    drawerProfiles += ProfileSettingDrawerItem().also {
                        it.identifier = target.id.toLong()
                        it.nameRes = target.nameRes
                        it.descriptionRes = target.descriptionRes ?: -1
                        it.icon = target.icon?.toImageHolder()
                    }
                }
                else -> continue
            }
        }

        drawerItems += ExpandableDrawerItem().also {
            it.identifier = -1L
            it.nameRes = R.string.menu_more
            it.icon = CommunityMaterial.Icon.cmd_dots_horizontal.toImageHolder()
            it.subItems = drawerItemsMore.toMutableList()
            it.isSelectedBackgroundAnimated = false
            it.isSelectable = false
        }
        drawerItems += DividerDrawerItem()
        drawerItems += drawerItemsBottom

        // seems that this cannot be open, because the itemAdapter has Profile items
        // instead of normal Drawer items...
        drawer.profileSelectionClose()
        drawer.setItems(*drawerItems.toTypedArray())
        drawer.removeAllProfileSettings()
        drawer.addProfileSettings(*drawerProfiles.toTypedArray())
    }

    override fun onBackPressed() {
        if (App.config.ui.openDrawerOnBackPressed) {
            if (drawer.isOpen)
                navigateUp()
            else if (!navView.onBackPressed())
                drawer.open()
        } else {
            if (!navView.onBackPressed())
                navigateUp()
        }
    }

    fun error(error: ApiError) = errorSnackbar.addError(error).show()
    fun snackbar(
        text: String,
        actionText: String? = null,
        onClick: (() -> Unit)? = null,
    ) = mainSnackbar.snackbar(text, actionText, onClick)

    fun snackbarDismiss() = mainSnackbar.dismiss()
}
