package theorycrafter

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import compose.utils.*
import eve.data.EveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.jetbrains.skiko.Library
import theorycrafter.esi.refreshEveSsoTokens
import theorycrafter.fits.FitHandle
import theorycrafter.fitting.Module
import theorycrafter.tournaments.TournamentDescriptorById
import theorycrafter.ui.*
import theorycrafter.ui.fiteditor.DefaultSuggestedEveItemTypeIcon
import theorycrafter.ui.fiteditor.FitEditor
import theorycrafter.ui.fitstats.FitStats
import theorycrafter.ui.market.MiniMarketTree
import theorycrafter.ui.widgets.LocalStandardDialogs
import theorycrafter.ui.widgets.ProvideStandardDialogs
import theorycrafter.ui.widgets.StandardDialogs
import theorycrafter.utils.*
import java.awt.EventQueue
import java.io.File
import java.io.PrintStream
import java.text.SimpleDateFormat
import java.util.Date
import javax.swing.SwingUtilities
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds


/**
 * The layout for showing the status while the app is loading.
 */
@Composable
private fun SplashScreenLayout(
    content: @Composable BoxScope.() -> Unit
) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(TheorycrafterTheme.colors.base().background),
        contentAlignment = Alignment.Center,
        content = content
    )
}


/**
 * Displayed when the app is loading, with the given message.
 */
@Composable
private fun AppLoading(message: String) {
    SplashScreenLayout {
        Text(
            text = message,
            style = TheorycrafterTheme.textStyles.hugeTitle,
        )
    }
}


/**
 * Displayed when the app failed to load, with the given message.
 */
@Composable
private fun AppLoadingFailed(message: String){
    SplashScreenLayout {
        Column(
            verticalArrangement = Arrangement.spacedBy(TheorycrafterTheme.spacing.xxxlarge)
        ) {
            Text(
                text = message,
                textAlign = TextAlign.Center,
                style = TheorycrafterTheme.textStyles.hugeTitle,
            )
            TheorycrafterTheme.RaisedButtonWithText(
                text = "Close",
                modifier = Modifier.align(Alignment.CenterHorizontally),
                onClick = {
                    exitProcess(1)
                }
            )
        }
    }
}


/**
 * The app, in the given state.
 */
@Composable
private fun MainWindowContent(state: AppState) {
    when (val status = state.status) {
        is AppState.Status.Initializing -> {
            AppLoading(status.message)
        }
        is AppState.Status.WarmingUp -> {
            AppLoading("Warming up")
        }
        is AppState.Status.Initialized -> {
            MainWindowContent()
        }
        is AppState.Status.Failed -> {
            AppLoadingFailed(status.message)
        }
    }
}


/**
 * The app state holder.
 */
@Stable
private class AppState(initialStatus: Status) {


    /**
     * The app status.
     */
    var status by mutableStateOf<Status>(initialStatus)


    /**
     * The possible app statuses.
     */
    sealed interface Status {

        class Initializing(val message: String) : Status

        data object WarmingUp : Status

        data object Initialized : Status

        class Failed(exception: Throwable): Status {

            val message = """
            Error loading app
            ${exception.message ?: exception.javaClass.simpleName}
            """.trimIndent()

        }

    }


}


/**
 * Provides a [WindowExceptionHandlerFactory] that writes the exception to a file.
 */
@Composable
@ExperimentalComposeUiApi
private fun ProvideCompositionExceptionsLogger(content: @Composable () -> Unit) {
    ProvideAdditionalWindowExceptionHandler(
        handler = { throwable ->
            @Suppress("SimpleDateFormat")
            val timestamp = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(Date())
            File(LOGS_DIR, "crashlog_${timestamp}.txt").writeText(throwable.stackTraceToString())
        },
        content = content
    )
}


