// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.settings

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.util.Comparing
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.SwingHelper
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance.BadExecutable
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance.ExecutableWithPath
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.ExecutableType
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.resources.message
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.CompletionException
import javax.swing.JComponent
import javax.swing.JPanel

class AwsSettingsConfigurable : SearchableConfigurable {
    private lateinit var panel: JPanel
    private lateinit var samHelp: JComponent
    private lateinit var serverlessSettings: JPanel
    private lateinit var applicationLevelSettings: JPanel
    private lateinit var defaultRegionHandling: ComboBox<UseAwsCredentialRegion>
    private lateinit var profilesNotification: ComboBox<ProfilesNotification>
    lateinit var samExecutablePath: TextFieldWithBrowseButton
        private set
    lateinit var enableTelemetry: JBCheckBox
        private set
    private val samExecutableInstance: SamExecutable
        get() = ExecutableType.getExecutable(SamExecutable::class.java)
    private val samTextboxInput: String?
        get() = StringUtil.nullize(samExecutablePath.text.trim { it <= ' ' })

    override fun createComponent(): JComponent = panel

    private fun createUIComponents() {
        samHelp = createHelpLink(HelpIds.SAM_CLI_INSTALL)
        samExecutablePath = createCliConfigurationElement(samExecutableInstance, SAM)
        defaultRegionHandling = ComboBox(UseAwsCredentialRegion.values())
        profilesNotification = ComboBox(ProfilesNotification.values())
    }

    init {
        applicationLevelSettings.border = IdeBorderFactory.createTitledBorder(message("aws.settings.global_label"))
        serverlessSettings.border = IdeBorderFactory.createTitledBorder(message("aws.settings.serverless_label"))
        SwingHelper.setPreferredWidth(samExecutablePath, panel.width)
    }

    override fun getId(): String = "aws"
    override fun getDisplayName(): String = message("aws.settings.title")

    override fun isModified(): Boolean {
        val awsSettings = AwsSettings.getInstance()
        return samTextboxInput != getSavedExecutablePath(samExecutableInstance, false) ||
            isModified(enableTelemetry, awsSettings.isTelemetryEnabled) ||
            // isModified for ComboBoxes is removed from 2021.3
            !Comparing.equal(defaultRegionHandling.selectedItem, awsSettings.useDefaultCredentialRegion) ||
            !Comparing.equal(profilesNotification.selectedItem, awsSettings.profilesNotification)
    }

    override fun apply() {
        validateAndSaveCliSettings(
            samExecutablePath.textField as JBTextField,
            "sam",
            samExecutableInstance,
            getSavedExecutablePath(samExecutableInstance, false),
            samTextboxInput
        )
        saveAwsSettings()
    }

    override fun reset() {
        val awsSettings = AwsSettings.getInstance()
        samExecutablePath.setText(getSavedExecutablePath(samExecutableInstance, false))
        enableTelemetry.isSelected = awsSettings.isTelemetryEnabled
        defaultRegionHandling.selectedItem = awsSettings.useDefaultCredentialRegion
        profilesNotification.selectedItem = awsSettings.profilesNotification
    }

    private fun createHelpLink(helpId: HelpIds): JComponent = ActionLink(message("aws.settings.learn_more")) { BrowserUtil.browse(helpId.url) }

    private fun createCliConfigurationElement(executableType: ExecutableType<*>, cliName: String): TextFieldWithBrowseButton {
        val autoDetectPath = getSavedExecutablePath(executableType, true)
        val executablePathField = JBTextField()
        val field = TextFieldWithBrowseButton(executablePathField)
        if (autoDetectPath != null) {
            executablePathField.emptyText.setText(autoDetectPath)
        }
        field.addBrowseFolderListener(
            message("aws.settings.find.title", cliName),
            message("aws.settings.find.description", cliName),
            null,
            FileChooserDescriptorFactory.createSingleLocalFileDescriptor()
        )
        return field
    }

    // modifyMessageBasedOnDetectionStatus will append "Auto-detected: ...." to the
    // message if the executable is found this is used for setting the empty box text
    private fun getSavedExecutablePath(executableType: ExecutableType<*>, modifyMessageBasedOnDetectionStatus: Boolean): String? = try {
        ExecutableManager.getInstance().getExecutable(executableType).thenApply {
            if (it is ExecutableWithPath) {
                if (it !is ExecutableInstance.Executable) {
                    return@thenApply (it as ExecutableWithPath).executablePath.toString()
                } else {
                    val path = it.executablePath.toString()
                    val autoResolved = it.autoResolved
                    if (autoResolved && modifyMessageBasedOnDetectionStatus) {
                        return@thenApply message("aws.settings.auto_detect", path)
                    } else if (autoResolved) {
                        // If it is auto detected, we do not want to return text as the
                        // box will be filled by empty text with the auto-resolve message
                        return@thenApply null
                    } else {
                        return@thenApply path
                    }
                }
            }
            null
        }.toCompletableFuture().join()
    } catch (ignored: CompletionException) {
        null
    }

    private fun validateAndSaveCliSettings(
        textField: JBTextField,
        executableName: String,
        executableType: ExecutableType<*>,
        saved: String?,
        currentInput: String?
    ) {
        // If input is null, wipe out input and try to autodiscover
        if (currentInput == null) {
            ExecutableManager.getInstance().removeExecutable(executableType)
            ExecutableManager.getInstance()
                .getExecutable(executableType)
                .thenRun {
                    val autoDetectPath = getSavedExecutablePath(executableType, true)
                    runInEdt(ModalityState.any()) {
                        if (autoDetectPath != null) {
                            textField.emptyText.setText(autoDetectPath)
                        } else {
                            textField.emptyText.setText("")
                        }
                    }
                }
            return
        }
        if (currentInput == saved) {
            return
        }
        val path: Path
        try {
            path = Paths.get(currentInput)
            if (!Files.isExecutable(path) || !path.toFile().exists() || !path.toFile().isFile) {
                throw IllegalArgumentException("Set file is not an executable")
            }
        } catch (e: Exception) {
            throw ConfigurationException(message("aws.settings.executables.executable_invalid", executableName, currentInput))
        }
        val instance = ExecutableManager.getInstance().validateExecutablePath(executableType, path)
        if (instance is BadExecutable) {
            throw ConfigurationException(instance.validationError)
        }

        // We have validated so now we can set
        ExecutableManager.getInstance().setExecutablePath(executableType, path)
    }

    private fun saveAwsSettings() {
        val awsSettings = AwsSettings.getInstance()
        awsSettings.isTelemetryEnabled = enableTelemetry.isSelected
        awsSettings.useDefaultCredentialRegion = defaultRegionHandling.selectedItem as? UseAwsCredentialRegion ?: UseAwsCredentialRegion.Never
        awsSettings.profilesNotification = profilesNotification.selectedItem as? ProfilesNotification ?: ProfilesNotification.Always
    }

    companion object {
        private const val SAM = "sam"
    }
}


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service

@State(name = "cawsSpace", storages = [Storage("aws.xml")])
class CawsSpaceTracker : PersistentStateComponent<CawsSpaceState> {
    private val state = CawsSpaceState()

    override fun getState() = state

    override fun loadState(state: CawsSpaceState) {
        this.state.lastSpaceName = state.lastSpaceName
    }

    fun lastSpaceName() = state.lastSpaceName

    fun changeSpaceName(newName: String?) {
        state.lastSpaceName = newName
    }

    companion object {
        fun getInstance() = service<CawsSpaceTracker>()
    }
}

data class CawsSpaceState(
    var lastSpaceName: String? = null
)

interface CawsSpaceSelectionChange {
    fun newSpace(spaceName: String)
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleServiceManager
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.vfs.VirtualFile
import software.aws.toolkits.jetbrains.services.lambda.deploy.CreateCapabilities
import java.nio.file.Paths

@State(name = "deploySettings")
class DeploySettings : PersistentStateComponent<DeployConfigs> {
    private var state = DeployConfigs()

    override fun getState(): DeployConfigs = state

    override fun loadState(state: DeployConfigs) {
        this.state = state
    }

    fun samStackName(samPath: String): String? = state.samConfigs[samPath]?.stackName
    fun setSamStackName(samPath: String, value: String) {
        state.samConfigs.computeIfAbsent(samPath) { DeploySamConfig() }.stackName = value
    }

    fun samBucketName(samPath: String): String? = state.samConfigs[samPath]?.bucketName
    fun setSamBucketName(samPath: String, value: String) {
        state.samConfigs.computeIfAbsent(samPath) { DeploySamConfig() }.bucketName = value
    }

    fun samEcrRepoUri(samPath: String): String? = state.samConfigs[samPath]?.repoUri
    fun setSamEcrRepoUri(samPath: String, value: String?) {
        state.samConfigs.computeIfAbsent(samPath) { DeploySamConfig() }.repoUri = value
    }

    fun samAutoExecute(samPath: String): Boolean? = state.samConfigs[samPath]?.autoExecute
    fun setSamAutoExecute(samPath: String, value: Boolean) {
        state.samConfigs.computeIfAbsent(samPath) { DeploySamConfig() }.autoExecute = value
    }

    fun samUseContainer(samPath: String): Boolean? = state.samConfigs[samPath]?.useContainer
    fun setSamUseContainer(samPath: String, value: Boolean) {
        state.samConfigs.computeIfAbsent(samPath) { DeploySamConfig() }.useContainer = value
    }

    fun enabledCapabilities(samPath: String): List<CreateCapabilities>? = state.samConfigs[samPath]?.enabledCapabilities
    fun setEnabledCapabilities(samPath: String, value: List<CreateCapabilities>) {
        state.samConfigs.computeIfAbsent(samPath) { DeploySamConfig() }.enabledCapabilities = value
    }

