package theorycrafter

import androidx.compose.runtime.*
import androidx.compose.ui.awt.ComposeWindow
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isUnspecified
import androidx.compose.ui.window.*
import compose.input.KeyShortcut
import compose.utils.*
import compose.widgets.ViewConfigurationForDragToReorder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.hostOs
import theorycrafter.fitting.EveItem
import theorycrafter.generated.resources.Res
import theorycrafter.generated.resources.window_icon
import theorycrafter.tournaments.DoctrinesWindow
import theorycrafter.tournaments.TournamentDescriptor
import theorycrafter.ui.*
import theorycrafter.ui.graphs.GraphsWindow
import theorycrafter.ui.graphs.GraphsWindowPane
import theorycrafter.ui.graphs.GraphsWindowState
import theorycrafter.ui.market.MarketTreeWindow
import theorycrafter.ui.settings.SettingsPane
import theorycrafter.ui.settings.SettingsWindow
import theorycrafter.ui.settings.SettingsWindowState
import theorycrafter.ui.tournaments.TournamentWindow
import theorycrafter.ui.tournaments.TournamentWindowState
import theorycrafter.ui.widgets.ProvideStandardDialogs
import theorycrafter.utils.deiconifyAndBringToFront
import java.awt.Desktop
import java.awt.Dimension
import java.awt.GraphicsEnvironment
import java.awt.Toolkit
import kotlin.math.roundToInt


/**
 * Manages the windows of the app.
 */