@OptIn(ExperimentalComposeUiApi::class)
fun main() = runBlocking {
    // If an exception is thrown while checking if another instance is running, continue as usual because it's better
    // than being unable to run the app because of some unexpected error.
    kotlin.runCatching {
        // Check if another instance is already running
        if (!SingleAppInstanceManager.ensureFirstAppInstance("Theorycrafter")) {
            // Another instance is already running and has been signaled
            println("Application is already running; shutting down this one")
            exitProcess(0)
        }
    }

    createUserDirs()
    configureStdStreams()

    // Start loading skiko in parallel.
    // We need to load skiko before using TheorycrafterTheme, because the theme uses skiko's currentSystemTheme,
    // which doesn't by itself load skiko, unfortunately.
    // Also it's probably a good way to speed up startup anyway.
    val skikoLoading = launch(Dispatchers.IO) {
        timeAction("Loading skiko") {
            Library.load()
        }
    }

    // Start loading EveData in parallel
    val eveDataDeferred = async(Dispatchers.IO) {
        timeAction("Loading EVE data") {
            EveData.loadStandard()
        }
    }

    // Load settings before opening any windows, as their bounds are determined by the settings.
    TheorycrafterContext.settings = timeAction("Loading settings") {
        TheorycrafterSettings.load()
    }

    // Wait for skiko to complete loading before proceeding
    skikoLoading.join()

    // For old style shadows
    ComposeUiFlags.useLegacyRenderNodeLayers = true

    application {
        ProvideCompositionExceptionsLogger {
            val appState: AppState = remember { AppState(AppState.Status.Initializing("Initializing")) }
            LaunchedEffect(Unit) {
                runCatchingExceptCancellation {
                    TheorycrafterContext.initialize(
                        eveDataDeferred = eveDataDeferred,
                        onProgress = { message ->
                            appState.status = AppState.Status.Initializing(message)
                        },
                    )
                    appState.status = AppState.Status.WarmingUp
                }.onFailure {
                    it.printStackTrace()
                    appState.status = AppState.Status.Failed(it)
                }
            }

            TheorycrafterWindows(appState)

            LaunchOnStartupIoProcesses(appState)
        }
    }
}


/**
 * Configures stdout and stderr to also write to a log file.
 */
private fun configureStdStreams() {
    val logFile = File(LOGS_DIR, "log.txt")
    val logFileStream = PrintStream(logFile.outputStream())
    System.setOut(PrintStream(TeeOutputStream(System.out, logFileStream)))
    System.setErr(PrintStream(TeeOutputStream(System.err, logFileStream)))
}


/**
 * The minimum size of the main window.
 */
private val MainWindowMinimumSize = DpSize(1300.dp, 600.dp)


/**
 * Displays all the Theorycrafter windows.
 */
@Composable
private fun ApplicationScope.TheorycrafterWindows(appState: AppState) {
    val mainWindowState = rememberWindowStateAndUpdateSettings(
        windowSettings = TheorycrafterContext.settings.mainWindow,
        defaultPosition = { WindowPosition.PlatformDefault },
        defaultSize = { TheorycrafterTheme.sizes.mainWindowDefaultSize },
        minimumSize = MainWindowMinimumSize
    )

    val coroutineScope = rememberCoroutineScope()
    val windowManager = remember {
        TheorycrafterWindowManager(mainWindowState, coroutineScope)
    }
    CompositionLocalProvider(LocalTheorycrafterWindowManager provides windowManager) {
        TheorycrafterTheme {
            MainWindow(
                appState = appState,
                windowState = mainWindowState,
            )

            // Show secondary windows after a short delay.
            // This is a workaround for focus issues when both the main window
            // and a secondary window request focus. For example, the fit
            // search field in the main window and the composition search field
            // in the tournament window.
            if (appState.status == AppState.Status.Initialized) {
                AfterDelay(200.milliseconds) {
                    windowManager.AllSecondaryWindows()
                }
            }
        }
    }
    LaunchedEffect(windowManager) {
        SingleAppInstanceManager.setBringToFrontCallback {
            SwingUtilities.invokeLater {
                windowManager.bringMainWindowToFront()
            }
        }
    }
}


/**
 * Handles uncaught exceptions in the main window.
 */
private class MainWindowExceptionHandler {


    /**
     * Whether an exception has been thrown in the main window.
     */
    var exceptionThrown: Boolean = false


    /**
     * Provides the [WindowExceptionHandlerFactory] for the main window.
     */
    @OptIn(ExperimentalComposeUiApi::class)
    @Composable
    fun ProvideMainWindowExceptionHandlerFactory(content: @Composable () -> Unit) {
        ProvideAdditionalWindowExceptionHandler(
            handler = remember {
                WindowExceptionHandler {
                    // When an exception is thrown in the main window, it's typically because something unexpected
                    // happened w.r.t. the displayed fit. To avoid having the app crash every time it's opened,
                    // we reset the displayed fit.
                    // Note that Compose doesn't currently allow to recover from an uncaught exception, but at least the
                    // app will not crash again on the next run.
                    TheorycrafterContext.settings.mainWindow.fitHandle = null

                    // Mark that an exception has been thrown
                    exceptionThrown = true
                }
            },
            content = content
        )
    }

}