    companion object {
        @JvmStatic
        fun getInstance(module: Module): DeploySettings? = ModuleServiceManager.getService(module, DeploySettings::class.java)
    }
}

data class DeployConfigs(
    var samConfigs: MutableMap<String, DeploySamConfig> = mutableMapOf()
)

data class DeploySamConfig(
    var stackName: String? = null,
    var bucketName: String? = null,
    var repoUri: String? = null,
    var autoExecute: Boolean = false,
    var useContainer: Boolean = false,
    var enabledCapabilities: List<CreateCapabilities>? = null
)

/**
 * @return The relative SAM template file path against the module root. This is used as the key of SAM configs
 * @see DeployConfigs.samConfigs
 */
fun relativeSamPath(module: Module, templateFile: VirtualFile): String? = module.rootManager.contentRoots
    .find { Paths.get(templateFile.path).startsWith(it.path) }
    ?.let { Paths.get(it.path).relativize(Paths.get(templateFile.path)) }
    ?.toString()


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.InputValidator
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.messages.MessagesService
import com.intellij.ui.CheckBoxList
import com.intellij.ui.FilterComponent
import com.intellij.ui.ListSpeedSearch
import com.intellij.ui.layout.panel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
import software.aws.toolkits.core.utils.replace
import software.aws.toolkits.jetbrains.core.coroutines.applicationCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.explorer.ExplorerToolWindow
import software.aws.toolkits.jetbrains.services.dynamic.DynamicResourceSupportedTypes
import software.aws.toolkits.jetbrains.services.dynamic.explorer.OtherResourcesNode
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
import software.aws.toolkits.jetbrains.ui.feedback.FEEDBACK_SOURCE
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.FeedbackTelemetry
import javax.swing.ListSelectionModel

class DynamicResourcesConfigurable : BoundConfigurable(message("aws.settings.dynamic_resources_configurable.title")) {

    private val coroutineScope = applicationCoroutineScope()
    private val checklist = CheckBoxList<String>()
    private val allResources = mutableSetOf<String>()
    private val selected = mutableSetOf<String>()
    private val filter = object : FilterComponent("filter", 5) {
        override fun filter() {
            updateCheckboxList()
        }
    }

    init {
        checklist.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
        checklist.setCheckBoxListListener(::checkboxStateHandler)

        ListSpeedSearch(checklist) {
            it.text.substringAfter("::")
        }
    }

    override fun getPreferredFocusedComponent() = checklist

    override fun createPanel() = panel {
        selected.replace(DynamicResourcesSettings.getInstance().selected)
        coroutineScope.launch(getCoroutineBgContext()) {
            allResources.addAll(DynamicResourceSupportedTypes.getInstance().getSupportedTypes())
            withContext(getCoroutineUiContext()) {
                updateCheckboxList()
            }
        }
        row {
            cell(isFullWidth = true) {
                filter(growX, pushX)
                link(message("aws.settings.dynamic_resources_configurable.suggest_types.prompt")) {
                    showTypeSuggestionBox()?.let { suggestion ->
                        submitSuggestion(suggestion)
                    }
                }
            }
        }
        row {
            scrollPane(checklist)
                .constraints(growX, pushX)
                .onIsModified { selected != DynamicResourcesSettings.getInstance().selected }
                .onApply {
                    DynamicResourcesSettings.getInstance().selected = selected
                    refreshAwsExplorer()
                }
                .onReset {
                    selected.replace(DynamicResourcesSettings.getInstance().selected)
                    updateCheckboxList()
                }

            cell(isVerticalFlow = true) {
                val sizeGroup = "buttons"
                button(message("aws.settings.dynamic_resources_configurable.select_all")) {
                    checklist.toggleAll(true)
                }.sizeGroup(sizeGroup)

                button(message("aws.settings.dynamic_resources_configurable.clear_all")) {
                    checklist.toggleAll(false)
                }.sizeGroup(sizeGroup)
            }
        }
    }

    private fun submitSuggestion(suggestion: String) {
        coroutineScope.launch(getCoroutineBgContext()) {
            try {
                TelemetryService.getInstance().sendFeedback(Sentiment.NEGATIVE, suggestion, mapOf(FEEDBACK_SOURCE to "Resource Type Suggestions")).also {
                    FeedbackTelemetry.result(project = null, success = true)
                }
            } catch (e: Exception) {
                e.notifyError(message("feedback.submit_failed", e))
                FeedbackTelemetry.result(project = null, success = false)
            }
        }
    }

    private fun CheckBoxList<*>.toggleAll(state: Boolean) {
        (0 until model.size).forEach { idx ->
            checkboxStateHandler(idx, state)
        }
        updateCheckboxList()
    }

    private fun updateCheckboxList() {
        checklist.clear()
        allResources.filter { it.contains(filter.filter, ignoreCase = true) }.sorted().forEach { checklist.addItem(it, it, it in selected) }
    }

    private fun checkboxStateHandler(idx: Int, state: Boolean) {
        checklist.getItemAt(idx)?.let { value ->
            if (state) {
                selected.add(value)
            } else {
                selected.remove(value)
            }
        }
    }

    private fun refreshAwsExplorer() {
        ProjectManager.getInstance().openProjects.forEach { project ->
            if (!project.isDisposed) {
                val toolWindow = ExplorerToolWindow.getInstance(project)
                toolWindow.findNode(OtherResourcesNode::class).then { node ->
                    node.let {
                        toolWindow.invalidateTree(it)
                    }
                }
            }
        }
    }