@Stable
class TheorycrafterWindowManager(


    /**
     * The main window state.
     */
    val mainWindowState: WindowState,


    /**
     * The coroutine scope in which to run any background tasks of the window manager.
     */
    private val coroutineScope: CoroutineScope


) {


    /**
     * The application's main window.
     */
    var mainWindow: ComposeWindow? by mutableStateOf(null)


    /**
     * The app settings.
     */
    private val settings: TheorycrafterSettings
        get() = TheorycrafterContext.settings


    /**
     * The fit handle displayed in the main window.
     */
    val fitHandleInMainWindow: FitHandle?
        get() = _fitHandleInMainWindow?.let {
            // We could get called between a fit getting deleted and the deletion listener job getting called,
            // and we don't want to return a deleted fit.
            if (it.isDeleted) null else it
        }


    /**
     * The deletion listener job for the fit displayed in the main window.
     */
    private var mainWindowFitDeletionListenerJob: Job? = null


    /**
     * The backing property for [fitHandleInMainWindow]
     */
    private var _fitHandleInMainWindow: FitHandle? by mutableStateOfSetting(
        setting = settings.mainWindow::fitHandle,
        initialValue = null
        // Can't use the value of the setting here because the setting itself only knows the fit id, and to get the
        // fitHandle from it, TheorycrafterContext must be initialized (but it's not yet initialized when
        // TheorycrafterWindowManager is created
    )


    /**
     * The settings of open secondary windows (mutable version for internal use).
     */
    private val _openSecondaryFitWindowSettings = mutableStateListOf<TheorycrafterSettings.FitWindowSettings>()


    /**
     * The settings of open secondary windows.
     */
    val openSecondaryFitWindowSettings: Collection<TheorycrafterSettings.FitWindowSettings>
        get() = _openSecondaryFitWindowSettings


    /**
     * Maps the fit handles to the corresponding [SecondaryFitWindowInfo].
     */
    private val secondaryWindowInfoByFitHandle = mutableMapOf<FitHandle, SecondaryFitWindowInfo>()


    /**
     * The state of the settings window.
     *
     * This is read by the code that actually displays the window in order to show it.
     */
    var settingsWindowState: SettingsWindowState by mutableStateOf(SettingsWindowState.Closed)
        private set


    /**
     * The state of the market tree window (whether it's open).
     *
     * This is read by the code that actually displays the window in order to show it.
     */
    var marketTreeWindowState: SimpleSavedWindowState by mutableStateOf(
        if (settings.marketTreeWindow.open)
            SimpleSavedWindowState.Open(settings.marketTreeWindow)
        else
            SimpleSavedWindowState.Closed
    )
        private set


    /**
     * The state of the graphs window.
     *
     * This is read by the code that actually displays the window in order to show it.
     */
    var graphsWindowState: GraphsWindowState by mutableStateOf(GraphsWindowState.Closed)


    /**
     * The state of the tournament window.
     *
     * This is read by the code that actually displays the window in order to show it.
     */
    var tournamentWindowState: TournamentWindowState by mutableStateOf(
        run {
            val windowSettings = settings.tournamentWindow
            val tournamentDescriptor = windowSettings.tournamentDescriptor
            if (tournamentDescriptor != null)
                TournamentWindowState.Open(tournamentDescriptor, windowSettings)
            else
                TournamentWindowState.Closed
        }
    )
        private set


    /**
     * The state of the doctrines window (whether it's open).
     *
     * This is read by the code that actually displays the window in order to show it.
     */
    var doctrinesWindowState: SimpleSavedWindowState by mutableStateOf(
        if (settings.doctrinesWindow.open)
            SimpleSavedWindowState.Open(settings.doctrinesWindow)
        else
            SimpleSavedWindowState.Closed
    )
        private set


    /**
     * The displayed item info windows; for internal use.
     */
    private var _itemInfoWindowStates = mutableStateListOf<EveItemInfoWindowState>()


    /**
     * The displayed item info windows.
     */
    val itemInfoWindowStates: List<EveItemInfoWindowState>
        get() = _itemInfoWindowStates


    /**
     * Invoked when the [TheorycrafterContext] is ready.
     */
    fun onAppReady() {
        showFitInMainWindow(settings.mainWindow.fitHandle)
        for (secondaryWindow in settings.secondaryFitWindows) {
            if (secondaryWindow.fitHandle != null)
                openFitInSecondaryWindow(secondaryWindow)
        }

        with(Desktop.getDesktop()) {
            if (isSupported(Desktop.Action.APP_PREFERENCES)) {
                setPreferencesHandler {
                    showSettingsWindow()
                }
            }
        }
    }


    /**
     * Brings the main window to the front.
     */
    fun bringMainWindowToFront() = mainWindow?.deiconifyAndBringToFront()


    /**
     * Shows all the windows except for the main one.
     */
    @Composable
    fun AllSecondaryWindows() {
        FitWindows()
        MarketTreeWindow()
        GraphsWindow()
        TournamentWindow()
        DoctrinesWindow()
        SettingsWindow()
        EveItemInfoWindows()
    }


    /**
     * Closes all secondary windows and brings the main window to the front.
     */
    fun closeAllSecondaryWindows() {
        secondaryWindowInfoByFitHandle.keys.toList().forEach {
            closeSecondaryFitWindow(it)
        }
        closeMarketTreeWindow()
        closeGraphsWindow()
        closeTournamentWindow()
        closeSettingsWindow()
        itemInfoWindowStates.toList().forEach {
            closeItemInfoWindow(it)
        }

        // Bring the main window to front
        mainWindow?.deiconifyAndBringToFront()
    }


    /**
    * Shows the given fit in the main window.
     */
    fun showFitInMainWindow(fitHandle: FitHandle?) {
        // Hack to prevent the same fit from being shown in two windows at the same time
        if ((fitHandle != null) && (fitHandle in secondaryWindowInfoByFitHandle))
            closeSecondaryFitWindow(fitHandle)

        mainWindowFitDeletionListenerJob?.cancel()
        _fitHandleInMainWindow = fitHandle
        if (fitHandle != null) {
            mainWindowFitDeletionListenerJob = fitDeletionListenerJob(fitHandle) {
                showFitInMainWindow(null)
            }
        }
    }


    /**
     * Starts a job that calls the given function when the given fit has been deleted.
     */
    private fun fitDeletionListenerJob(fitHandle: FitHandle, onDeleted: () -> Unit): Job {
        return coroutineScope.launch {
            snapshotFlow { fitHandle.isDeleted }
                .collect { isDeleted ->
                    if (isDeleted) {
                        onDeleted()
                    }
                }
        }
    }


    /**
     * Opens a secondary fit window with the given settings.
     */
    private fun openFitInSecondaryWindow(windowSettings: TheorycrafterSettings.FitWindowSettings) {
        val fitHandle = windowSettings.requireFitHandle()
        if (fitHandle in secondaryWindowInfoByFitHandle) {
            System.err.println("Warning: window for fit $fitHandle already exists")
            return
        }

        _openSecondaryFitWindowSettings.add(windowSettings)

        // Listen for the fit handle being deleted, and close the window
        secondaryWindowInfoByFitHandle[fitHandle] = SecondaryFitWindowInfo(
            deletionListener = fitDeletionListenerJob(fitHandle) {
                closeSecondaryFitWindow(fitHandle)
            },
            windowSettings = windowSettings
        )

        // We can't currently show the same fit in two windows at the same time due to selection breaking, so we
        // stop showing it in the main window when it's opened in a secondary window.
        if (fitHandleInMainWindow == fitHandle) {
            _fitHandleInMainWindow = null
        }
    }


    /**
     * Opens the given fit in a secondary window.
     */
    fun openFitInSecondaryWindow(fitHandle: FitHandle) {
        val currentInfo = secondaryWindowInfoByFitHandle[fitHandle]
        if (currentInfo == null) {
            val settings = TheorycrafterContext.settings.secondaryFitWindows.onWindowAdded(fitHandle)
            openFitInSecondaryWindow(settings)
        }
        else {
            currentInfo.bringWindowToFront()
        }
    }


    /**
     * Closes the secondary window where the given fit handle is displayed.
     */
    fun closeSecondaryFitWindow(fitHandle: FitHandle) {
        val info = secondaryWindowInfoByFitHandle.remove(fitHandle)
            ?: throw IllegalArgumentException("No window associated with fit $fitHandle")
        info.deletionListener.cancel()

        TheorycrafterContext.settings.secondaryFitWindows.onWindowRemoved(info.windowSettings)
        _openSecondaryFitWindowSettings.remove(info.windowSettings)
    }


    /**
     * Associates the given [ComposeWindow] with the given fit handle (presumably it's displayed in that window).
     */
    fun registerSecondaryFitWindow(fitHandle: FitHandle, window: ComposeWindow) {
        val info = secondaryWindowInfoByFitHandle[fitHandle]
            ?: throw IllegalArgumentException("No window associated with fit $fitHandle")

        info.window = window
    }


    /**
     * Shows the settings window on the given pane.
     */
    fun showSettingsWindow(pane: SettingsPane = SettingsPane.Default) {
        when (val state = settingsWindowState) {
            is SettingsWindowState.Closed -> settingsWindowState = SettingsWindowState.Open(pane)
            is SettingsWindowState.Open -> {
                state.pane = pane
                state.bringWindowToFront()
            }
        }
    }


    /**
     * Closes the settings window.
     */
    fun closeSettingsWindow() {
        settingsWindowState = SettingsWindowState.Closed
    }


    /**
     * Shows the window backed by the given states.
     */
    private inline fun showWindow(
        getWindowState: () -> SimpleSavedWindowState,
        setWindowState: (SimpleSavedWindowState) -> Unit,
        settingsWindowState: TheorycrafterSettings.WindowSettingsWithOpenState
    ) {
        // If it's already open, just bring it to the front
        (getWindowState() as? SimpleSavedWindowState.Open)?.let {
            it.bringWindowToFront()
            return
        }

        settingsWindowState.open = true
        setWindowState(
            SimpleSavedWindowState.Open(
                windowSettings = settingsWindowState
            )
        )
    }


    /**
     * Closes the window backed by the given states.
     */
    private inline fun closeWindow(
        setWindowState: (SimpleSavedWindowState) -> Unit,
        settingsWindowState: TheorycrafterSettings.WindowSettingsWithOpenState
    ) {
        settingsWindowState.open = false
        setWindowState(SimpleSavedWindowState.Closed)
    }


    /**
     * Shows the market tree window.
     */
    fun showMarketTreeWindow() {
        showWindow(
            getWindowState = { marketTreeWindowState },
            setWindowState = { marketTreeWindowState = it },
            settingsWindowState = settings.marketTreeWindow
        )
    }


    /**
     * Closes the market tree window.
     */
    fun closeMarketTreeWindow() {
        closeWindow(
            setWindowState = { marketTreeWindowState = it },
            settingsWindowState = settings.marketTreeWindow
        )
    }


    /**
     * Shows the given pane in the graphs window.
     */
    fun showGraphsWindow(pane: GraphsWindowPane = GraphsWindowPane.Damage, displayedFit: FitHandle? = null) {
        when (val state = graphsWindowState) {
            is GraphsWindowState.Closed -> graphsWindowState = GraphsWindowState.Open(
                pane = pane,
                initialDisplayedFit = displayedFit,
                windowSettings = settings.graphs,
            )
            is GraphsWindowState.Open -> {
                state.pane = pane
                state.initialDisplayedFit = displayedFit
                state.bringWindowToFront()
            }
        }
    }


    /**
     * Closes the graphs window.
     */
    fun closeGraphsWindow() {
        graphsWindowState = GraphsWindowState.Closed
    }


    /**
     * Shows the given tournament in the tournament window.
     */
    fun showTournamentWindow(tournamentDescriptor: TournamentDescriptor) {
        (tournamentWindowState as? TournamentWindowState.Open)?.let {
            if (it.tournamentDescriptor == tournamentDescriptor) {
                it.bringWindowToFront()
                return
            }
        }

        settings.tournamentWindow.tournamentDescriptor = tournamentDescriptor
        tournamentWindowState = TournamentWindowState.Open(
            tournamentDescriptor = tournamentDescriptor,
            windowSettings = settings.tournamentWindow
        )
    }


    /**
     * Hides the tournament window.
     */
    fun closeTournamentWindow() {
        settings.tournamentWindow.tournamentDescriptor = null
        tournamentWindowState = TournamentWindowState.Closed
    }


    /**
     * Shows the doctrines window.
     */
    fun showDoctrinesWindow() {
        showWindow(
            getWindowState = { doctrinesWindowState },
            setWindowState = { doctrinesWindowState = it },
            settingsWindowState = settings.doctrinesWindow
        )
    }


    /**
     * Closes the doctrines window.
     */
    fun closeDoctrinesWindow() {
        closeWindow(
            setWindowState = { doctrinesWindowState = it },
            settingsWindowState = settings.doctrinesWindow
        )
    }


    /**
     * Shows an info window for the given EVE item.
     */
    fun showItemInfoWindow(item: EveItem<*>) {
        _itemInfoWindowStates.add(EveItemInfoWindowState(item))
    }


    /**
     * Closes the item info window.
     */
    fun closeItemInfoWindow(windowState: EveItemInfoWindowState) {
        _itemInfoWindowStates.remove(windowState)
    }


}