/**
 * The main Theorycrafter window.
 */
@Composable
private fun ApplicationScope.MainWindow(
    appState: AppState,
    windowState: WindowState,
) {
    val activeTournamentId = TheorycrafterContext.settings.activeTournamentId
    val windowTitle = remember(activeTournamentId) {
        val tournamentDesc = activeTournamentId?.let { TournamentDescriptorById[it] }
        if (tournamentDesc == null)
            Theorycrafter.AppName
        else
            "${Theorycrafter.AppName}  ·  ${tournamentDesc.name}"
    }
    lateinit var dialogs: StandardDialogs
    val exceptionHandler = remember { MainWindowExceptionHandler() }
    exceptionHandler.ProvideMainWindowExceptionHandlerFactory {
        TheorycrafterWindow(
            onCloseRequest = {
                if (exceptionHandler.exceptionThrown) {
                    exitApplication()
                } else {
                    dialogs.showConfirmDialog(
                        text = "Quit Theorycrafter?",
                        confirmText = "Quit",
                        onConfirm = {
                            TheorycrafterContext.close()
                            exitApplication()
                        }
                    )
                }
            },
            title = windowTitle,
            state = windowState,
            minimumSize = MainWindowMinimumSize
        ) {
            dialogs = LocalStandardDialogs.current
            MainWindowContent(appState)

            val windowManager = LocalTheorycrafterWindowManager.current
            LaunchedEffect(windowManager, window) {
                windowManager.mainWindow = window
            }

            // The warmup is done in a separate step, and inside `Window` because it blocks the UI thread,
            // but we want to draw the "Warming up" message before it begins.
            if (appState.status == AppState.Status.WarmingUp) {
                val density = LocalDensity.current
                LaunchedEffect(Unit) {
                    delay(16)  // Delay to allow the "Warming up" text to get drawn
                    runCatching {
                        TheorycrafterContext.warmup(density)
                        appState.status = AppState.Status.Initialized
                        windowManager.onAppReady()
                    }.onFailure {
                        it.printStackTrace()
                        appState.status = AppState.Status.Failed(it)
                    }
                }
            }

            AboutDialog(window)
        }
    }
}


/**
 * Creates and destroys some fits, to warm up the JVM.
 */
suspend fun TheorycrafterContext.warmup(density: Density) {

    if (!EventQueue.isDispatchThread()) {
        throw IllegalStateException(
            "Warmup must be done on the main thread because ImageComposeScene can't " +
                    "be used concurrently with another composition"
        )
    }
    timeAction("Warmup") {
        val caracal = eveData.shipType("Caracal")
        val neut = eveData.moduleType("Small Energy Neutralizer I")
        val smallCapacitorBooster = eveData.moduleType("Small Capacitor Booster I")
        val capBoosterCharge = eveData.chargeType("Cap Booster 100")
        val damageControl = eveData.moduleType("Damage Control I")
        fits.withTemporaryFit(caracal) {
            repeat(3) {
                val fit = modify {
                    val fit = newFit(caracal)
                    fit.fitModule(neut, slotIndex = 0).also { it.setState(Module.State.ACTIVE) }
                    fit.fitModule(smallCapacitorBooster, slotIndex = 0).also {
                        it.setCharge(capBoosterCharge)
                        it.setState(Module.State.ACTIVE)
                    }
                    fit.fitModule(damageControl, slotIndex = 0)
                    fit
                }
                renderComposeScene(
                    width = 1000,
                    height = 1000,
                    density = density
                ) {
                    OffscreenApplicationUi {
                        FitEditor(fit)
                        FitStats(fit)
                        MiniMarketTree(
                            toplevelMarketGroups = with(eveData.marketGroups) {
                                listOf(shipEquipment)
                            },
                            toplevelTitle = "Warmup",
                            itemFilter = { true },
                            onItemPressed = {},
                            itemContent = { item ->
                                DefaultSuggestedEveItemTypeIcon(item)
                                Text(item.name)
                            }
                        )
                    }
                }
                modify {
                    fit.remove()
                }
            }
        }
    }
}


/**
 * A hack to work around the window flashing its background color when closed
 * (https://youtrack.jetbrains.com/issue/CMP-5651).
 */