    companion object {
        private const val INITIAL_INPUT = "AWS::"
        private const val MAX_LENGTH = 2000

        private fun showTypeSuggestionBox(): String? = MessagesService.getInstance().showMultilineInputDialog(
            project = null,
            message = message("aws.settings.dynamic_resources_configurable.suggest_types.dialog.message"),
            title = message("aws.settings.dynamic_resources_configurable.suggest_types.dialog.title"),
            initialValue = INITIAL_INPUT,
            icon = Messages.getQuestionIcon(),
            object : InputValidator {
                override fun checkInput(inputString: String?) = validateSuggestion(inputString)
                override fun canClose(inputString: String?) = validateSuggestion(inputString)
            }
        )?.takeIf { it.isNotBlank() }

        private fun validateSuggestion(inputString: String?) =
            inputString != null && inputString.isNotBlank() && inputString != INITIAL_INPUT && inputString.length <= MAX_LENGTH
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.SimplePersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.util.xmlb.annotations.Property
import software.aws.toolkits.core.utils.replace

interface DynamicResourcesSettings {
    var selected: Set<String>

    companion object {
        fun getInstance(): DynamicResourcesSettings = service()
    }
}

@State(name = "resources", storages = [Storage("aws.xml")])
internal class DefaultDynamicResourcesSettings :
    DynamicResourcesSettings,
    SimplePersistentStateComponent<DynamicResourcesConfiguration>(DynamicResourcesConfiguration()) {
    override var selected: Set<String>
        get() = state.selected.toSet()
        set(value) {
            state.selected.replace(value)
        }
}

internal class DynamicResourcesConfiguration : BaseState() {
    // using a list because `stringSet` doesn't automatically increment the modification counter and ends up not getting persisted
    @get:Property
    val selected by list<String>()
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage

@State(name = "ecsExec", storages = [Storage("aws.xml")])
class EcsExecCommandSettings : PersistentStateComponent<AwsEcsExecConfiguration> {
    private var state = AwsEcsExecConfiguration()

    override fun getState(): AwsEcsExecConfiguration? = state

    override fun loadState(state: AwsEcsExecConfiguration) {
        this.state = state
    }

    var showExecuteCommandWarning: Boolean
        get() = state.showExecuteCommandWarning
        set(value) {
            state.showExecuteCommandWarning = value
        }

    companion object {
        fun getInstance(): EcsExecCommandSettings = ApplicationManager.getApplication().getService(EcsExecCommandSettings::class.java)
    }
}

data class AwsEcsExecConfiguration(
    var showExecuteCommandWarning: Boolean = true
)


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.util.text.StringUtil
import java.io.File

class ExecutableDetector {

    fun find(paths: Array<String>, executables: Array<String>): String? {
        paths.forEach { path ->
            executables.forEach { executable ->
                val file = File(path, executable)
                if (file.exists()) {
                    return file.path
                }
            }
        }

        executables.forEach { executable ->
            val result = checkInPath(executable)
            if (result != null) {
                return result
            }
        }

        return null
    }

    private fun checkInPath(executableName: String): String? {
        val pathEnvVar = System.getenv(PATH_ENV) ?: return null
        val pathEntries = StringUtil.split(pathEnvVar, File.pathSeparator)
        pathEntries.forEach { pathEntry ->
            val f = File(pathEntry, executableName)
            if (f.exists()) {
                return f.path
            }
        }
        return null
    }

    private companion object {
        const val PATH_ENV = "PATH"
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project

@State(name = "lambda", storages = [Storage("aws.xml")])
class LambdaSettings(private val project: Project) : PersistentStateComponent<LambdaConfiguration> {
    private var state = LambdaConfiguration()

    override fun getState(): LambdaConfiguration = state

    override fun loadState(state: LambdaConfiguration) {
        this.state = state
    }

    var showAllHandlerGutterIcons: Boolean
        get() = state.showAllHandlerGutterIcons
        set(value) {
            state.showAllHandlerGutterIcons = value
            project.messageBus.syncPublisher(LambdaSettingsChangeListener.TOPIC).samShowAllHandlerGutterIconsSettingsChange(value)
        }

    companion object {
        @JvmStatic
        fun getInstance(project: Project): LambdaSettings = project.service()
    }
}

data class LambdaConfiguration(
    var showAllHandlerGutterIcons: Boolean = false
)


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.util.messages.Topic

interface LambdaSettingsChangeListener {
    companion object {
        @JvmStatic val TOPIC = Topic("SAM Setting has been changed", LambdaSettingsChangeListener::class.java)
    }

    /**
     * SAM CLI version evaluation finished for the chosen profile, and it may be validated synchronously now.
     */
    fun samShowAllHandlerGutterIconsSettingsChange(isShow: Boolean)
}


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service

@State(name = "samAccDevMode", storages = [Storage("aws.xml")])
class SamDisplayDevModeWarningSettings : PersistentStateComponent<SamDevModeWarningConfiguration> {
    private var state = SamDevModeWarningConfiguration()

    override fun getState(): SamDevModeWarningConfiguration? = state

    override fun loadState(state: SamDevModeWarningConfiguration) {
        this.state = state
    }

    var showDevModeWarning: Boolean
        get() = state.showDevModeWarning
        set(value) {
            state.showDevModeWarning = value
        }

    companion object {
        fun getInstance(): SamDisplayDevModeWarningSettings = service()
    }
}

data class SamDevModeWarningConfiguration(
    var showDevModeWarning: Boolean = true
)


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleServiceManager
import software.aws.toolkits.jetbrains.services.lambda.deploy.CreateCapabilities

@State(name = "syncSettings")
class SyncSettings : PersistentStateComponent<SyncConfigs> {
    private var state = SyncConfigs()

    override fun getState(): SyncConfigs = state

    override fun loadState(state: SyncConfigs) {
        this.state = state
    }

    fun samStackName(samPath: String): String? = state.samConfigs[samPath]?.stackName
    fun setSamStackName(samPath: String, value: String) {
        state.samConfigs.computeIfAbsent(samPath) { SyncSamConfig() }.stackName = value
    }

    fun samBucketName(samPath: String): String? = state.samConfigs[samPath]?.bucketName
    fun setSamBucketName(samPath: String, value: String) {
        state.samConfigs.computeIfAbsent(samPath) { SyncSamConfig() }.bucketName = value
    }

    fun samEcrRepoUri(samPath: String): String? = state.samConfigs[samPath]?.repoUri
    fun setSamEcrRepoUri(samPath: String, value: String?) {
        state.samConfigs.computeIfAbsent(samPath) { SyncSamConfig() }.repoUri = value
    }

    fun samUseContainer(samPath: String): Boolean? = state.samConfigs[samPath]?.useContainer
    fun setSamUseContainer(samPath: String, value: Boolean) {
        state.samConfigs.computeIfAbsent(samPath) { SyncSamConfig() }.useContainer = value
    }

    fun enabledCapabilities(samPath: String): List<CreateCapabilities>? = state.samConfigs[samPath]?.enabledCapabilities
    fun setEnabledCapabilities(samPath: String, value: List<CreateCapabilities>) {
        state.samConfigs.computeIfAbsent(samPath) { SyncSamConfig() }.enabledCapabilities = value
    }

    fun samTags(samPath: String): Map<String, String>? = state.samConfigs[samPath]?.tags
    fun setSamTags(samPath: String, value: Map<String, String>) {
        state.samConfigs.computeIfAbsent(samPath) { SyncSamConfig() }.tags = value
    }

    fun samTempParameterOverrides(samPath: String): Map<String, String>? = state.samConfigs[samPath]?.tempParameterOverrides
    fun setSamTempParameterOverrides(samPath: String, value: Map<String, String>) {
        state.samConfigs.computeIfAbsent(samPath) { SyncSamConfig() }.tempParameterOverrides = value
    }

    companion object {
        fun getInstance(module: Module): SyncSettings? = ModuleServiceManager.getService(module, SyncSettings::class.java)
    }
}

data class SyncConfigs(
    var samConfigs: MutableMap<String, SyncSamConfig> = mutableMapOf()
)

data class SyncSamConfig(
    var stackName: String? = null,
    var bucketName: String? = null,
    var repoUri: String? = null,
    var useContainer: Boolean = false,
    var enabledCapabilities: List<CreateCapabilities>? = null,
    var tags: Map<String, String>? = null,
    var tempParameterOverrides: Map<String, String>? = null
)


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service

@State(name = "updateLambdaState", storages = [Storage("aws.xml")])
private class UpdateLambdaState : PersistentStateComponent<UpdateLambda> {
    private var settings = UpdateLambda()

    override fun getState(): UpdateLambda = settings
    override fun loadState(state: UpdateLambda) {
        this.settings = state
    }

    companion object {
        @JvmStatic
        internal fun getInstance(): UpdateLambdaState = service()
    }
}

class UpdateLambdaSettings private constructor(private val arn: String) {
    private val stateService = UpdateLambdaState.getInstance()

    var useContainer: Boolean?
        get() = stateService.state.configs[arn]?.useContainer
        set(value) {
            stateService.state.configs.computeIfAbsent(arn) { UpdateConfig() }.useContainer = value ?: false
        }

    var bucketName: String?
        get() = stateService.state.configs[arn]?.bucketName
        set(value) {
            stateService.state.configs.computeIfAbsent(arn) { UpdateConfig() }.bucketName = value
        }

    var ecrRepo: String?
        get() = stateService.state.configs[arn]?.ecrRepo
        set(value) {
            stateService.state.configs.computeIfAbsent(arn) { UpdateConfig() }.ecrRepo = value
        }

    var dockerfile: String?
        get() = stateService.state.configs[arn]?.dockerfile
        set(value) {
            stateService.state.configs.computeIfAbsent(arn) { UpdateConfig() }.dockerfile = value
        }

    companion object {
        fun getInstance(arn: String) = UpdateLambdaSettings(arn)
    }
}

data class UpdateLambda(
    var configs: MutableMap<String, UpdateConfig> = mutableMapOf()
)

data class UpdateConfig(
    var bucketName: String? = null,
    var ecrRepo: String? = null,
    var dockerfile: String? = null,
    var useContainer: Boolean = false
)


// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.icons.AllIcons
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComponent
import javax.swing.event.ChangeListener

open class ActionPopupComboLabel(private val logic: ActionPopupComboLogic) : BorderLayoutPanel() {
    private val label = JBLabel()

    init {
        isOpaque = false

        val arrowLabel = JBLabel(AllIcons.General.ArrowDown)
        addToCenter(label)
        addToRight(arrowLabel)

        val clickAdapter = object : MouseAdapter() {
            override fun mouseClicked(e: MouseEvent) {
                logic.showPopup(this@ActionPopupComboLabel)
            }
        }
        label.addMouseListener(clickAdapter)
        arrowLabel.addMouseListener(clickAdapter)
        logic.addChangeListener {
            updateText()
        }

        updateText()
    }

    fun updateText() {
        label.text = logic.displayValue()
        label.toolTipText = logic.tooltip()
    }
}

interface ActionPopupComboLogic {
    fun showPopup(sourceComponent: JComponent)
    fun displayValue(): String
    fun addChangeListener(changeListener: ChangeListener)
    fun tooltip(): String? = null
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBLabel
import com.intellij.util.Alarm
import com.intellij.util.AlarmFactory
import kotlinx.coroutines.launch
import org.jetbrains.annotations.TestOnly
import org.jetbrains.concurrency.AsyncPromise
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.utils.ui.selected
import software.aws.toolkits.resources.message
import java.awt.Component
import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.DefaultComboBoxModel
import javax.swing.JList
import javax.swing.ListCellRenderer
import javax.swing.MutableComboBoxModel
import javax.swing.event.ListDataListener

class AsyncComboBox<T> private constructor(
    private val comboBoxModel: MutableComboBoxModel<T>
) : ComboBox<T>(comboBoxModel), Disposable {
    private val loading = AtomicBoolean(false)
    private val scope = disposableCoroutineScope(this)

    constructor(
        comboBoxModel: MutableComboBoxModel<T> = DefaultComboBoxModel(),
        customizer: SimpleListCellRenderer.Customizer<in T>? = null
    ) : this(comboBoxModel) {
        renderer = object : SimpleListCellRenderer<T>() {
            override fun getListCellRendererComponent(
                list: JList<out T>?,
                value: T?,
                index: Int,
                selected: Boolean,
                hasFocus: Boolean
            ): Component {
                val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleListCellRenderer<*>

                if (loading.get() && index == -1) {
                    component.icon = AnimatedIcon.Default.INSTANCE
                    component.text = message("loading_resource.loading")
                }

                return component
            }

            override fun customize(list: JList<out T>, value: T, index: Int, selected: Boolean, hasFocus: Boolean) {
                customizer?.customize(this, value, index)
            }
        }
    }

    constructor(
        comboBoxModel: MutableComboBoxModel<T> = DefaultComboBoxModel(),
        customRenderer: ListCellRenderer<T>
    ) : this(comboBoxModel) {
        renderer = ListCellRenderer { list, value, index, selected, hasFocus ->
            if (loading.get() && index == -1) {
                val component = JBLabel(AnimatedIcon.Default.INSTANCE)
                component.text = message("loading_resource.loading")

                return@ListCellRenderer component
            }

            customRenderer.getListCellRendererComponent(list, value, index, selected, hasFocus)
        }
    }

    init {
        putClientProperty(AnimatedIcon.ANIMATION_IN_RENDERER_ALLOWED, true)
    }

    private val reloadAlarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.SWING_THREAD, this)
    private var currentIndicator: ProgressIndicator? = null

    @Synchronized
    fun proposeModelUpdate(newModel: suspend (MutableComboBoxModel<T>) -> Unit) {
        reloadAlarm.cancelAllRequests()
        currentIndicator?.cancel()
        loading.set(true)
        removeAllItems()
        repaint()
        val indicator = EmptyProgressIndicator(ModalityState.any()).also {
            currentIndicator = it
        }
        // delay with magic number to debounce
        reloadAlarm.addRequest(
            {
                ProgressManager.getInstance().runProcess(
                    {
                        scope.launch {
                            newModel.invoke(delegatedComboBoxModel(indicator))
                        }.invokeOnCompletion {
                            loading.set(false)
                            repaint()
                        }
                    },
                    indicator
                )
            },
            350,
            ModalityState.any()
        )
    }

    override fun dispose() {
    }

    override fun getSelectedItem(): Any? {
        if (loading.get()) {
            return null
        }
        return super.getSelectedItem()
    }

    @TestOnly
    @Synchronized
    internal fun waitForSelection(): Future<T?> {
        val future = AsyncPromise<T>()
        while (loading.get()) {
            Thread.onSpinWait()
        }
        future.setResult(selected())

        return future
    }

    override fun setSelectedItem(anObject: Any?) {
        if (loading.get()) {
            return
        }
        super.setSelectedItem(anObject)
    }

    private fun delegatedComboBoxModel(indicator: ProgressIndicator) =
        object : MutableComboBoxModel<T> {
            override fun getSize() = comboBoxModel.size
            override fun getElementAt(index: Int): T = comboBoxModel.getElementAt(index)

            override fun addListDataListener(l: ListDataListener?) {
                throw NotImplementedError()
            }

            override fun removeListDataListener(l: ListDataListener?) {
                throw NotImplementedError()
            }

            override fun setSelectedItem(anItem: Any?) {
                comboBoxModel.selectedItem = anItem
            }

            override fun getSelectedItem(): Any = comboBoxModel.selectedItem

            override fun addElement(item: T?) {
                indicator.checkCanceled()
                comboBoxModel.addElement(item)
            }

            override fun removeElement(obj: Any?) {
                indicator.checkCanceled()
                comboBoxModel.removeElement(item)
            }

            override fun insertElementAt(item: T?, index: Int) {
                indicator.checkCanceled()
                comboBoxModel.insertElementAt(item, index)
            }

            override fun removeElementAt(index: Int) {
                indicator.checkCanceled()
                comboBoxModel.removeElementAt(index)
            }
        }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.json.JsonLanguage
import com.intellij.openapi.project.Project
import com.intellij.ui.EditorTextField
import com.intellij.ui.EditorTextFieldProvider
import com.intellij.ui.components.JBLabel
import javax.swing.JPanel

class ConfirmPolicyPanel(
    private val project: Project,
    warning: String
) {
    lateinit var component: JPanel
        private set
    lateinit var policyDocument: EditorTextField
        private set
    lateinit var warningText: JBLabel
        private set

    init {
        warningText.text = warning
    }

    private fun createUIComponents() {
        policyDocument = EditorTextFieldProvider.getInstance().getEditorField(JsonLanguage.INSTANCE, project, emptyList())
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.observable.properties.GraphProperty
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ComboboxSpeedSearch
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.layout.CellBuilder
import com.intellij.ui.layout.PropertyBinding
import com.intellij.ui.layout.Row
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import kotlin.reflect.KMutableProperty0

sealed class CredentialChoice<T>(val value: T)
private class ValidCredentialIdentifier(value: CredentialIdentifier) : CredentialChoice<CredentialIdentifier>(value)
private class InvalidCredentialIdentifier(value: String) : CredentialChoice<String>(value)

/**
 * Combo box used to select a credential provider
 */
class CredentialProviderSelector2 : ComboBox<CredentialChoice<*>>() {
    private val comboBoxModel = Model(CredentialManager.getInstance().getCredentialIdentifiers())

    init {
        model = comboBoxModel
        setRenderer(
            SimpleListCellRenderer.create("") {
                when (it) {
                    is InvalidCredentialIdentifier -> "${it.value} (Not found)"
                    is ValidCredentialIdentifier -> it.value.displayName
                    else -> ""
                }
            }
        )
        ComboboxSpeedSearch(this)
    }

    fun setCredentialsProviders(providers: List<CredentialIdentifier>) {
        comboBoxModel.setCredentialsProviders(providers)
    }

    fun setSelectedCredentialsProvider(providerId: String) {
        val item = comboBoxModel.items.filterIsInstance<ValidCredentialIdentifier>().firstOrNull { it.value.id == providerId }
            ?: comboBoxModel.addInvalidItem(providerId)
        selectedItem = item
    }

    /**
     * Returns the ID of the selected provider, even if it was invalid
     */
    fun getSelectedCredentialsIdentifier(): String? {
        comboBoxModel.selected.let {
            return when (it) {
                is ValidCredentialIdentifier -> it.value.id
                is InvalidCredentialIdentifier -> it.value
                else -> null
            }
        }
    }

    fun isSelectionValid(): Boolean = comboBoxModel.selected is ValidCredentialIdentifier
    fun getSelectedCredentialIdentifier(): CredentialIdentifier {
        if (!isSelectionValid()) {
            throw IllegalStateException("Can't get credential identifier when the selection is an invalid one")
        }

        return (comboBoxModel.selected as ValidCredentialIdentifier).value
    }

    fun setSelectedCredentialsProvider(provider: CredentialIdentifier) {
        selectedItem = provider.id
    }

    override fun setSelectedItem(anObject: Any?) {
        comboBoxModel.selectedItem = anObject
    }

    private class Model(credentialIdentifiers: List<CredentialIdentifier>) : CollectionComboBoxModel<CredentialChoice<*>>() {
        init {
            setCredentialsProviders(credentialIdentifiers)
        }

        fun setCredentialsProviders(providers: List<CredentialIdentifier>) {
            replaceAll(providers.sortedBy { it.displayName }.map { ValidCredentialIdentifier(it) })
        }

        fun addInvalidItem(id: String): InvalidCredentialIdentifier {
            val invalidItem = InvalidCredentialIdentifier(id)
            add(0, invalidItem)
            return invalidItem
        }
    }

    companion object {
        fun Row.credentialSelector(prop: KMutableProperty0<CredentialIdentifier?>): CellBuilder<CredentialProviderSelector2> {
            val binding = PropertyBinding(
                get = { prop.get() },
                set = { prop.set(it) }
            )
            return credentialSelector(binding)
        }

        fun Row.credentialSelector(prop: GraphProperty<CredentialIdentifier?>): CellBuilder<CredentialProviderSelector2> {
            val binding = PropertyBinding(
                get = { runCatching { prop.get() }.getOrNull() },
                set = { prop.set(it) }
            )
            return credentialSelector(binding)
        }

        fun Row.credentialSelector(binding: PropertyBinding<CredentialIdentifier?>): CellBuilder<CredentialProviderSelector2> {
            val credentialProviderSelector = CredentialProviderSelector2()
            return component(credentialProviderSelector)
                .withValidationOnApply { if (!credentialProviderSelector.isSelectionValid()) error("FOO") else null }
                .withBinding(
                    { component -> component.getSelectedCredentialIdentifier() },
                    { component, value -> component.selectedItem = value },
                    binding
                )
        }
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.ComboboxSpeedSearch
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.layout.CellBuilder
import com.intellij.ui.layout.PropertyBinding
import com.intellij.ui.layout.Row
import com.intellij.ui.layout.ValidationInfoBuilder
import com.intellij.ui.layout.applyToComponent
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.credentials.CredentialType
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.resources.message
import javax.swing.JList
import kotlin.reflect.KMutableProperty0

/**
 * Combo box used to select a credential identifier
 */
class CredentialIdentifierSelector(identifiers: List<CredentialIdentifier> = CredentialManager.getInstance().getCredentialIdentifiers()) :
    ComboBox<CredentialIdentifier>() {
    private val comboBoxModel = CollectionComboBoxModel(identifiers.toMutableList())

    init {
        model = comboBoxModel
        setRenderer(object : ColoredListCellRenderer<CredentialIdentifier>() {
            override fun customizeCellRenderer(
                list: JList<out CredentialIdentifier>,
                value: CredentialIdentifier?,
                index: Int,
                selected: Boolean,
                hasFocus: Boolean
            ) {
                if (value is InvalidCredentialIdentifier) {
                    append(value.displayName, SimpleTextAttributes.ERROR_ATTRIBUTES)
                } else {
                    append(value?.displayName ?: "")
                }
            }
        })
        ComboboxSpeedSearch(this)
    }

    /**
     * Selects the specified credential identifier with the matching ID
     * If none is found, a invalid placeholder is created and selected
     */
    fun setSelectedCredentialIdentifierId(identifierId: String?) {
        val newSelection = identifierId?.let {
            comboBoxModel.items.firstOrNull { it.id == identifierId } ?: InvalidCredentialIdentifier(identifierId).also {
                comboBoxModel.add(it)
            }
        }
        selectedItem = newSelection
    }

    /**
     * Selects the specified credential identifier
     * If none is found, a invalid placeholder is created and selected
     */
    fun setSelectedCredentialIdentifier(identifier: CredentialIdentifier?) {
        setSelectedCredentialIdentifierId(identifier?.id)
    }

    fun isSelectionValid(): Boolean = comboBoxModel.selected != null && comboBoxModel.selected !is InvalidCredentialIdentifier

    /**
     * Returns the ID of the selected provider, even if it was invalid
     */
    fun getSelectedCredentialsIdentifierId(): String? = comboBoxModel.selected?.id

    /**
     * Returns the selected CredentialIdentifier if and only if it is valid
     */
    fun getSelectedValidCredentialIdentifier(): CredentialIdentifier? = comboBoxModel.selected?.takeIf { isSelectionValid() }

    private class InvalidCredentialIdentifier(override val id: String) : CredentialIdentifier {
        override val displayName: String = message("credentials.invalid.not_found", id)
        override val factoryId: String = "InvalidCredentialIdentifier"
        override val credentialType: CredentialType? = null
        override val defaultRegionId: String? = null
    }

    companion object {
        fun Row.credentialSelector(
            prop: KMutableProperty0<CredentialIdentifier?>,
            identifiers: List<CredentialIdentifier> = CredentialManager.getInstance().getCredentialIdentifiers()
        ): CellBuilder<CredentialIdentifierSelector> {
            val binding = PropertyBinding(
                get = { prop.get() },
                set = { prop.set(it) }
            )
            return createSelector(binding, identifiers)
        }

        /**
         * Credential selector that forces the selection to be valid
         *
         * Note: Initial property may be a lateinit property, in that case there will be no default selection
         */
        fun Row.validCredentialSelector(
            prop: KMutableProperty0<CredentialIdentifier>,
            identifiers: List<CredentialIdentifier> = CredentialManager.getInstance().getCredentialIdentifiers()
        ): CellBuilder<CredentialIdentifierSelector> {
            val binding = PropertyBinding(
                get = { runCatching { prop.get() }.getOrNull() },
                set = { prop.set(it!!) /* Guarded by apply check */ }
            )
            return createSelector(binding, identifiers)
                .withValidationOnApply { validateSelection(it) }
                .withValidationOnInput { validateSelection(it) }
        }

        private fun ValidationInfoBuilder.validateSelection(selector: CredentialIdentifierSelector): ValidationInfo? = if (!selector.isSelectionValid()) {
            error(message("credentials.invalid.invalid_selection"))
        } else {
            null
        }

        private fun Row.createSelector(
            binding: PropertyBinding<CredentialIdentifier?>,
            identifiers: List<CredentialIdentifier>
        ): CellBuilder<CredentialIdentifierSelector> {
            val credentialProviderSelector = CredentialIdentifierSelector(identifiers)
            return component(credentialProviderSelector)
                .applyToComponent { selectedItem = binding.get() }
                .withBinding(
                    { component -> component.getSelectedValidCredentialIdentifier() },
                    { component, value -> component.setSelectedCredentialIdentifierId(value?.id) },
                    binding
                )
        }
    }
}

fun tryResolveConnectionSettings(credentialsIdentifier: CredentialIdentifier?, region: AwsRegion?) =
    credentialsIdentifier?.let { credId ->
        region?.let { region ->
            val credentialProvider = CredentialManager.getInstance().getAwsCredentialProvider(credentialsIdentifier, region)
            ConnectionSettings(credentialProvider, region)
        }
    }


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ComboboxSpeedSearch
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.util.containers.OrderedSet
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.jetbrains.utils.ui.selected

/**
 * Combo box used to select a credential provider
 *
 * TODO: Find a way to make this more typesafe
 */
class CredentialProviderSelector : ComboBox<Any>() {
    private val comboBoxModel = Model()

    init {
        model = comboBoxModel
        setRenderer(RENDERER)
        ComboboxSpeedSearch(this)
    }

    fun setCredentialsProviders(providers: List<CredentialIdentifier>) {
        comboBoxModel.items = providers
    }

    /**
     * Returns the ID of the selected provider, even if it was invalid
     */
    fun getSelectedCredentialsProvider(): String? {
        selected().let {
            return when (it) {
                is CredentialIdentifier -> it.id
                is String -> it
                else -> null
            }
        }
    }

    fun setSelectedCredentialsProvider(provider: CredentialIdentifier) {
        selectedItem = provider
    }

    /**
     * Secondary method to set the selected credential provider using ID.
     * This should be used if the provider ID does not exist and we want to expose that
     * to the user. For example, run configurations that persist the ID used to invoke it
     * that had the ID deleted between uses should show the raw ID and that it is not found
     */
    fun setSelectedInvalidCredentialsProvider(providerId: String?) {
        comboBoxModel.add(providerId)
        selectedItem = providerId
    }

    private inner class Model : CollectionComboBoxModel<Any>(OrderedSet<Any>()) {
        fun setItems(newItems: List<Any>) {
            internalList.apply {
                clear()
                addAll(newItems)
            }
        }
    }

    private companion object {
        val RENDERER = SimpleListCellRenderer.create<Any>("") {
            when (it) {
                is String -> "$it (Not valid)"
                is CredentialIdentifier -> it.displayName
                else -> ""
            }
        }
    }
}


@file:Suppress("AbsentOrWrongFileLicense")
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package software.aws.toolkits.jetbrains.ui

import com.intellij.ui.UIBundle
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.CellBuilder
import com.intellij.ui.layout.Row
import com.intellij.ui.layout.toBinding
import kotlin.reflect.KMutableProperty0

// https://github.com/JetBrains/intellij-community/blob/e0a2caa682d9d853b2736f0cc0b303ea1936a3c3/platform/platform-impl/src/com/intellij/ui/layout/Cell.kt#L506
fun Row.intTextField(prop: KMutableProperty0<Int>, columns: Int? = null, range: IntRange? = null): CellBuilder<JBTextField> {
    val binding = prop.toBinding()
    return textField(
        { binding.get().toString() },
        { value -> value.toIntOrNull()?.let { intValue -> binding.set(range?.let { intValue.coerceIn(it.first, it.last) } ?: intValue) } },
        columns
    ).withValidationOnInput {
        val value = it.text.toIntOrNull()
        when {
            value == null -> error(UIBundle.message("please.enter.a.number"))
            range != null && value !in range -> error(UIBundle.message("please.enter.a.number.from.0.to.1", range.first, range.last))
            else -> null
        }
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.EditorTextField
import com.intellij.util.text.nullize
import com.intellij.util.textCompletion.TextFieldWithCompletion
import net.miginfocom.swing.MigLayout
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.Lambda.findPsiElementsForHandler
import software.aws.toolkits.jetbrains.services.lambda.completion.HandlerCompletionProvider
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JPanel

class HandlerPanel(private val project: Project) : JPanel(MigLayout("novisualpadding, ins 0, fillx, hidemode 3")) {
    private var handlerCompletionProvider = HandlerCompletionProvider(project, null)
    private val simpleHandler = EditorTextField()
    private val handlerWithCompletion = TextFieldWithCompletion(project, handlerCompletionProvider, "", true, true, true, true)

    private var runtime: Runtime? = null

    val handler: EditorTextField
        get() = if (handlerCompletionProvider.isCompletionSupported) {
            handlerWithCompletion
        } else {
            simpleHandler
        }

    init {
        initSimpleHandler()
        initHandlerWithCompletion()

        apply {
            add(simpleHandler, "growx")
            add(handlerWithCompletion, "growx")
        }

        switchCompletion()
    }

    override fun setEnabled(enabled: Boolean) {
        super.setEnabled(enabled)
        simpleHandler.isEnabled = enabled
        handlerWithCompletion.isEnabled = enabled
    }

    override fun setVisible(isVisible: Boolean) {
        super.setVisible(isVisible)
        if (!isVisible) {
            simpleHandler.isVisible = false
            handlerWithCompletion.isVisible = false
            return
        }

        switchCompletion()
    }

    fun setRuntime(runtime: Runtime?) {
        this.runtime = runtime
        handlerCompletionProvider = HandlerCompletionProvider(project, runtime?.let { LambdaRuntime.fromValue(it) })
        switchCompletion()
    }

    private fun initSimpleHandler() {
        simpleHandler.toolTipText = message("lambda.function.handler.tooltip")
        simpleHandler.addComponentListener(
            object : ComponentAdapter() {
                override fun componentShown(e: ComponentEvent?) {
                    super.componentShown(e)
                    simpleHandler.text = handlerWithCompletion.text
                }
            }
        )
    }

    private fun initHandlerWithCompletion() {
        handlerWithCompletion.toolTipText = message("lambda.function.handler.tooltip")
        handlerWithCompletion.addComponentListener(
            object : ComponentAdapter() {
                override fun componentShown(e: ComponentEvent?) {
                    super.componentShown(e)
                    handlerWithCompletion.text = simpleHandler.text
                }
            }
        )
    }

    private fun switchCompletion() {
        val isCompletionSupported = handlerCompletionProvider.isCompletionSupported
        handlerWithCompletion.isVisible = isCompletionSupported
        simpleHandler.isVisible = !isCompletionSupported
    }

    fun validateHandler(handlerMustExist: Boolean): ValidationInfo? {
        val handlerValue = handler.text.nullize(true)
            ?: return handler.validationInfo(message("lambda.upload_validation.handler"))

        if (handlerMustExist) {
            val runtime = runtime
                ?: throw IllegalStateException("Runtime was not set in the HandlerPanel")

            val psiFile = findPsiElementsForHandler(project, runtime, handlerValue).firstOrNull()?.containingFile
                ?: return handler.validationInfo(message("lambda.upload_validation.handler_not_found"))

            ModuleUtil.findModuleForFile(psiFile)
                ?: return handler.validationInfo(message("lambda.upload_validation.module_not_found", psiFile))
        }

        return null
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.execution.util.EnvVariablesTable
import com.intellij.execution.util.EnvironmentVariable
import com.intellij.icons.AllIcons
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.UserActivityProviderComponent
import org.jetbrains.annotations.Nls
import software.aws.toolkits.resources.message
import java.awt.Component
import java.util.LinkedHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.event.ChangeEvent
import javax.swing.event.ChangeListener
import javax.swing.event.DocumentEvent

/**
 * Our version of [com.intellij.execution.configuration.EnvironmentVariablesTextFieldWithBrowseButton].
 * It has been modified to support our use case of having a compact, generic key-value entry dialog.
 * Inheriting system env vars is not supported, but rest of UX is generally the same
 */
class KeyValueTextField(
    @Nls dialogTitle: String = message("environment.variables.dialog.title")
) : TextFieldWithBrowseButton(), UserActivityProviderComponent {
    private var data = EnvironmentVariablesData.create(emptyMap(), false)
    private val listeners = CopyOnWriteArrayList<ChangeListener>()

    var envVars: Map<String, String>
        get() = data.envs
        set(value) {
            data = EnvironmentVariablesData.create(value, false)
            text = stringify(data.envs)
        }

    init {
        addActionListener {
            EnvironmentVariablesDialog(this, dialogTitle).show()
        }

        textField.document.addDocumentListener(
            object : DocumentAdapter() {
                override fun textChanged(e: DocumentEvent) {
                    if (!StringUtil.equals(stringify(data.envs), text)) {
                        val textEnvs = EnvVariablesTable.parseEnvsFromText(text)
                        data = EnvironmentVariablesData.create(textEnvs, data.isPassParentEnvs)
                        fireStateChanged()
                    }
                }
            }
        )
    }

    private fun convertToVariables(envVars: Map<String, String>, readOnly: Boolean): List<EnvironmentVariable> = envVars.map { (key, value) ->
        object : EnvironmentVariable(key, value, readOnly) {
            override fun getNameIsWriteable(): Boolean = !readOnly
        }
    }

    override fun getDefaultIcon(): Icon = AllIcons.General.InlineVariables

    override fun getHoveredIcon(): Icon = AllIcons.General.InlineVariablesHover

    override fun addChangeListener(changeListener: ChangeListener) {
        listeners.add(changeListener)
    }

    override fun removeChangeListener(changeListener: ChangeListener) {
        listeners.remove(changeListener)
    }

    private fun fireStateChanged() {
        listeners.forEach {
            it.stateChanged(ChangeEvent(this))
        }
    }

    private fun stringify(envVars: Map<String, String>): String {
        if (envVars.isEmpty()) {
            return ""
        }

        return buildString {
            for ((key, value) in envVars) {
                if (isNotEmpty()) {
                    append(";")
                }
                append(StringUtil.escapeChar(key, ';'))
                append("=")
                append(StringUtil.escapeChar(value, ';'))
            }
        }
    }

    private inner class EnvironmentVariablesDialog(parent: Component, title: String) : DialogWrapper(parent, true) {
        private val envVarTable = EnvVariablesTable().apply {
            setValues(convertToVariables(data.envs, false))
            setPasteActionEnabled(true)
        }

        init {
            this.title = title
            init()
        }

        override fun createCenterPanel(): JComponent = envVarTable.component

        override fun doOKAction() {
            envVarTable.stopEditing()
            val newEnvVars = LinkedHashMap<String, String>()
            for (variable in envVarTable.environmentVariables) {
                newEnvVars[variable.name] = variable.value
            }
            envVars = newEnvVars
            super.doOKAction()
        }
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.util.ui.LocalPathCellEditor
import java.awt.event.ActionListener
import javax.swing.JTable

/**
 * A custom implementation of [LocalPathCellEditor] that defaults to the [project] basedir
 * if the currently selected value of a cell is null.
 */
class LocalPathProjectBaseCellEditor(private val project: Project) : LocalPathCellEditor(project) {

    override fun createActionListener(table: JTable?) = ActionListener {
        val selected = (cellEditorValue as? String)?.takeIf { StringUtil.isNotEmpty(it) }?.let { LocalFileSystem.getInstance().findFileByPath(it) }

        val initialFile = selected ?: project.guessProjectDir()
        FileChooser.chooseFile(fileChooserDescriptor, project, table, initialFile) { file ->
            var path = file.presentableUrl
            if (SystemInfo.isWindows && path.length == 2 && Character.isLetter(path[0]) && path[1] == ':') {
                path += "\\" // make path absolute
            }
            myComponent.childComponent.text = path
        }
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.ui.ComponentWithBrowseButton
import com.intellij.openapi.ui.TextComponentAccessor
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.ComboboxWithBrowseButton
import javax.swing.JComponent
import javax.swing.JTextField

/** Similar to [com.intellij.openapi.ui.TextBrowseFolderListener], but tries to set the initial directory to the assumed project root **/
private class ProjectFileBrowseListener<T : JComponent>(
    project: Project,
    component: ComponentWithBrowseButton<T>,
    fileChooserDescriptor: FileChooserDescriptor,
    textComponentAccessor: TextComponentAccessor<T>,
    private val onChosen: ((VirtualFile) -> String?)? = null
) : ComponentWithBrowseButton.BrowseFolderActionListener<T>(
    /* title */
    null,
    /* description */
    null,
    component,
    project,
    fileChooserDescriptor,
    textComponentAccessor
) {
    override fun getInitialFile(): VirtualFile? {
        val text = componentText
        if (text.isEmpty()) {
            val file = project?.guessProjectDir()
            if (file != null) {
                return file
            }
        }
        return super.getInitialFile()
    }

    override fun onFileChosen(chosenFile: VirtualFile) {
        if (onChosen == null) {
            super.onFileChosen(chosenFile)
        } else {
            myTextComponent?.let { textComponent ->
                val text = onChosen.invoke(chosenFile) ?: return@let
                myAccessor.setText(textComponent, text)
            }
        }
    }
}

/** Customization of [com.intellij.ui.components.installFileCompletionAndBrowseDialog] **/
fun <T : JComponent> installProjectFileRootedCompletionAndBrowseDialog(
    project: Project,
    component: ComponentWithBrowseButton<T>,
    textField: JTextField?,
    fileChooserDescriptor: FileChooserDescriptor,
    textComponentAccessor: TextComponentAccessor<T>,
    onChosen: ((VirtualFile) -> String)? = null
) {
    component.addActionListener(
        ProjectFileBrowseListener(project, component, fileChooserDescriptor, textComponentAccessor, onChosen)
    )

    textField?.let {
        FileChooserFactory.getInstance().installFileCompletion(
            it,
            fileChooserDescriptor,
            true,
            null /* infer disposable from UI context */
        )
    }
}

@JvmOverloads
fun installTextFieldProjectFileBrowseListener(
    project: Project,
    component: ComponentWithBrowseButton<JTextField>,
    fileChooserDescriptor: FileChooserDescriptor,
    onChosen: ((VirtualFile) -> String)? = null
) {
    installProjectFileRootedCompletionAndBrowseDialog(
        project = project,
        component = component,
        textField = component.childComponent,
        fileChooserDescriptor = fileChooserDescriptor,
        textComponentAccessor = TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
        onChosen = onChosen
    )
}

/* because [com.intellij.ui.ComboboxWithBrowseButton] is deprecated anyways and can't seem to make java happy */
@Deprecated("ComboboxWithBrowseButton is deprecated")
fun installComboBoxProjectFileBrowseListener(
    project: Project,
    component: ComboboxWithBrowseButton,
    fileChooserDescriptor: FileChooserDescriptor,
    onChosen: ((VirtualFile) -> String)? = null
) {
    installProjectFileRootedCompletionAndBrowseDialog(
        project = project,
        component = component,
        textField = null,
        fileChooserDescriptor = fileChooserDescriptor,
        textComponentAccessor = TextComponentAccessor.STRING_COMBOBOX_WHOLE_TEXT,
        onChosen = onChosen
    )
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ComboboxSpeedSearch
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.layout.Cell
import com.intellij.ui.layout.CellBuilder
import com.intellij.ui.layout.PropertyBinding
import com.intellij.ui.layout.applyToComponent
import com.intellij.ui.layout.toBinding
import com.intellij.ui.layout.toNullable
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.utils.ui.selected
import kotlin.reflect.KMutableProperty0

/**
 * Combo box used to select a region
 * TODO: Determine the UX for the box, do we want to categorize?
 */
class RegionSelector : ComboBox<AwsRegion>() {
    private val comboBoxModel = CollectionComboBoxModel<AwsRegion>()

    init {
        model = comboBoxModel
        setRenderer(RENDERER) // use the setter, not protected field
        ComboboxSpeedSearch(this)
    }

    fun setRegions(regions: List<AwsRegion>) {
        comboBoxModel.replaceAll(regions)
    }

    var selectedRegion: AwsRegion?
        get() = selected()
        set(value) {
            selectedItem = if (comboBoxModel.items.contains(value)) {
                value
            } else {
                null
            }
        }

    companion object {
        private val RENDERER = SimpleListCellRenderer.create<AwsRegion>("") {
            it.displayName
        }

        /**
         * @param serviceId If specified, will filter the list of regions down to only the regions that support the specified service
         */
        fun Cell.regionSelector(prop: KMutableProperty0<AwsRegion>, serviceId: String? = null): CellBuilder<ComboBox<AwsRegion>> =
            regionSelector(prop.toBinding(), serviceId)

        fun Cell.regionSelector(binding: PropertyBinding<AwsRegion>, serviceId: String? = null): CellBuilder<ComboBox<AwsRegion>> {
            val regionProvider = AwsRegionProvider.getInstance()
            val regions = when {
                serviceId != null -> regionProvider.allRegionsForService(serviceId).values.toMutableList()
                else -> regionProvider.allRegions().values.toMutableList()
            }
            val model = CollectionComboBoxModel(regions)
            return comboBox(model, binding.toNullable(), RENDERER).applyToComponent {
                ComboboxSpeedSearch(this)
            }
        }
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.ComponentValidator
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.ComboboxSpeedSearch
import com.intellij.ui.MutableCollectionComboBoxModel
import com.intellij.ui.layout.Cell
import com.intellij.ui.layout.PropertyBinding
import com.intellij.util.ExceptionUtil
import org.jetbrains.annotations.TestOnly
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.utils.Either
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.ui.find
import software.aws.toolkits.resources.message
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
import javax.swing.ListCellRenderer
import kotlin.reflect.KMutableProperty0

private typealias Selector<T> = Either<Any?, (T) -> Boolean>

typealias ConnectionSettingsSupplier = () -> ConnectionSettings?

@Suppress("UNCHECKED_CAST")
class ResourceSelector<T> private constructor(
    private val resourceType: () -> Resource<out Collection<T>>?,
    comboBoxModel: MutableCollectionComboBoxModel<T>,
    customRenderer: ListCellRenderer<T>?,
    loadOnCreate: Boolean,
    private val sortOnLoad: Boolean,
    private val connectionSettingsSupplier: ConnectionSettingsSupplier
) : ComboBox<T>(comboBoxModel) {
    private val loading = message("loading_resource.loading")

    @Volatile
    private var loadingStatus: Status = Status.NOT_LOADED
    private var shouldBeEnabled: Boolean = isEnabled
    private var selector: Selector<T>? = null

    @Volatile
    private var loadingFuture: CompletableFuture<*>? = null

    init {
        customRenderer?.let {
            setRenderer(customRenderer)
        }

        ComboboxSpeedSearch(this)

        if (loadOnCreate) {
            reload()
        }
    }

    @JvmOverloads
    @Synchronized
    fun reload(forceFetch: Boolean = false) {
        val previouslySelected = model.selectedItem
        val previousLoading = loadingFuture

        runInEdt(ModalityState.any()) {
            loadingStatus = Status.LOADING
            super.setEnabled(false)
            setEditable(true)

            super.setSelectedItem(loading)

            val resource = resourceType.invoke()
            val connectionSettings = connectionSettingsSupplier()
            if (resource == null || connectionSettings == null) {
                processSuccess(emptyList(), null)
            } else {
                val resultFuture = AwsResourceCache.getInstance().getResource(
                    resource,
                    connectionSettings = connectionSettings,
                    forceFetch = forceFetch
                ).toCompletableFuture()

                // If this reload will supersede a previous once, cancel it
                if (previousLoading != resultFuture) {
                    previousLoading?.cancel(true)
                }

                loadingFuture = resultFuture
                resultFuture.whenComplete { value, error ->
                    when {
                        error is CancellationException -> {
                        } // Do nothing, results don't matter
                        error != null -> processFailure(error)
                        value != null -> processSuccess(value, previouslySelected)
                    }
                }
            }
        }
    }

    override fun getModel(): MutableCollectionComboBoxModel<T> =
        // javax.swing.DefaultComboBoxModel.addAll(java.util.Collection<? extends E>) isn't in Java 8
        // The addElement method can lead to multiple selection events firing as elements are added
        // Use IntelliJ's to work around this short coming
        super.getModel() as MutableCollectionComboBoxModel<T>

    @Suppress("UNCHECKED_CAST")
    fun selected(): T? = if (loadingStatus == Status.LOADED) this.selectedItem as? T else null

    override fun setSelectedItem(item: Any?) {
        selector = Either.Left(item)

        if (item == null) {
            // TODO: Do we need to clear previouslySelected?
            super.setSelectedItem(null)
        } else {
            if (loadingStatus == Status.LOADED) {
                super.setSelectedItem(item)
            }
        }
    }

    /**
     * Should be used when a filter expression can be applied to find which item should be selected.
     * Useful when the loaded type is more complex than a string (e.g. you have an id and want to
     * select the resource that has that id).
     */
    fun selectedItem(matcher: (T) -> Boolean) {
        selector = Either.Right(matcher)
        if (loadingStatus == Status.LOADED) {
            super.setSelectedItem(model.find(matcher))
        }
    }

    @Synchronized
    override fun setEnabled(enabled: Boolean) {
        shouldBeEnabled = enabled
        when {
            loadingStatus == Status.LOADED || !enabled -> super.setEnabled(enabled)
            loadingStatus == Status.NOT_LOADED -> reload()
        }
    }

    val isLoading: Boolean
        get() = loadingStatus == Status.LOADING

    @TestOnly
    fun forceLoaded() {
        loadingStatus = Status.LOADED
    }

    private fun processSuccess(value: Collection<T>, previouslySelected: Any?) {
        runInEdt(ModalityState.any()) {
            setEditable(false)

            if (sortOnLoad) {
                model.replaceAll(value.sortedBy { it.toString().toLowerCase() })
            } else {
                model.replaceAll(value.toList())
            }
            loadingStatus = Status.LOADED

            super.setEnabled(shouldBeEnabled)

            when {
                value.isEmpty() -> super.setSelectedItem(null)
                value.size == 1 -> super.setSelectedItem(value.first())
                else -> {
                    // if the select item was removed or set to an invalid value, setSelectedItem
                    // will cause the selected item to be the string "Loading..." so we need to
                    // make sure after setting that that is not the case. Otherwise, set it to
                    // null to inform the user they need to set it again
                    super.setSelectedItem(determineSelection(selector, previouslySelected))
                    if (selectedItem == loading) {
                        super.setSelectedItem(null)
                    }
                }
            }
        }
    }

    private fun processFailure(error: Throwable) {
        val message = message("loading_resource.failed")
        LOG.warn(error) { message }
        runInEdt(ModalityState.any()) {
            loadingStatus = Status.NOT_LOADED
            super.setSelectedItem(message)
            notifyError(message, ExceptionUtil.getThrowableText(error))
            val validationInfo = ValidationInfo(error.message ?: message, this)
            ComponentValidator.createPopupBuilder(validationInfo, null)
                .setCancelOnClickOutside(true)
                .createPopup()
                .showUnderneathOf(this)
        }
    }

    private fun determineSelection(selector: Selector<T>?, previouslySelected: Any?): Any? = when (selector) {
        is Either.Right<(T) -> Boolean> -> model.find(selector.value)
        is Either.Left<*> -> selector.value
        else -> previouslySelected
    }

    private enum class Status {
        NOT_LOADED,
        LOADING,
        LOADED
    }

    companion object {
        private val LOG = getLogger<ResourceSelector<*>>()

        @JvmStatic
        fun builder() = object : ResourceBuilder {
            override fun <T> resource(resource: (() -> Resource<out Collection<T>>?)): ResourceBuilderOptions<T> = ResourceBuilderOptions(resource)
        }
    }

    interface ResourceBuilder {
        fun <T> resource(resource: Resource<out Collection<T>>): ResourceBuilderOptions<T> = resource { resource }
        fun <T> resource(resource: (() -> Resource<out Collection<T>>?)): ResourceBuilderOptions<T>
    }

    class ResourceBuilderOptions<T> internal constructor(private val resource: (() -> Resource<out Collection<T>>?)) {
        private var loadOnCreate = true
        private var sortOnLoad = true
        private var comboBoxModel: MutableCollectionComboBoxModel<T> = MutableCollectionComboBoxModel<T>()
        private var customRenderer: ListCellRenderer<T>? = null
        private lateinit var connectionSettings: ConnectionSettingsSupplier

        fun comboBoxModel(comboBoxModel: MutableCollectionComboBoxModel<T>): ResourceBuilderOptions<T> = also {
            it.comboBoxModel = comboBoxModel
        }

        fun customRenderer(customRenderer: ListCellRenderer<T>): ResourceBuilderOptions<T> = also {
            it.customRenderer = customRenderer
        }

        fun disableAutomaticLoading(): ResourceBuilderOptions<T> = also { it.loadOnCreate = false }

        fun disableAutomaticSorting(): ResourceBuilderOptions<T> = also { it.sortOnLoad = false }

        fun awsConnection(project: Project): ResourceBuilderOptions<T> = awsConnection { AwsConnectionManager.getInstance(project).connectionSettings() }

        fun awsConnection(connectionSettings: ConnectionSettings): ResourceBuilderOptions<T> = awsConnection { connectionSettings }

        fun awsConnection(connectionSettings: ConnectionSettingsSupplier): ResourceBuilderOptions<T> = also {
            it.connectionSettings = connectionSettings
        }

        fun build() = ResourceSelector(
            resource,
            comboBoxModel,
            customRenderer,
            loadOnCreate,
            sortOnLoad,
            connectionSettings
        )
    }
}

inline fun <reified T : Any> Cell.resourceSelector(resource: Resource<out Collection<T>>, connectionSettings: ConnectionSettings, prop: KMutableProperty0<T?>) =
    component(ResourceSelector.builder().resource(resource).awsConnection(connectionSettings).build()).withBinding(
        { it.selected() },
        { component, value -> component.selectedItem = value },
        PropertyBinding(prop::get, prop::set)
    )


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.ui.ValidationInfo
import org.jetbrains.annotations.TestOnly
import software.aws.toolkits.resources.message
import java.awt.event.FocusAdapter
import java.awt.event.FocusEvent
import javax.swing.JPanel
import javax.swing.JSlider
import javax.swing.JTextField

// A panel with a slider and text field, of which the slider and text field always synced up.
class SliderPanel(
    private val min: Int,
    private val max: Int,
    defaultValue: Int = min,
    minTick: Int = min,
    maxTick: Int = max,
    minorTick: Int = (max - min) / 30,
    majorTick: Int = (max - min) / 5,
    snap: Boolean = false
) {
    @Suppress("UnusedPrivateMember") // root element must be bound
    private lateinit var content: JPanel
    lateinit var slider: JSlider
        private set
    lateinit var textField: JTextField
        private set

    init {
        slider.majorTickSpacing = majorTick
        slider.minorTickSpacing = minorTick
        slider.maximum = maxTick
        slider.minimum = minTick
        slider.paintLabels = true
        slider.snapToTicks = snap
        slider.value = defaultValue
        slider.addChangeListener { textField.text = validValue(slider.value).toString() }
        textField.text = slider.value.toString()
        textField.addFocusListener(
            object : FocusAdapter() {
                // When the text field lost focus, we force the value to be valid to reset to
                // - default value if the input is not a valid integer, or
                // - min if it is smaller than min, or
                // - max if it is bigger than max.
                override fun focusLost(e: FocusEvent) {
                    val value = validValue(textField.text.toInt())
                    slider.value = value
                    textField.text = value.toString()
                }
            }
        )
    }

    var value: Int
        get() = slider.value
        set(value) {
            slider.value = value
        }

    fun validate(): ValidationInfo? {
        val value = textField.text.toIntOrNull()
        return if (value == null || value < min || value > max) {
            ValidationInfo(message("lambda.slider_validation", min, max), textField)
        } else {
            null
        }
    }

    fun setEnabled(enabled: Boolean) {
        slider.isEnabled = enabled
        textField.isEnabled = enabled
    }

    @get:TestOnly
    val isVisible: Boolean
        get() = slider.parent.isVisible && slider.isVisible && textField.isVisible

    private fun validValue(originalValue: Int): Int = originalValue.coerceAtMost(max).coerceAtLeast(min)
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui

import com.intellij.BundleBase
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.ui.ComponentValidator
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.ui.layout.Cell
import com.intellij.ui.layout.CellBuilder
import com.intellij.ui.layout.LayoutBuilder
import com.intellij.ui.layout.RowBuilder
import com.intellij.ui.layout.panel
import com.intellij.util.Alarm
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.jetbrains.core.utils.buildList
import java.awt.event.ActionEvent
import javax.swing.AbstractAction
import javax.swing.JButton
import javax.swing.JComponent

class ValidatingPanel internal constructor(
    parentDisposable: Disposable,
    private val checkContinuously: Boolean,
    val contentPanel: DialogPanel,
    validatingButtons: Map<JButton, (event: ActionEvent) -> Unit>
) : BorderLayoutPanel() {
    private val disposable = Disposer.newDisposable(parentDisposable, this::class.java.name)
    private val validatingActions = createButtonActions(validatingButtons)

    // Used for the validateOnApply checking
    private val validateCallbacks = contentPanel.validateCallbacks.toList()
    private val validationAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, disposable)
    private var previousErrors = emptyList<ValidationInfo>()
    private var validatorStarted = false

    init {
        // Used for the validateOnInput checking
        contentPanel.registerValidators(disposable) { map ->
            updateActionButtons(map.isEmpty())
        }

        addToCenter(contentPanel)
    }

    private fun createButtonActions(buttons: Map<JButton, (event: ActionEvent) -> Unit>): List<ValidatingAction> = buildList(buttons.size) {
        buttons.forEach { (button, action) ->
            add(
                ValidatingAction(button.text, action).also {
                    button.action = it
                }
            )
        }
    }

    // TODO: change back to private after moving to 221 release targets
    internal fun updateActionButtons(panelIsValid: Boolean) {
        validatingActions.forEach { it.isEnabled = panelIsValid }
    }

    private fun performValidation(): List<ValidationInfo> {
        if (validateCallbacks.isNotEmpty()) {
            val result = mutableListOf<ValidationInfo>()
            for (callback in validateCallbacks) {
                callback.invoke()?.let {
                    result.add(it)
                }
            }
            return result
        }
        return emptyList()
    }

    private fun updateErrorInfo(info: List<ValidationInfo>) {
        val updateNeeded = previousErrors != info
        if (updateNeeded) {
            runOnUi {
                setErrorInfoAll(info)
                updateActionButtons(info.all { it.okEnabled })
            }
        }
    }

    fun getPreferredFocusedComponent(): JComponent? = contentPanel.preferredFocusedComponent

    private fun startTrackingValidation() {
        runOnUi {
            if (!validatorStarted) {
                validatorStarted = true
                initValidation()
            }
        }
    }

    private fun initValidation() {
        validationAlarm.cancelAllRequests()
        val validateRequest = Runnable {
            if (!isDisposed()) {
                updateErrorInfo(performValidation())
                initValidation()
            }
        }
        validationAlarm.addRequest(validateRequest, VALIDATION_INTERVAL_MS, ModalityState.stateForComponent(this))
    }

    fun runValidation(): Boolean {
        val errorList = performValidation()
        updateErrorInfo(errorList)

        return if (errorList.isNotEmpty()) {
            // Give the first error focus
            val info = errorList.first()
            info.component?.let {
                IdeFocusManager.getInstance(null).requestFocus(it, true)
            }

            if (checkContinuously) {
                startTrackingValidation()
            }
            false
        } else {
            updateErrorInfo(emptyList())
            contentPanel.apply()
            true
        }
    }

    private inner class ValidatingAction(text: String, private val listener: (ActionEvent) -> Unit) : AbstractAction(text) {
        override fun actionPerformed(e: ActionEvent) {
            if (runValidation()) {
                listener.invoke(e)
            }
        }
    }

    private fun setErrorInfoAll(latestErrors: List<ValidationInfo>) {
        if (previousErrors == latestErrors) return

        // Remove corrected errors
        previousErrors.asSequence()
            .filterNot { latestErrors.contains(it) }
            .mapNotNull {
                it.component?.let { c ->
                    ComponentValidator.getInstance(c)?.orElseGet(null)
                }
            }
            .forEach { it.updateInfo(null) }

        previousErrors = latestErrors
        previousErrors.forEach {
            it.component?.let { c ->
                val validator = ComponentValidator.getInstance(c).orElseGet {
                    ComponentValidator(disposable).installOn(c)
                }

                validator.updateInfo(it)
            }
        }
    }

    private fun isDisposed(): Boolean = Disposer.isDisposed(disposable)

    private fun runOnUi(action: () -> Unit) {
        val application = ApplicationManager.getApplication()
        if (application.isDispatchThread) {
            action()
        } else {
            application.invokeLater(action, ModalityState.stateForComponent(this)) { isDisposed() }
        }
    }

    fun apply() {
        if (runValidation()) {
            contentPanel.apply()
        }
    }

    private companion object {
        const val VALIDATION_INTERVAL_MS = 300
    }
}

interface ValidatingPanelBuilder : RowBuilder {
    fun Cell.validatingButton(text: String, actionListener: (event: ActionEvent) -> Unit): CellBuilder<JButton>
}

class ValidatingPanelBuilderImpl(private val contentBuilder: LayoutBuilder) :
    ValidatingPanelBuilder,
    RowBuilder by contentBuilder {
    internal val actions = mutableMapOf<JButton, (event: ActionEvent) -> Unit>()

    override fun Cell.validatingButton(text: String, actionListener: (event: ActionEvent) -> Unit): CellBuilder<JButton> {
        val button = JButton(BundleBase.replaceMnemonicAmpersand(text))
        actions[button] = actionListener
        return component(button)
    }

    fun build(parentDisposable: Disposable, checkContinuously: Boolean, contentPanel: DialogPanel): ValidatingPanel =
        ValidatingPanel(parentDisposable, checkContinuously, contentPanel, actions)
}

fun validatingPanel(disposable: Disposable, checkContinuously: Boolean = true, init: ValidatingPanelBuilder.() -> Unit): ValidatingPanel {
    lateinit var builder: ValidatingPanelBuilderImpl
    val contentPanel = panel {
        builder = ValidatingPanelBuilderImpl(this)
        builder.init()
    }

    return builder.build(disposable, checkContinuously, contentPanel)
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.ui.connection

import software.aws.toolkits.jetbrains.ui.CredentialProviderSelector
import software.aws.toolkits.jetbrains.ui.RegionSelector
import javax.swing.JPanel

class AwsConnectionSettings {
    lateinit var panel: JPanel
    lateinit var credentialProvider: CredentialProviderSelector
    lateinit var region: RegionSelector
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.ui.connection

import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.LocatableConfigurationBase
import com.intellij.execution.configurations.RuntimeConfigurationError
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.options.SettingsEditorGroup
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.XmlSerializer
import com.intellij.util.xmlb.annotations.Property
import org.jdom.Element
import software.aws.toolkits.core.credentials.CredentialProviderNotFoundException
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.resources.message
import javax.swing.JComponent

/**
 * AWS settings for run configurations. Settings will be serialized with the rest of the run configuration.
 * @param project The active project
 * @param serviceName The ID of the service (accessed through <AWS client>.SERVICE_NAME in the JAVA SDK v2)
 * used to filter out regions based on what regions a service is available in.
 * @param settingsChangedListener A callback for when settings are changed
 */
class AwsConnectionSettingsEditor<T : AwsConnectionsRunConfigurationBase<*>>(
    project: Project,
    serviceName: String? = null,
    settingsChangedListener: (AwsRegion?, String?) -> Unit = { _, _ -> }
) : SettingsEditor<T>() {
    private val awsConnectionSelector = AwsConnectionSettingsSelector(project, serviceName) {
        // TODO: Undo this unwrapping
        settingsChangedListener.invoke(it?.region, it?.credentials?.id)
    }

    override fun createEditor(): JComponent = awsConnectionSelector.selectorPanel()

    override fun resetEditorFrom(settings: T) {
        val accountOptions = settings.serializableOptions.accountOptions
        awsConnectionSelector.resetAwsConnectionOptions(accountOptions.regionId, accountOptions.credentialProviderId)
    }

    override fun applyEditorTo(settings: T) {
        settings.credentialProviderId(awsConnectionSelector.selectedCredentialProvider())
        settings.regionId(awsConnectionSelector.selectedRegion()?.id)
    }
}

fun <T : AwsConnectionsRunConfigurationBase<*>> SettingsEditorGroup<T>.addAwsConnectionEditor(settings: AwsConnectionSettingsEditor<T>) {
    addEditor(message("aws_connection.tab.label"), settings)
}

abstract class AwsConnectionsRunConfigurationBase<T : BaseAwsConnectionOptions>(
    project: Project,
    configFactory: ConfigurationFactory,
    id: String?
) : LocatableConfigurationBase<T>(project, configFactory, id) {

    abstract val serializableOptions: T

    fun credentialProviderId(id: String?) {
        serializableOptions.accountOptions.credentialProviderId = id
    }

    fun credentialProviderId(): String? = serializableOptions.accountOptions.credentialProviderId

    fun regionId(id: String?) {
        serializableOptions.accountOptions.regionId = id
    }

    fun regionId(): String? = serializableOptions.accountOptions.regionId

    protected fun resolveCredentials() = credentialProviderId()?.let {
        try {
            val credentialManager = CredentialManager.getInstance()
            val credentialsIdentifier = credentialManager.getCredentialIdentifierById(it)
                ?: throw CredentialProviderNotFoundException("No provider with id '$it'")

            credentialManager.getAwsCredentialProvider(credentialsIdentifier, resolveRegion())
        } catch (e: CredentialProviderNotFoundException) {
            throw RuntimeConfigurationError(message("lambda.run_configuration.credential_not_found_error", it))
        } catch (e: Exception) {
            throw RuntimeConfigurationError(
                message(
                    "lambda.run_configuration.credential_error",
                    e.message ?: "Unknown"
                )
            )
        }
    } ?: throw RuntimeConfigurationError(message("lambda.run_configuration.no_credentials_specified"))

    protected fun resolveRegion() = regionId()?.let {
        AwsRegionProvider.getInstance().allRegions()[it]
    } ?: throw RuntimeConfigurationError(message("configure.validate.no_region_specified"))

    final override fun readExternal(element: Element) {
        super.readExternal(element)
        XmlSerializer.deserializeInto(serializableOptions, element)
    }

    final override fun writeExternal(element: Element) {
        super.writeExternal(element)
        XmlSerializer.serializeInto(serializableOptions, element)
    }
}

open class BaseAwsConnectionOptions {
    @get:Property(flat = true) // flat for backwards compat
    var accountOptions = AccountOptions()
}

class AccountOptions {
    var credentialProviderId: String? = null
    var regionId: String? = null
}