/**
 * The information we hold about Theorycrafter windows.
 */
@Stable
open class TheorycrafterWindowInfo {


    /**
     * The actual displayed window.
     *
     * Note that this is `null` between the request to show the window and it actually being displayed.
     */
    var window: ComposeWindow? by mutableStateOf(null)


    /**
     * Brings the window to the front.
     */
    fun bringWindowToFront() {
        // if there's no window yet, it means it will open soon, so we don't need to do anything
        window?.deiconifyAndBringToFront()
    }


}


/**
 * The information we hold about Theorycrafter windows.
 */
open class TheorycrafterWindowWithSettingsInfo<S: TheorycrafterSettings.WindowSettings>(


    /**
     * The window settings.
     */
    val windowSettings: S


): TheorycrafterWindowInfo()


/**
 * The information we keep about secondary fit windows.
 */
private class SecondaryFitWindowInfo(


    /**
     * The job that listens to the fit being deleted.
     */
    val deletionListener: Job,


    /**
     * The window settings.
     */
    windowSettings: TheorycrafterSettings.FitWindowSettings


): TheorycrafterWindowWithSettingsInfo<TheorycrafterSettings.FitWindowSettings>(windowSettings)


/**
 * A window state type for windows whose state is saved into settings.
 */
sealed interface SimpleSavedWindowState {