@Composable
fun WindowScope.windowBackgroundFlashingOnCloseWorkaround() {
    val backgroundColor = TheorycrafterTheme.colors.base().background
    LaunchedEffect(window, backgroundColor) {
        window.background = java.awt.Color(backgroundColor.toArgb())
    }
}


/**
 * Provides all the composition locals needed by the UI to render off-screen.
 *
 * This is used by tests, and the application warmup in [TheorycrafterContext.warmup]
 */
@Composable
fun OffscreenApplicationUi(content: @Composable () -> Unit) {
    val fitOpener = object: FitOpener {
        override val canOpenFitsInCurrentWindow: Boolean
            get() = true

        override fun openFitInCurrentWindow(fitHandle: FitHandle) { }
        override fun openFitInSecondaryWindow(fitHandle: FitHandle) { }
    }

    val coroutineScope = rememberCoroutineScope()
    val mainWindowState = WindowState(LocalWindowInfo.current, WindowPosition(0.dp, 0.dp), LocalDensity.current)
    val windowManager = remember {
        TheorycrafterWindowManager(mainWindowState, coroutineScope)
    }

    TheorycrafterTheme {
        CompositionLocalProvider(
            LocalTheorycrafterWindowManager provides windowManager,
            LocalWindowState provides mainWindowState,
            LocalSnackbarHost provides remember { SnackbarHostState() },
            LocalFitOpener provides fitOpener,
            LocalKeyShortcutsManager provides remember { KeyShortcutsManager() }
        ) {
            ProvideStandardDialogs {
                TooltipHost {
                    content()
                }
            }
        }
    }
}


/**
 * Launches the I/O processes we want to run on app startup.
 */
@Composable
private fun LaunchOnStartupIoProcesses(appState: AppState) {
    LaunchedEffect(Unit) {
        snapshotFlow { appState.status }.collectLatest {
            if (it is AppState.Status.Initialized) {
                withContext(Dispatchers.IO) {
                    launch {
                        periodicallyRefreshEveSsoTokens()
                    }
                    launch {
                        checkLatestTheorycrafterReleases()
                    }
                    launch {
                        reportAppLaunchAndUse()
                    }
                    launch {
                        keepEveItemPricesUpdated(this)
                    }
                }
            }
        }
    }
}


/**
 * Periodically refreshes the EVE SSO tokens in the background.
 */
private suspend fun periodicallyRefreshEveSsoTokens() {
    delay(30.seconds)
    while (true) {
        val skillSets = TheorycrafterContext.skillSets
        val time = System.currentTimeMillis()
        for (skillSet in skillSets.userHandles) {
            if (!skillSet.isCharacterSkillSet)  // Not a character skill set
                continue
            if (time < skillSet.ssoTokens.lastRefreshedUtcMillis + 1.days.inWholeMilliseconds)  // Refreshed recently
                continue

            val charName = skillSet.ssoTokens.characterName
            println("Refreshing SSO tokens for \"$charName\"")
            val result = refreshEveSsoTokens(skillSet.ssoTokens).result
            if (result.isSuccess) {
                println("Successfully refreshed SSO tokens for \"$charName\"")
                skillSets.setEveSsoTokens(skillSet, result.value())
            } else {
                println("Failed to refresh SSO tokens for \"$charName\": ${result.failure()}")
            }
        }
        delay(1.days)
    }
}


/**
 * Checks the latest Theorycrafter releases.
 */
private suspend fun checkLatestTheorycrafterReleases() {
    delay(20.seconds)
    while (true) {
        lateinit var releases: TheorycrafterReleases
        while (true) {
            println("Fetching latest release versions from ${Theorycrafter.ReleasesInfoUrl}")
            val result = loadLatestTheorycrafterReleases()
            if (result.isSuccess) {
                releases = result.value()
                println("Successfully fetched latest release versions")
                break
            } else {
                System.err.println("Failed to fetch latest release versions: ${result.failure()}")
                delay((10..30).random().minutes)
            }
        }
        TheorycrafterContext.settings.theorycrafterReleases.updateReleaseVersionCodes(releases)
        delay(1.days)
    }
}


/**
 * Notifies analytics that the app started, after a short delay, and then app-use every 24 hours.
 */
private suspend fun reportAppLaunchAndUse() {
    fun userConsented() = TheorycrafterContext.settings.analytics.consent == AnalyticsConsent.Yes

    delay(5.seconds)
    if (userConsented())
        reportAppLaunch()
    while (true) {
        delay(1.days)
        if (userConsented())
            reportAppUse()
    }
}