    /**
     * The window is closed.
     */
    @Stable
    data object Closed: SimpleSavedWindowState


    /**
     * The window is open.
     */
    @Stable
    class Open(

        /**
         * The window settings.
         */
        val windowSettings: TheorycrafterSettings.WindowSettings


    ): TheorycrafterWindowInfo(), SimpleSavedWindowState


}


/**
 * The composition local for the [TheorycrafterWindowManager].
 */
val LocalTheorycrafterWindowManager =
    staticCompositionLocalOf<TheorycrafterWindowManager> { error("No WindowManager provided") }


/**
 * The default minimum window size.
 */
private val DefaultMinWindowSize = DpSize(200.dp, 160.dp)


/**
 * Returns a remembered [WindowState] from the given [TheorycrafterSettings.WindowSettings], and with given default values.
 * The function will also update the settings whenever the [WindowState] changes.
 */
@Composable
fun rememberWindowStateAndUpdateSettings(
    windowSettings: TheorycrafterSettings.WindowSettings,
    defaultPosition: () -> WindowPosition,
    defaultSize: () -> DpSize,
    minimumSize: DpSize = DefaultMinWindowSize,
): WindowState {
    return remember(windowSettings) {
        WindowState(
            position = windowSettings.position ?: defaultPosition(),
            size = windowSettings.size ?: defaultSize()
        ).also { it.enforceGoodInitialState(minimumSize) }
    }.also { windowState ->
        LaunchedEffect(windowSettings, windowState) {
            snapshotFlow {
                Pair(windowState.position, windowState.size)
            }.collect {
                windowSettings.setBounds(windowState)
            }
        }
    }
}


/**
 * Coerces the given [WindowState] to have a minimum size.
 */
fun WindowState.enforceGoodInitialState(minimumSize: DpSize) {
    if (size.isUnspecified)
        return
    if ((size.width < minimumSize.width) || (size.height < minimumSize.height)) {
        size = DpSize(
            width = size.width.coerceAtLeast(minimumSize.width * ApplicationUiScale),
            height = size.height.coerceAtLeast(minimumSize.height * ApplicationUiScale)
        )
    }
}


/**
 * Returns the default icon that should be used for Theorycrafter windows.
 */
@Composable
private fun defaultWindowIcon(): Painter? {
    if (hostOs == OS.MacOS)
        return null

    return painterResource(Res.drawable.window_icon)
}


/**
 * The UI is scaled by this factor in order to fit better on small screens.
 */
val DefaultApplicationUiScale = run {
    val gfxConfig =  GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration
    val screenSize = gfxConfig.bounds
    val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gfxConfig)
    val usableScreenSize = Dimension(
        screenSize.width - screenInsets.left - screenInsets.right,
        screenSize.height - screenInsets.top - screenInsets.bottom
    )
    with(usableScreenSize) {
        if ((DefaultMainWindowWidth > width) || (DefaultMainWindowHeight > height))
            0.75f
        else
            1f
    }
}


/**
 * Provides the composition locals at the window level.
 */
@Composable
fun ProvideWindowCompositionLocals(content: @Composable () -> Unit) {
    val standardDensity = LocalDensity.current.density
    CompositionLocalProvider(
        LocalDensity provides Density(standardDensity * ApplicationUiScale),
        LocalViewConfiguration provides ViewConfigurationForDragToReorder(),
        content = content
    )
}


/**
 * The window that should normally be used in Theorycrafter.
 *
 * It uses the Theorycrafter icon, and provides the common composition locals.
 */
@Composable
fun TheorycrafterWindow(
    onCloseRequest: () -> Unit,
    state: WindowState = rememberWindowState(),
    visible: Boolean = true,
    title: String = "Untitled",
    icon: Painter? = defaultWindowIcon(),
    undecorated: Boolean = false,
    transparent: Boolean = false,
    resizable: Boolean = true,
    enabled: Boolean = true,
    focusable: Boolean = true,
    alwaysOnTop: Boolean = false,
    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
    onKeyEvent: (KeyEvent) -> Boolean = { false },
    minimumSize: DpSize? = null,
    content: @Composable FrameWindowScope.() -> Unit
) {
    val shortcutsManager = remember { KeyShortcutsManager() }

    val windowManager = LocalTheorycrafterWindowManager.current
    val fitOpener = remember(windowManager) {
        FitOpenerInNewWindowOnly(windowManager)
    }

    Window(
        onCloseRequest = onCloseRequest,
        state = state,
        visible = visible,
        title = title,
        icon = icon,
        undecorated = undecorated,
        transparent = transparent,
        resizable = resizable,
        enabled = enabled,
        focusable = focusable,
        alwaysOnTop = alwaysOnTop,
        onPreviewKeyEvent = {
            if (onPreviewKeyEvent(it))
                return@Window true

            if (shortcutsManager.onKeyEvent(it))
                return@Window true

            if (KeyShortcut.CloseUi.matches(it)) {
                if (it.type == KeyEventType.KeyDown)
                    onCloseRequest()
                return@Window true
            }

            if (KeyShortcut.CloseAllUi.matches(it)) {
                if (it.type == KeyEventType.KeyDown)
                    windowManager.closeAllSecondaryWindows()
                return@Window true
            }
            return@Window false
        },
        onKeyEvent = onKeyEvent,
    ) {
        ProvideWindowCompositionLocals {
            CompositionLocalProvider(
                LocalWindowState provides state,
                LocalWindow provides window,
                LocalKeyShortcutsManager provides shortcutsManager,
                LocalFitOpener provides fitOpener,
            ) {
                TooltipHost(TheorycrafterTheme.TooltipHostProperties) {
                    ProvideStandardDialogs {
                        content()
                    }
                }
            }
        }

        windowBackgroundFlashingOnCloseWorkaround()

        LaunchedEffect(window, minimumSize, ApplicationUiScale) {
            if (minimumSize != null) {
                window.minimumSize = Dimension(
                    (minimumSize.width.value * ApplicationUiScale).roundToInt() ,
                    (minimumSize.height.value * ApplicationUiScale).roundToInt()
                )
            }
        }
    }
}


/**
 * The dialog window that should normally be used in Theorycrafter.
 *
 * It uses the Theorycrafter icon, and provides the common composition locals.
 */
@Composable
fun TheorycrafterDialogWindow(
    onCloseRequest: () -> Unit,
    state: DialogState = rememberDialogState(),
    visible: Boolean = true,
    title: String = "Untitled",
    icon: Painter? = defaultWindowIcon(),
    undecorated: Boolean = false,
    transparent: Boolean = false,
    resizable: Boolean = true,
    enabled: Boolean = true,
    focusable: Boolean = true,
    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
    content: @Composable DialogWindowScope.() -> Unit
) {
    DialogWindow(
        onCloseRequest = onCloseRequest,
        state = state,
        visible = visible,
        title = title,
        icon = icon,
        undecorated = undecorated,
        transparent = transparent,
        resizable = resizable,
        enabled = enabled,
        focusable = focusable,
        onPreviewKeyEvent = {
            if (onPreviewKeyEvent(it))
                return@DialogWindow true

            if (KeyShortcut.CloseUi.matches(it) || KeyShortcut.Esc.matches(it)) {
                if (it.type == KeyEventType.KeyDown)
                    onCloseRequest()
                return@DialogWindow true
            }

            return@DialogWindow false
        },
        onKeyEvent = onKeyEvent,
    ) {
        ProvideWindowCompositionLocals {
            TooltipHost(TheorycrafterTheme.TooltipHostProperties) {
                ProvideStandardDialogs {
                    content()
                }
            }
        }
        windowBackgroundFlashingOnCloseWorkaround()
    }
}
