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

package software.aws.toolkits.jetbrains.core.executables

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.application.ApplicationManager
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.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.io.lastModified
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance.ExecutableWithPath
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.resources.message
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.FileTime
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.ConcurrentHashMap

// A startup activity to load the executable manager at startup. This validates the executables if they exist on disk
// which allows us to use them without explicitly loading loading them.
// For more background on why this is the way it is: Services are lazily loaded, which means on the first get of the service it will be loaded.
// Additionally, once it is loaded, the executables are validated in an async way, so they do not finish validating before the first real call happens.
// This means that the first call to getExecutableIfPresent will fail because it does not explicitly validate (by design).
class ExecutableLoader : StartupActivity, DumbAware {
    override fun runActivity(project: Project) {
        ExecutableManager.getInstance()
    }
}

interface ExecutableManager {
    fun getExecutable(type: ExecutableType<*>): CompletionStage<ExecutableInstance>
    fun getExecutableIfPresent(type: ExecutableType<*>): ExecutableInstance
    fun validateExecutablePath(type: ExecutableType<*>, path: Path): ExecutableInstance
    fun setExecutablePath(type: ExecutableType<*>, path: Path): CompletionStage<ExecutableInstance>
    fun removeExecutable(type: ExecutableType<*>)

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

inline fun <reified T : ExecutableType<*>> ExecutableManager.getExecutable() = getExecutable(ExecutableType.getInstance<T>())
inline fun <reified T : ExecutableType<*>> ExecutableManager.getExecutableIfPresent() = getExecutableIfPresent(ExecutableType.getInstance<T>())
inline fun <reified T : ExecutableType<*>> ExecutableManager.setExecutablePath(path: Path) = setExecutablePath(ExecutableType.getInstance<T>(), path)

private typealias ExecutableData = Triple<ExecutableState, ExecutableInstance?, FileTime?>

@State(name = "executables", storages = [Storage("aws.xml")])
class DefaultExecutableManager : PersistentStateComponent<ExecutableStateList>, ExecutableManager {
    private var internalState = ConcurrentHashMap<String, ExecutableData>()

    override fun getState(): ExecutableStateList = ExecutableStateList(internalState.values.map { it.first }.toList())

    override fun loadState(state: ExecutableStateList) {
        val newState = ConcurrentHashMap<String, ExecutableData>()

        state.value.forEach {
            val id = it.id ?: return@forEach
            newState[id] = Triple(it, null, null)
        }

        internalState = newState

        ExecutableType.executables().forEach {
            getExecutable(it)
        }
    }

    override fun getExecutableIfPresent(type: ExecutableType<*>): ExecutableInstance {
        val executableData = internalState[type.id]
        val instance = executableData?.second?.takeIf {
            when (it) {
                is ExecutableWithPath -> it.executablePath.exists()
                else -> true
            }
        }

        // If the executable is unresolved, either the path does not exist, or there is no
        // entry in the cache. In this case, always try to get the executable out of the cache.
        if (instance == null) {
            getExecutable(type).exceptionally {
                LOG.warn(it) { "Error thrown while updating executable cache" }
                null
            }
            return ExecutableInstance.UnresolvedExecutable(
                message("executableCommon.missing_executable", type.displayName, message("executableCommon.not_installed"))
            )
        }

        // Check if the set executable was modified. If it was, start an update in the background. Overlapping
        // runs of update are eventually consistent, and called often, so we do not have to keep track of the future
        val lastModified = (instance as ExecutableWithPath).executablePath.lastModifiedOrNull()
        if (lastModified != executableData.third) {
            getExecutable(type).exceptionally {
                LOG.warn(it) { "Error thrown while updating executable cache" }
                null
            }
        }
        return instance
    }

    override fun validateExecutablePath(type: ExecutableType<*>, path: Path): ExecutableInstance = validate(type, path, false)

    override fun getExecutable(type: ExecutableType<*>): CompletionStage<ExecutableInstance> {
        val future = CompletableFuture<ExecutableInstance>()
        ApplicationManager.getApplication().executeOnPooledThread {
            val loaded = internalState[type.id]
            if (loaded == null) {
                future.complete(load(type, null))
                return@executeOnPooledThread
            }

            val (persisted, instance, lastValidated) = loaded
            val lastKnownFileTime = persisted.lastKnownFileTime?.let { FileTime.fromMillis(it) }

            future.complete(
                when {
                    // If this is an autoresolved path, and has been updated, remove the autoresolvedness and revalidate
                    instance is ExecutableWithPath && persisted.autoResolved == true && instance.executablePath.isNewerThan(lastKnownFileTime) ->
                        validateAndSave(type, instance.executablePath, autoResolved = false)
                    // If it is valid, and has not changed, we return the existing cached one
                    instance is ExecutableInstance.Executable && instance.executablePath.lastModifiedOrNull() == lastValidated ->
                        instance
                    else ->
                        load(type, persisted)
                }
            )
        }
        return future
    }

    override fun setExecutablePath(type: ExecutableType<*>, path: Path): CompletionStage<ExecutableInstance> {
        val future = CompletableFuture<ExecutableInstance>()
        ApplicationManager.getApplication().executeOnPooledThread {
            val executable = validateAndSave(type, path, false)
            future.complete(executable)
        }
        return future
    }

    override fun removeExecutable(type: ExecutableType<*>) {
        internalState[type.id] = Triple(ExecutableState(type.id), null, null)
    }

    private fun load(type: ExecutableType<*>, persisted: ExecutableState?): ExecutableInstance {
        val persistedPath = persisted?.executablePath?.let { Paths.get(it) }
        val autoResolved = persisted?.autoResolved ?: false
        return when {
            persistedPath?.exists() == true -> validateAndSave(type, persistedPath, autoResolved)
            else -> resolve(type)
        }
    }

    private fun updateInternalState(type: ExecutableType<*>, instance: ExecutableInstance) {
        val resolved = instance as? ExecutableWithPath
        val newPersistedState = ExecutableState(
            type.id,
            resolved?.executablePath?.toString(),
            resolved?.autoResolved,
            resolved?.executablePath?.lastModifiedOrNull()?.toMillis()
        )
        val lastModified = try {
            resolved?.executablePath?.lastModified()
        } catch (e: Exception) {
            null
        }
        internalState[type.id] = Triple(newPersistedState, instance, lastModified)
    }

    private fun resolve(type: ExecutableType<*>): ExecutableInstance = try {
        (type as? AutoResolvable)?.resolve()?.let { validateAndSave(type, it, autoResolved = true) }
            ?: ExecutableInstance.UnresolvedExecutable(
                message("executableCommon.missing_executable", type.displayName, message("executableCommon.not_installed"))
            )
    } catch (e: Exception) {
        ExecutableInstance.UnresolvedExecutable(message("aws.settings.executables.resolution_exception", type.displayName, e.asString))
    }

    private fun validate(type: ExecutableType<*>, path: Path, autoResolved: Boolean): ExecutableInstance =
        try {
            (type as? Validatable)?.validate(path)
            determineVersion(type, path, autoResolved)
        } catch (e: IllegalStateException) {
            val errorMessage = if (SystemInfo.isWindows && type is SamExecutable) {
                message("sam.cli.version.upgrade.required.windows") + "\n" + e.asString
            } else {
                message("aws.settings.executables.executable_invalid", type.displayName, e.asString)
            }
            ExecutableInstance.InvalidExecutable(
                path,
                null,
                autoResolved,
                errorMessage
            )
        } catch (e: Exception) {
            val message = message("aws.settings.executables.executable_invalid", type.displayName, e.asString)
            LOG.warn(e) { message }

            ExecutableInstance.InvalidExecutable(
                path,
                null,
                autoResolved,
                message
            )
        }

    private fun validateAndSave(type: ExecutableType<*>, path: Path, autoResolved: Boolean): ExecutableInstance {
        val originalValue = internalState[type.id]?.second

        return when (val instance = validate(type, path, autoResolved)) {
            is ExecutableInstance.Executable -> {
                updateInternalState(type, instance)
                instance
            }
            is ExecutableInstance.UnresolvedExecutable, is ExecutableInstance.InvalidExecutable -> {
                if (originalValue is ExecutableInstance.Executable) {
                    originalValue
                } else {
                    updateInternalState(type, instance)
                    instance
                }
            }
        }
    }

    private fun determineVersion(type: ExecutableType<*>, path: Path, autoResolved: Boolean): ExecutableInstance = try {
        ExecutableInstance.Executable(path, type.version(path).toString(), autoResolved, type)
    } catch (e: Exception) {
        ExecutableInstance.InvalidExecutable(
            path,
            null,
            autoResolved,
            message("aws.settings.executables.cannot_determine_version", type.displayName, e.asString)
        )
    }

    private val Exception.asString: String get() = this.message ?: this.toString()
    private fun Path.lastModifiedOrNull() = this.takeIf { it.exists() }?.lastModified()
    private fun Path.isNewerThan(time: FileTime?): Boolean {
        if (time == null) return false
        return lastModifiedOrNull()?.let { it.toMillis() > time.toMillis() } == true
    }

    companion object {
        val LOG = getLogger<DefaultExecutableManager>()
    }
}

sealed class ExecutableInstance {
    abstract val version: String?

    interface ExecutableWithPath {
        val executablePath: Path
        val autoResolved: Boolean
    }

    interface BadExecutable {
        val validationError: String
    }

    class Executable(
        override val executablePath: Path,
        override val version: String,
        override val autoResolved: Boolean,
        private val executableType: ExecutableType<*>
    ) : ExecutableInstance(), ExecutableWithPath {
        // TODO get executable name as part of this
        fun getCommandLine(): GeneralCommandLine =
            ExecutableCommon.getCommandLine(executablePath.toAbsolutePath().toString(), executablePath.fileName.toString(), executableType)
    }

    class InvalidExecutable(
        override val executablePath: Path,
        override val version: String?,
        override val autoResolved: Boolean,
        override val validationError: String
    ) : ExecutableInstance(), ExecutableWithPath, BadExecutable

    class UnresolvedExecutable(override val validationError: String) : ExecutableInstance(), BadExecutable {
        override val version: String? = null
    }
}

// PersistentStateComponent requires a bean, so we wrap the List
data class ExecutableStateList(
    var value: List<ExecutableState> = listOf()
)

data class ExecutableState(
    var id: String? = null,
    var executablePath: String? = null,
    var autoResolved: Boolean? = false,
    var lastKnownFileTime: Long? = null
)


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

package software.aws.toolkits.jetbrains.core.executables

import com.intellij.openapi.application.PathManager
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.util.io.createDirectories
import java.nio.file.Path
import java.nio.file.Paths

interface ExecutableType<VersionScheme> {
    val id: String
    val displayName: String

    /**
     * Determine the version number of the given path
     */
    fun version(path: Path): VersionScheme

    companion object {
        val EP_NAME = ExtensionPointName<ExecutableType<*>>("aws.toolkit.executable")

        internal fun executables(): List<ExecutableType<*>> = EP_NAME.extensionList

        @JvmStatic
        fun <T : ExecutableType<*>> getExecutable(clazz: Class<T>): T = executables().filterIsInstance(clazz).first()

        inline fun <reified T : ExecutableType<*>> getInstance(): ExecutableType<*> = getExecutable(T::class.java)

        val EXECUTABLE_DIRECTORY: Path = Paths.get(PathManager.getSystemPath(), "aws-static-resources", "executables").createDirectories()
    }
}

interface AutoResolvable {
    /**
     * Attempt to automatically resolve the path
     *
     * @return the resolved path or null if not found
     * @throws Exception if an exception occurred attempting to resolve the path
     */
    fun resolve(): Path?
}

@Deprecated("Should not be used, delete after ToolManager migration")
interface Validatable {
    /**
     * Validate the executable at the given path, beyond being a supported version to ensure this executable is compatible wit the toolkit.
     */
    fun validate(path: Path)
}


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

package software.aws.toolkits.jetbrains.core.execution

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.value.TargetEnvironmentFunction
import com.intellij.execution.target.value.constant
import com.intellij.openapi.util.Key
import com.intellij.util.xmlb.XmlSerializer
import org.jdom.Element
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.credentials.mergeWithExistingEnvironmentVariables
import software.aws.toolkits.core.credentials.toEnvironmentVariables
import software.aws.toolkits.core.region.mergeWithExistingEnvironmentVariables
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.execution.AwsCredentialInjectionOptions.Companion.DEFAULT_OPTIONS
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.Result.Failed
import software.aws.toolkits.telemetry.Result.Succeeded

class AwsConnectionRunConfigurationExtension<T : RunConfigurationBase<*>> {
    fun addToTargetEnvironment(configuration: T, environment: MutableMap<String, TargetEnvironmentFunction<String>>, runtimeString: () -> String? = { null }) {
        injectCredentials(configuration, runtimeString) { settings ->
            val putFn = { map: Map<String, String> ->
                map.forEach { (k, v) -> environment[k] = constant(v) }
            }

            settings.region.mergeWithExistingEnvironmentVariables(environment.keys, putFn)
            settings.credentials.resolveCredentials().mergeWithExistingEnvironmentVariables(environment.keys, environment::remove, putFn)
        }
    }

    fun addEnvironmentVariables(configuration: T, environment: MutableMap<String, String>, runtimeString: () -> String? = { null }) {
        injectCredentials(configuration, runtimeString) {
            it.region.mergeWithExistingEnvironmentVariables(environment)
            it.credentials.resolveCredentials().mergeWithExistingEnvironmentVariables(environment)
        }
    }

    fun addToTargetCommandLineBuilder(configuration: T, cmdLine: TargetedCommandLineBuilder, runtimeString: () -> String? = { null }) {
        injectCredentials(configuration, runtimeString) {
            it.credentials.resolveCredentials().toEnvironmentVariables().forEach { key, value -> cmdLine.addEnvironmentVariable(key, value) }
            it.region.toEnvironmentVariables().forEach { key, value -> cmdLine.addEnvironmentVariable(key, value) }
        }
    }

    private fun injectCredentials(configuration: T, runtimeString: () -> String?, environmentMutator: (ConnectionSettings) -> Unit) {
        try {
            val credentialConfiguration = credentialConfiguration(configuration) ?: return
            if (credentialConfiguration == DEFAULT_OPTIONS) return

            val connection = getConnection(configuration, credentialConfiguration)
            environmentMutator(connection)
            AwsTelemetry.injectCredentials(configuration.project, result = Succeeded, runtimeString = tryOrNull { runtimeString() })
        } catch (e: Exception) {
            AwsTelemetry.injectCredentials(configuration.project, result = Failed, runtimeString = tryOrNull { runtimeString() })
            LOG.error(e) { message("run_configuration_extension.inject_aws_connection_exception") }
        }
    }

    fun validateConfiguration(runConfiguration: T) {
        val credentialConfiguration = runConfiguration.getCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY) ?: return
        if (credentialConfiguration == DEFAULT_OPTIONS) return

        getConnection(runConfiguration, credentialConfiguration)
    }

    fun readExternal(runConfiguration: T, element: Element) {
        runConfiguration.putCopyableUserData(
            AWS_CONNECTION_RUN_CONFIGURATION_KEY,
            XmlSerializer.deserialize(
                element,
                AwsCredentialInjectionOptions::class.java
            )
        )
    }

    fun writeExternal(runConfiguration: T, element: Element) {
        runConfiguration.getCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY)?.let {
            XmlSerializer.serializeInto(it, element)
        }
    }

    private fun credentialConfiguration(configuration: T) = configuration.getCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY)

    private fun getConnection(configuration: T, credentialConfiguration: AwsCredentialInjectionOptions): ConnectionSettings {
        val regionProvider = AwsRegionProvider.getInstance()
        val credentialManager = CredentialManager.getInstance()

        return if (credentialConfiguration.useCurrentConnection) {
            AwsConnectionManager.getInstance(configuration.project).connectionSettings() ?: throw RuntimeException(message("configure.toolkit"))
        } else {
            val region = credentialConfiguration.region?.let {
                regionProvider.allRegions()[it]
            } ?: throw IllegalStateException(message("configure.validate.no_region_specified"))

            val credentialProviderId = credentialConfiguration.credential ?: throw IllegalStateException(message("aws.notification.credentials_missing"))

            val credentialProvider = credentialManager.getCredentialIdentifierById(credentialProviderId)?.let {
                credentialManager.getAwsCredentialProvider(it, region)
            } ?: throw RuntimeException(message("aws.notification.credentials_missing"))

            ConnectionSettings(credentialProvider, region)
        }
    }

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

fun <T : RunConfigurationBase<*>> AwsConnectionRunConfigurationExtension<T>.addEnvironmentVariables(
    configuration: T,
    cmdLine: GeneralCommandLine,
    runtimeString: () -> String? = { null }
) = addEnvironmentVariables(configuration, cmdLine.environment, runtimeString)

fun <T : RunConfigurationBase<*>> connectionSettingsEditor(configuration: T): AwsConnectionExtensionSettingsEditor<T> =
    configuration.getProject().let { AwsConnectionExtensionSettingsEditor(it, false) }

val AWS_CONNECTION_RUN_CONFIGURATION_KEY =
    Key.create<AwsCredentialInjectionOptions>(
        "aws.toolkit.runConfigurationConnection"
    )

data class AwsCredentialInjectionOptions(
    var useCurrentConnection: Boolean = false,
    var region: String? = null,
    var credential: String? = null
) {
    companion object {
        operator fun invoke(block: AwsCredentialInjectionOptions.() -> Unit) = AwsCredentialInjectionOptions().apply(block)
        val DEFAULT_OPTIONS by lazy { AwsCredentialInjectionOptions() }
    }
}


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

package software.aws.toolkits.jetbrains.core.execution

import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.ui.dsl.builder.panel
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.ui.CredentialProviderSelector
import software.aws.toolkits.resources.message
import javax.swing.JComponent

class AwsConnectionExtensionSettingsEditor<T : RunConfigurationBase<*>>(private val project: Project, private val showHeader: Boolean) : SettingsEditor<T>() {
    internal val view = AwsConnectionExtensionSettingsPanel()
    private val regionProvider = AwsRegionProvider.getInstance()
    private val credentialManager = CredentialManager.getInstance()

    init {
        view.manuallyConfiguredConnection.addActionListener { updateComponents() }
        view.none.addActionListener { updateComponents() }
        view.useCurrentConnection.addActionListener { updateComponents() }
    }

    override fun resetEditorFrom(configuration: T) {
        configuration.getCopyableUserData(AWS_CONNECTION_RUN_CONFIGURATION_KEY)?.let { config ->
            view.region.isEnabled = false
            view.credentialProvider.isEnabled = false
            when {
                config.useCurrentConnection -> {
                    view.useCurrentConnection.isSelected = true
                }
                config.region != null || config.credential != null -> {
                    view.manuallyConfiguredConnection.isSelected = true
                    populateRegionsIfRequired()
                    populateCredentialsIfRequired()
                    view.region.isEnabled = true
                    view.credentialProvider.isEnabled = true

                    view.region.selectedRegion = config.region?.let { regionProvider[it] }
                    when (val credential = config.credential) {
                        is String -> view.credentialProvider.setSelectedCredential(credential)
                        else -> view.credentialProvider.selectedItem = null
                    }
                }
                else -> {
                    view.none.isSelected = true
                }
            }
        }
    }

    override fun createEditor(): JComponent = if (showHeader) {
        panel {
            collapsibleGroup(message("aws_connection.tab.label")) {
                row {
                    cell(view.panel)
                }
            }.also {
                it.expanded = true
            }
        }
    } else {
        view.panel
    }

    public override fun applyEditorTo(configuration: T) {
        configuration.putCopyableUserData(
            AWS_CONNECTION_RUN_CONFIGURATION_KEY,
            AwsCredentialInjectionOptions().also {
                when {
                    view.useCurrentConnection.isSelected -> it.useCurrentConnection = true
                    view.manuallyConfiguredConnection.isSelected -> {
                        it.region = view.region.selectedRegion?.id
                        it.credential = view.credentialProvider.getSelectedCredentialsProvider()
                    }
                }
            }
        )
    }

    private fun updateComponents() {
        if (view.manuallyConfiguredConnection.isSelected) {
            populateRegionsIfRequired()
            populateCredentialsIfRequired()
            if (view.region.selectedRegion == null || view.credentialProvider.selectedItem == null) {
                AwsConnectionManager.getInstance(project).connectionSettings()?.run {
                    if (view.region.selectedRegion == null) {
                        view.region.selectedRegion = region
                    }
                    if (view.credentialProvider.selectedItem == null) {
                        view.credentialProvider.setSelectedCredential(credentials.id)
                    }
                }
            }
        }
        view.region.isEnabled = view.manuallyConfiguredConnection.isSelected
        view.credentialProvider.isEnabled = view.manuallyConfiguredConnection.isSelected
    }

    private fun populateCredentialsIfRequired() {
        if (view.credentialProvider.itemCount == 0) {
            view.credentialProvider.setCredentialsProviders(credentialManager.getCredentialIdentifiers())
        }
    }

    private fun populateRegionsIfRequired() {
        if (view.region.itemCount == 0) {
            view.region.setRegions(regionProvider.allRegions().values.toList())
        }
    }

    private fun CredentialProviderSelector.setSelectedCredential(id: String) {
        when (val identifier = credentialManager.getCredentialIdentifierById(id)) {
            is CredentialIdentifier -> setSelectedCredentialsProvider(identifier)
            else -> setSelectedInvalidCredentialsProvider(id)
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.execution

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

class AwsConnectionExtensionSettingsPanel {
    lateinit var panel: JPanel
    lateinit var none: JRadioButton
    lateinit var useCurrentConnection: JRadioButton
    lateinit var manuallyConfiguredConnection: JRadioButton
    lateinit var credentialProvider: CredentialProviderSelector
    lateinit var region: RegionSelector
}


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

package software.aws.toolkits.jetbrains.core.execution

import com.intellij.execution.RunConfigurationExtension
import com.intellij.execution.application.ApplicationConfiguration
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.ui.SettingsEditorFragment
import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfiguration
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.roots.ModuleRootManager
import org.jdom.Element
import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperiment
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.resources.message

class JavaAwsConnectionExtension : RunConfigurationExtension() {
    private val delegate = AwsConnectionRunConfigurationExtension<RunConfigurationBase<*>>()

    /*
     * This works with Maven and IntelliJ managed, but breaks for Gradle. In gradle the run config runs another run config (the actual Gradle one), which
     * does not pass down the environment variables. The base that controls this is ExternalSystemRunConfiguration, so we should use that to be safe
     * so that we don't encounter a similar situation with a different class based on it.
     */
    override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean =
        JavaAwsConnectionExperiment.isEnabled() && configuration !is ExternalSystemRunConfiguration

    override fun <T : RunConfigurationBase<*>> updateJavaParameters(configuration: T, params: JavaParameters, runnerSettings: RunnerSettings?) {
        if (JavaAwsConnectionExperiment.isEnabled()) {
            val environment = params.env

            delegate.addEnvironmentVariables(configuration, environment, runtimeString = { determineVersion(configuration) })
        }
    }

    override fun getEditorTitle() = message("aws_connection.tab.label")

    override fun <T : RunConfigurationBase<*>> createEditor(configuration: T): SettingsEditor<T>? = connectionSettingsEditor(configuration)

    override fun <T : RunConfigurationBase<*>> createFragments(configuration: T): List<SettingsEditor<T>>? =
        listOf(
            SettingsEditorFragment.createWrapper(
                serializationId,
                editorTitle,
                null,
                AwsConnectionExtensionSettingsEditor(configuration.project, true),
                ::isApplicableFor
            )
        )

    override fun validateConfiguration(configuration: RunConfigurationBase<*>, isExecution: Boolean) {
        delegate.validateConfiguration(configuration)
    }

    override fun readExternal(runConfiguration: RunConfigurationBase<*>, element: Element) = delegate.readExternal(runConfiguration, element)

    override fun writeExternal(runConfiguration: RunConfigurationBase<*>, element: Element) = delegate.writeExternal(runConfiguration, element)

    private fun <T> determineVersion(configuration: T): String? = (configuration as? ApplicationConfiguration)?.let {
        configuration.configurationModule?.module
    }?.let {
        ModuleRootManager.getInstance(it).sdk
    }?.let {
        JavaSdk.getInstance().getVersion(it)?.name
    }
}

object JavaAwsConnectionExperiment : ToolkitExperiment(
    "javaRunConfigurationExtension",
    { message("run_configuration_extension.feature.java.title") },
    { message("run_configuration_extension.feature.java.description") },
    default = true
)


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

package software.aws.toolkits.jetbrains.core.execution

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.jetbrains.python.run.AbstractPythonRunConfiguration
import com.jetbrains.python.run.PythonExecution
import com.jetbrains.python.run.PythonRunParams
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest
import com.jetbrains.python.run.target.PythonCommandLineTargetEnvironmentProvider
import software.aws.toolkits.jetbrains.core.experiments.isEnabled

class PythonAwsConnectionCommandLineTargetEnvironmentProvider : PythonCommandLineTargetEnvironmentProvider {
    private val delegate = AwsConnectionRunConfigurationExtension<AbstractPythonRunConfiguration<*>>()

    override fun extendTargetEnvironment(
        project: Project,
        helpersAwareTargetRequest: HelpersAwareTargetEnvironmentRequest,
        pythonExecution: PythonExecution,
        runParams: PythonRunParams
    ) {
        if (!PythonAwsConnectionExperiment.isEnabled() && Registry.`is`("python.use.targets.api", true)) {
            return
        }

        val configuration = (runParams as? AbstractPythonRunConfiguration<*>)
            ?: return

        delegate.addToTargetEnvironment(configuration, pythonExecution.envs, runtimeString = { configuration.getSdk()?.versionString })
    }
}


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

package software.aws.toolkits.jetbrains.core.execution

import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperiment
import software.aws.toolkits.resources.message

object PythonAwsConnectionExperiment : ToolkitExperiment(
    "pythonRunConfigurationExtension",
    { message("run_configuration_extension.feature.python.title") },
    { message("run_configuration_extension.feature.python.description") },
    default = true
)


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

package software.aws.toolkits.jetbrains.core.execution

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.openapi.options.SettingsEditor
import com.jetbrains.python.run.AbstractPythonRunConfiguration
import com.jetbrains.python.run.PythonRunConfigurationExtension
import org.jdom.Element
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.resources.message

class PythonAwsConnectionExtension : PythonRunConfigurationExtension() {
    private val delegate = AwsConnectionRunConfigurationExtension<AbstractPythonRunConfiguration<*>>()

    override fun isApplicableFor(configuration: AbstractPythonRunConfiguration<*>): Boolean = isEnabled()

    override fun isEnabledFor(applicableConfiguration: AbstractPythonRunConfiguration<*>, runnerSettings: RunnerSettings?): Boolean = isEnabled()

    override fun patchCommandLine(
        configuration: AbstractPythonRunConfiguration<*>,
        runnerSettings: RunnerSettings?,
        cmdLine: GeneralCommandLine,
        runnerId: String
    ) {
        if (isEnabled()) {
            delegate.addEnvironmentVariables(configuration, cmdLine, runtimeString = { configuration.getSdk()?.versionString })
        }
    }

    override fun readExternal(runConfiguration: AbstractPythonRunConfiguration<*>, element: Element) = delegate.readExternal(runConfiguration, element)

    override fun writeExternal(runConfiguration: AbstractPythonRunConfiguration<*>, element: Element) = delegate.writeExternal(runConfiguration, element)

    override fun getEditorTitle() = message("aws_connection.tab.label")

    override fun <P : AbstractPythonRunConfiguration<*>> createEditor(configuration: P): SettingsEditor<P>? = connectionSettingsEditor(
        configuration
    )

    override fun validateConfiguration(configuration: AbstractPythonRunConfiguration<*>, isExecution: Boolean) {
        delegate.validateConfiguration(configuration)
    }

    private fun isEnabled() = PythonAwsConnectionExperiment.isEnabled()
}


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

package software.aws.toolkits.jetbrains.core.experiments

import com.intellij.icons.AllIcons
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.ui.layout.panel
import software.aws.toolkits.core.utils.htmlWrap
import software.aws.toolkits.resources.message

class ExperimentConfigurable : BoundConfigurable(message("aws.toolkit.experimental.title")), SearchableConfigurable {
    override fun getId() = "aws.experiments"

    override fun createPanel() = panel {
        row { label(message("aws.toolkit.experimental.description").htmlWrap()).apply { component.icon = AllIcons.General.Warning } }
        ToolkitExperimentManager.visibleExperiments().forEach {
            row { checkBox(it.title(), it::isEnabled, it::setState, it.description()) }
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.experiments

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.resources.message

class ExperimentsActionGroup : DefaultActionGroup(message("aws.toolkit.experimental.title"), true), DumbAware {
    override fun getChildren(e: AnActionEvent?): Array<AnAction> =
        ToolkitExperimentManager.visibleExperiments().map { EnableExperimentAction(it) }.toTypedArray()

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = ToolkitExperimentManager.visibleExperiments().isNotEmpty()
    }
}

class EnableExperimentAction(private val experiment: ToolkitExperiment) : ToggleAction(experiment.title, experiment.description, null), DumbAware {
    override fun isSelected(e: AnActionEvent): Boolean = experiment.isEnabled()
    override fun setSelected(e: AnActionEvent, state: Boolean) = experiment.setState(state)
}


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

package software.aws.toolkits.jetbrains.core.experiments

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.BaseState
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.extensions.ExtensionPointName
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.util.messages.Topic
import com.intellij.util.xmlb.annotations.Property
import software.aws.toolkits.core.utils.replace
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.utils.createNotificationExpiringAction
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.ExperimentState.Activated
import software.aws.toolkits.telemetry.ExperimentState.Deactivated
import java.time.Duration
import java.time.Instant

/**
 * Used to control the state of an experimental feature.
 *
 * Use the `aws.toolkit.experiment` extensionpoint to register experiments. This surfaces the configuration in the AWS Settings panel - and in sub-menus.
 *
 * @param hidden determines whether this experiment should surface in the settings/menus; hidden experiments can only be enabled by system property or manually modifying config in aws.xml
 * @param default determines the default state of an experiment
 * @param suggestionSnooze how long to wait between prompting a suggestion to enable the experiment (when using the experiment suggestion system ([ToolkitExperiment.suggest]))
 *
 * `ToolkitExperiment` implementations should be an `object` for example:
 *
 * ```
 * object MyExperiment : ToolkitExperiment(..)
 * ```
 *
 * This allows simple use at branch-points for the experiment via the available extension functions:
 *
 * ```
 * if (MyExperiment.isEnabled()) {
 *   // surface experience
 * }
 * ```
 */
abstract class ToolkitExperiment(
    internal val id: String,
    internal val title: () -> String,
    internal val description: () -> String,
    internal val hidden: Boolean = false,
    internal val default: Boolean = false,
    internal val suggestionSnooze: Duration = Duration.ofDays(7)
) {
    override fun equals(other: Any?) = (other as? ToolkitExperiment)?.id?.equals(id) == true
    override fun hashCode() = id.hashCode()
}

fun ToolkitExperiment.isEnabled(): Boolean = ToolkitExperimentManager.getInstance().isEnabled(this)
internal fun ToolkitExperiment.setState(enabled: Boolean) = ToolkitExperimentManager.getInstance().setState(this, enabled)

/**
 * Surface a notification suggesting that the given experiment be enabled.
 */
fun ToolkitExperiment.suggest() {
    if (ToolkitExperimentManager.getInstance().shouldPrompt(this)) {
        notifyInfo(
            title = message("aws.toolkit.experimental.suggestion.title"),
            content = message("aws.toolkit.experimental.suggestion.description", title(), description()),
            notificationActions = listOf(
                createNotificationExpiringAction(EnableExperiment(this)),
                createNotificationExpiringAction(NeverShowAgain(this))
            ),
            stripHtml = false
        )
    }
}

private class EnableExperiment(private val experiment: ToolkitExperiment) :
    DumbAwareAction(message("aws.toolkit.experimental.enable")) {
    override fun actionPerformed(e: AnActionEvent) {
        experiment.setState(true)
    }
}

private class NeverShowAgain(private val experiment: ToolkitExperiment) : DumbAwareAction(message("settings.never_show_again")) {
    override fun actionPerformed(e: AnActionEvent) {
        ToolkitExperimentManager.getInstance().neverPrompt(experiment)
    }
}

@State(name = "experiments", storages = [Storage("aws.xml")])
internal class ToolkitExperimentManager : PersistentStateComponent<ExperimentState> {
    private val state = ExperimentState()
    private val enabledState get() = state.value

    fun isEnabled(experiment: ToolkitExperiment): Boolean =
        EP_NAME.extensionList.contains(experiment) && enabledState.getOrDefault(experiment.id, getDefault(experiment))

    fun setState(experiment: ToolkitExperiment, enabled: Boolean) {
        val previousState = isEnabled(experiment)
        if (enabled == getDefault(experiment)) {
            enabledState.remove(experiment.id)
        } else {
            enabledState[experiment.id] = enabled
        }
        if (enabled != previousState) {
            ApplicationManager.getApplication().messageBus.syncPublisher(EXPERIMENT_CHANGED).enableSettingsStateChanged(experiment)
        }
        AwsTelemetry.experimentActivation(
            experimentId = experiment.id,
            experimentState = if (enabled) {
                Activated
            } else {
                Deactivated
            }
        )
    }

    override fun getState(): ExperimentState = state

    override fun loadState(loadedState: ExperimentState) {
        state.value.replace(loadedState.value)
        state.nextSuggestion.replace(loadedState.nextSuggestion)
    }

    private fun getDefault(experiment: ToolkitExperiment): Boolean {
        val systemProperty = System.getProperty("aws.experiment.${experiment.id}")
        return when {
            systemProperty != null -> systemProperty.isBlank() || systemProperty.equals("true", ignoreCase = true)
            AwsToolkit.isDeveloperMode() -> true
            else -> experiment.default
        }
    }

    internal fun shouldPrompt(experiment: ToolkitExperiment, now: Instant = Instant.now()): Boolean {
        if (experiment.isEnabled()) {
            return false
        }
        val should = state.nextSuggestion[experiment.id]?.let { now.isAfter(Instant.ofEpochMilli(it)) } ?: true
        if (should) {
            state.nextSuggestion[experiment.id] = now.plus(experiment.suggestionSnooze).toEpochMilli()
        }
        return should
    }

    internal fun neverPrompt(experiment: ToolkitExperiment) {
        state.nextSuggestion[experiment.id] = Long.MAX_VALUE // This is ~240 years in the future, effectively "never".
    }

    companion object {
        internal val EP_NAME = ExtensionPointName.create<ToolkitExperiment>("aws.toolkit.experiment")
        internal val EXPERIMENT_CHANGED =
            Topic.create("experiment service enable state changed", ToolkitExperimentStateChangedListener::class.java)
        internal fun getInstance(): ToolkitExperimentManager = service()
        internal fun visibleExperiments(): List<ToolkitExperiment> = EP_NAME.extensionList.filterNot { it.hidden }
        internal fun enabledExperiments(): List<ToolkitExperiment> = EP_NAME.extensionList.filter { it.isEnabled() }
    }
}

internal class ExperimentState : BaseState() {
    // This represents whether an experiment is enabled or not, don't want to rename it as that will
    // cause problems with any experiments already out there in the wild who've been persisted
    // as 'value'
    @get:Property
    val value by map<String, Boolean>()

    @get:Property
    val nextSuggestion by map<String, Long>()
}

fun interface ToolkitExperimentStateChangedListener {
    fun enableSettingsStateChanged(toolkitExperiment: ToolkitExperiment)
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.ide.projectView.PresentationData
import com.intellij.openapi.extensions.ExtensionPointName
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode

/**
 * Processes AWS Explorer nodes after they are loaded from their primary source.
 *
 * This class is ran between the creation of the nodes, but before the nodes are shown on the UI.
 * Actions should be fast enough to not be detrimental to the loading times of the explorer tree.
 */
interface AwsExplorerNodeProcessor {
    companion object {
        val EP_NAME = ExtensionPointName<AwsExplorerNodeProcessor>("aws.toolkit.explorer.nodeProcessor")
    }

    /**
     * Runs after the [AwsExplorerNode.update] allowing for additional changes to its presentation.
     */
    fun postProcessPresentation(node: AwsExplorerNode<*>, presentation: PresentationData)
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.ide.projectView.TreeStructureProvider
import com.intellij.ide.util.treeView.AbstractTreeStructureBase
import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerRootNode

class AwsExplorerTreeStructure(project: Project) : AbstractTreeStructureBase(project) {
    override fun getProviders(): List<TreeStructureProvider> = defaultTreeStructureProvider + AwsExplorerTreeStructureProvider.EP_NAME.extensionList

    override fun getRootElement() = AwsExplorerRootNode(myProject)

    override fun commit() {}

    override fun hasSomethingToCommit() = false

    override fun isToBuildChildrenInBackground(element: Any) = true

    companion object {
        val defaultTreeStructureProvider = listOf(DefaultAwsExplorerTreeStructureProvider())
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.ide.projectView.TreeStructureProvider
import com.intellij.ide.projectView.ViewSettings
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.extensions.ExtensionPointName
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode

abstract class AwsExplorerTreeStructureProvider : TreeStructureProvider {
    companion object {
        val EP_NAME = ExtensionPointName<AwsExplorerTreeStructureProvider>("aws.toolkit.explorer.treeStructure")
    }

    /**
     * Hides the ViewSettings since it is tied to the project view tree
     */
    final override fun modify(
        parent: AbstractTreeNode<*>,
        children: MutableCollection<AbstractTreeNode<*>>,
        settings: ViewSettings?
    ): MutableCollection<AbstractTreeNode<*>> = modify(parent, children)

    /**
     * Runs after the [AwsExplorerNode.update] allowing for changes to the tree, like collapsing nodes
     */
    abstract fun modify(parent: AbstractTreeNode<*>, children: MutableCollection<AbstractTreeNode<*>>): MutableCollection<AbstractTreeNode<*>>
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.openapi.wm.ex.ToolWindowEx
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.core.experiments.ExperimentsActionGroup
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.utils.actions.OpenBrowserAction
import software.aws.toolkits.resources.message

class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        toolWindow.helpId = HelpIds.EXPLORER_WINDOW.id

        if (toolWindow is ToolWindowEx) {
            val actionManager = ActionManager.getInstance()
            toolWindow.setTitleActions(listOf(actionManager.getAction("aws.toolkit.explorer.titleBar")))
            toolWindow.setAdditionalGearActions(
                DefaultActionGroup().apply {
                    add(
                        OpenBrowserAction(
                            title = message("explorer.view_documentation"),
                            url = AwsToolkit.AWS_DOCS_URL
                        )
                    )
                    add(
                        OpenBrowserAction(
                            title = message("explorer.view_source"),
                            icon = AllIcons.Vcs.Vendors.Github,
                            url = AwsToolkit.GITHUB_URL
                        )
                    )
                    add(
                        OpenBrowserAction(
                            title = message("explorer.create_new_issue"),
                            icon = AllIcons.Vcs.Vendors.Github,
                            url = "${AwsToolkit.GITHUB_URL}/issues/new/choose"
                        )
                    )
                    add(actionManager.getAction("aws.toolkit.showFeedback"))
                    add(ExperimentsActionGroup())
                    add(actionManager.getAction("aws.settings.show"))
                }
            )
        }

        val contentManager = toolWindow.contentManager
        val content = contentManager.factory.createContent(AwsToolkitExplorerToolWindow.getInstance(project), null, false).also {
            it.isCloseable = true
            it.isPinnable = true
        }
        contentManager.addContent(content)
        toolWindow.activate(null)
        contentManager.setSelectedContent(content)
    }
    override fun init(toolWindow: ToolWindow) {
        toolWindow.stripeTitle = message("aws.notification.title")
    }

    companion object {
        const val TOOLWINDOW_ID = "aws.toolkit.explorer"
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.ActionToolbar
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.GotItTooltip
import com.intellij.ui.components.JBTabbedPane
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.jetbrains.core.credentials.CredsComboBoxActionGroup
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.DevToolsToolWindow
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.JComponent

class AwsToolkitExplorerToolWindow(private val project: Project) : SimpleToolWindowPanel(true, true) {
    private val tabPane = JBTabbedPane()

    private val tabComponents = mapOf<String, () -> Component>(
        EXPLORER_TAB_ID to { ExplorerToolWindow.getInstance(project) },
        DEVTOOLS_TAB_ID to { DevToolsToolWindow.getInstance(project) }
    )

    init {
        runInEdt {
            val content = BorderLayoutPanel()
            setContent(content)
            val group = CredsComboBoxActionGroup(project)

            toolbar = BorderLayoutPanel().apply {
                addToCenter(
                    ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, true).apply {
                        layoutPolicy = ActionToolbar.AUTO_LAYOUT_POLICY
                        setTargetComponent(this@AwsToolkitExplorerToolWindow)
                    }.component
                )

                val actionManager = ActionManager.getInstance()
                val rightActionGroup = DefaultActionGroup(
                    actionManager.getAction("aws.toolkit.toolwindow.credentials.rightGroup.more"),
                    actionManager.getAction("aws.toolkit.toolwindow.credentials.rightGroup.help")
                )

                addToRight(
                    ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, rightActionGroup, true).apply {
                        // revisit if these actions need the tool window as a data provider
                        setTargetComponent(component)
                    }.component
                )
            }

            // main content
            tabComponents.forEach { name, contentProvider ->
                tabPane.addTab(name, contentProvider())
            }
            content.addToCenter(tabPane)

            val toolkitToolWindowListener = ToolkitToolWindowListener(project)
            val onTabChange = {
                toolkitToolWindowListener.tabChanged(tabPane.getTitleAt(tabPane.selectedIndex))
            }
            val codeWhispererTooltip = GotItTooltip("aws.toolkit.devtool.tab.whatsnew", message("codewhisperer.explorer.tooltip.comment"), project)
                .withHeader(message("codewhisperer.explorer.tooltip.title"))
                .withPosition(Balloon.Position.above)
            getTabLabelComponent(DEVTOOLS_TAB_ID)?.let {
                codeWhispererTooltip.show(it as JComponent, GotItTooltip.TOP_MIDDLE)
            }
            tabPane.model.addChangeListener {
                onTabChange()
            }
            onTabChange()
        }
    }

    fun setDevToolsTabVisible(visible: Boolean) {
        val index = tabPane.indexOfTab(DEVTOOLS_TAB_ID)
        if (index == -1) {
            if (!visible) {
                // if we don't need to be visible we're done
                return
            }

            // else we need content
            tabComponents.onEachIndexed { i, (name, contentProvider) ->
                if (name == DEVTOOLS_TAB_ID) {
                    tabPane.insertTab(name, null, contentProvider(), null, i)
                    tabPane.selectedIndex = i
                    return
                }
            }
        }

        // we have content
        val content = tabPane.getComponentAt(index)
        if (!visible) {
            // remove if we want it gone
            content?.let {
                // only hide the compoenet instead of disposing all of them
                tabPane.removeTabAt(index)
            }
            return
        }

        // otherwise select the tab
        content?.let {
            tabPane.selectedIndex = index
        }
    }

    fun getTabLabelComponent(tabName: String): Component? {
        val index = tabPane.indexOfTab(tabName)
        if (index == -1) {
            return null
        }

        return tabPane.getTabComponentAt(index)
    }

    companion object {
        private val EXPLORER_TAB_ID = message("explorer.toolwindow.title")
        val DEVTOOLS_TAB_ID = message("aws.developer.tools.tab.title")

        fun getInstance(project: Project) = project.service<AwsToolkitExplorerToolWindow>()

        fun toolWindow(project: Project) = ToolWindowManager.getInstance(project).getToolWindow(AwsToolkitExplorerFactory.TOOLWINDOW_ID)
            ?: error("Can't find AwsToolkitExplorerToolWindow")
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.ide.util.treeView.AbstractTreeNode

class DefaultAwsExplorerTreeStructureProvider : AwsExplorerTreeStructureProvider() {
    // By default sort the children in alphabetical order
    override fun modify(parent: AbstractTreeNode<*>, children: MutableCollection<AbstractTreeNode<*>>): MutableCollection<AbstractTreeNode<*>> =
        children.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.toString() }).toMutableList()
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.panel
import software.aws.toolkits.resources.message
import javax.swing.JComponent
import javax.swing.event.DocumentEvent

class DeleteResourceDialog(
    project: Project,
    private val resourceType: String,
    private val resourceName: String,
    private val comment: String = ""
) : DialogWrapper(project) {
    private val deleteResourceConfirmation = JBTextField().apply {
        emptyText.text = message("delete_resource.confirmation_text")
        accessibleContext.accessibleName = message("general.delete_accessible_name")
    }
    private val warningIcon = JBLabel(Messages.getWarningIcon())
    private val component by lazy {
        panel {
            row {
                warningIcon(grow)
                label(message("delete_resource.message", resourceType, resourceName))
            }
            row {
                deleteResourceConfirmation(grow)
            }
            createNoteOrCommentRow(
                ComponentPanelBuilder.createCommentComponent(
                    comment,
                    true,
                    -1,
                    true
                )
            ).visible = !comment.isNullOrEmpty()
        }
    }

    init {
        super.init()
        title = message("delete_resource.title", resourceType, resourceName)
        okAction.isEnabled = false
        deleteResourceConfirmation.document.addDocumentListener(
            object : DocumentAdapter() {
                override fun textChanged(e: DocumentEvent) {
                    isOKActionEnabled = deleteResourceConfirmation.text == message("delete_resource.confirmation_text")
                }
            }
        )
    }

    override fun createCenterPanel(): JComponent = component
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.icons.AllIcons
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.components.JBList
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.resources.message
import java.awt.event.MouseEvent
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JLabel

data class PanelAction(
    val text: String,
    val icon: Icon?,
    val action: (() -> Unit)? = null
)

internal fun buildIamHelpPanel(connection: ToolkitConnection?): JComponent {
    if (connection == null) {
        return panel {
            row {
                label(message("settings.credentials.get_started"))
            }
        }
    }

    val (ctaText, ctaIcon) = if (CredentialManager.getInstance().getCredentialIdentifiers().isEmpty()) {
        message("settings.credentials.iam.add") to AllIcons.General.Add
    } else {
        message("settings.credentials.iam.select") to AllIcons.Actions.Refresh
    }

    val list = JBList(
        PanelAction(message("settings.credentials.iam.none_selected"), AllIcons.General.Error),
        PanelAction(ctaText, ctaIcon)
    )

    list.installCellRenderer {
        JLabel(it.text).apply {
            icon = it.icon
        }
    }

    object : DoubleClickListener() {
        override fun onDoubleClick(event: MouseEvent): Boolean {
            val action = list.selectedValue?.action ?: return false

            action()

            return true
        }
    }.installOn(list)

    return panel {
        row {
            cell(list)
                .horizontalAlign(HorizontalAlign.FILL)
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.execution.Location
import com.intellij.ide.DataManager
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.ide.util.treeView.NodeDescriptor
import com.intellij.ide.util.treeView.NodeRenderer
import com.intellij.ide.util.treeView.TreeState
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.Separator
import com.intellij.openapi.actionSystem.ex.ActionManagerEx
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.HyperlinkAdapter
import com.intellij.ui.HyperlinkLabel
import com.intellij.ui.JBColor
import com.intellij.ui.PopupHandler
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.TreeUIHelper
import com.intellij.ui.components.panels.NonOpaquePanel
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.tree.AsyncTreeModel
import com.intellij.ui.tree.StructureTreeModel
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.concurrency.Invoker
import com.intellij.util.ui.GridBag
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.tree.TreeUtil
import org.jetbrains.concurrency.CancellablePromise
import software.aws.toolkits.jetbrains.ToolkitPlaces
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManagerConnection
import software.aws.toolkits.jetbrains.core.credentials.ConnectionSettingsStateChangeNotifier
import software.aws.toolkits.jetbrains.core.credentials.ConnectionState
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
import software.aws.toolkits.jetbrains.core.explorer.ExplorerDataKeys.SELECTED_NODES
import software.aws.toolkits.jetbrains.core.explorer.ExplorerDataKeys.SELECTED_RESOURCE_NODES
import software.aws.toolkits.jetbrains.core.explorer.ExplorerDataKeys.SELECTED_SERVICE_NODE
import software.aws.toolkits.jetbrains.core.explorer.actions.CopyArnAction
import software.aws.toolkits.jetbrains.core.explorer.actions.DeleteResourceAction
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerServiceRootNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceActionNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceLocationNode
import software.aws.toolkits.jetbrains.services.dynamic.explorer.DynamicResourceResourceTypeNode
import java.awt.Component
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.event.MouseEvent
import javax.swing.JComponent
import javax.swing.JTextPane
import javax.swing.JTree
import javax.swing.event.HyperlinkEvent
import javax.swing.text.SimpleAttributeSet
import javax.swing.text.StyleConstants
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.TreeModel
import kotlin.reflect.KClass

class ExplorerToolWindow(project: Project) :
    SimpleToolWindowPanel(true, true),
    ConnectionSettingsStateChangeNotifier,
    ToolkitConnectionManagerListener,
    Disposable {
    private val actionManager = ActionManagerEx.getInstanceEx()
    private val treePanelWrapper = NonOpaquePanel()
    private val awsTreeModel = AwsExplorerTreeStructure(project)

    // The 4 max threads is arbitrary, but we want > 1 so that we can load more than one node at a time
    private val structureTreeModel = StructureTreeModel(awsTreeModel, null, Invoker.forBackgroundPoolWithReadAction(this), this)
    private val awsTree = createTree(AsyncTreeModel(structureTreeModel, true, this))
    private val awsTreePanel = ScrollPaneFactory.createScrollPane(awsTree)
    private val accountSettingsManager = AwsConnectionManager.getInstance(project)
    private val connectionManager = ToolkitConnectionManager.getInstance(project)

    init {
        background = UIUtil.getTreeBackground()
        setContent(treePanelWrapper)

        project.messageBus.connect(this).subscribe(AwsConnectionManager.CONNECTION_SETTINGS_STATE_CHANGED, this)
        ApplicationManager.getApplication().messageBus.connect(this).subscribe(ToolkitConnectionManagerListener.TOPIC, this)

        connectionChanged()
    }

    private fun createInfoPanel(state: ConnectionState): JComponent {
        val panel = NonOpaquePanel(GridBagLayout())

        val gridBag = GridBag()
        gridBag.defaultAnchor = GridBagConstraints.CENTER
        gridBag.defaultWeightX = 1.0
        gridBag.defaultInsets = JBUI.insets(0, JBUI.scale(30), JBUI.scale(6), JBUI.scale(30))

        val textPane = JTextPane().apply {
            val textColor = if (state is ConnectionState.InvalidConnection) {
                JBColor.red
            } else {
                UIUtil.getInactiveTextColor()
            }

            with(styledDocument) {
                val center = SimpleAttributeSet().apply {
                    StyleConstants.setAlignment(this, StyleConstants.ALIGN_CENTER)
                    StyleConstants.setForeground(this, textColor)
                }
                setParagraphAttributes(0, length, center, false)
            }

            text = state.displayMessage
            isEditable = false
            background = UIUtil.getTreeBackground()
        }

        panel.add(textPane, gridBag.nextLine().next().fillCell())

        state.actions.forEach {
            panel.add(createActionLabel(it), gridBag.nextLine().next())
        }

        return panel
    }

    private fun createActionLabel(action: AnAction): HyperlinkLabel {
        val label = HyperlinkLabel(action.templateText ?: "BUG: $action lacks a text description")
        label.addHyperlinkListener(
            object : HyperlinkAdapter() {
                override fun hyperlinkActivated(e: HyperlinkEvent) {
                    val event = AnActionEvent.createFromAnAction(action, e.inputEvent, ActionPlaces.UNKNOWN, DataManager.getInstance().getDataContext(label))
                    action.actionPerformed(event)
                }
            }
        )

        return label
    }

    override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
        connectionChanged()
    }

    override fun settingsStateChanged(newState: ConnectionState) {
        connectionChanged()
    }

    private fun connectionChanged() {
        val connection = connectionManager.activeConnection()
        val credentialstate = accountSettingsManager.connectionState

        when (connection) {
            is AwsConnectionManagerConnection -> {
                runInEdt {
                    treePanelWrapper.setContent(
                        when (credentialstate) {
                            is ConnectionState.ValidConnection -> {
                                invalidateTree()
                                awsTreePanel
                            }
                            else -> createInfoPanel(credentialstate)
                        }
                    )
                }
            }
            null, is AwsBearerTokenConnection -> {
                runInEdt {
                    treePanelWrapper.setContent(
                        buildIamHelpPanel(connection).apply {
                            isOpaque = false
                        }
                    )
                }
            }
            else -> {
                // TODO: tree doesn't support other connection types yet
            }
        }
    }

    /**
     * Invalidates tree nodes, causing IntelliJ to redraw the tree. Preserves node state.
     * Provide an AbstractTreeNode in order to redraw the tree from that point downwards
     * Otherwise redraws the entire tree
     *
     * @param selectedNode AbstractTreeNode to redraw the tree from
     */
    fun invalidateTree(selectedNode: AbstractTreeNode<*>? = null) {
        withSavedState(awsTree) {
            if (selectedNode != null) {
                structureTreeModel.invalidate(selectedNode, true)
            } else {
                structureTreeModel.invalidate()
            }
        }
    }

    fun <T : AbstractTreeNode<*>> findNode(nodeType: KClass<T>): CancellablePromise<AbstractTreeNode<*>?> =
        runReadAction {
            structureTreeModel.invoker.computeLater {
                structureTreeModel.root.children().asSequence()
                    .filterIsInstance<DefaultMutableTreeNode>()
                    .firstOrNull { nodeType.isInstance(it.userObject) }
                    ?.userObject as AbstractTreeNode<*>?
            }
        }

    // Save the state and reapply it after we invalidate (which is the point where the state is wiped).
    // Items are expanded again if their user object is unchanged (.equals()).
    private fun withSavedState(tree: Tree, block: () -> Unit) {
        // TODO: do we actually need this? re-evaluate limits at later time
        // never re-expand dynamic resource nodes because api throttles aggressively
        TreeUtil.collectExpandedPaths(tree).filter { TreeUtil.getLastUserObject(it) is DynamicResourceResourceTypeNode }.forEach {
            tree.collapsePath(it)
        }

        val state = TreeState.createOn(tree)
        block()
        state.applyTo(tree)
    }

    private fun createTree(model: TreeModel): Tree {
        val awsTree = Tree(model)
        TreeUIHelper.getInstance().installTreeSpeedSearch(awsTree)
        awsTree.isRootVisible = false
        awsTree.autoscrolls = true
        awsTree.cellRenderer = AwsTreeCellRenderer()

        object : DoubleClickListener() {
            override fun onDoubleClick(event: MouseEvent): Boolean {
                val path = awsTree.getClosestPathForLocation(event.x, event.y)
                ((path?.lastPathComponent as? DefaultMutableTreeNode)?.userObject as? AwsExplorerNode<*>)?.onDoubleClick()
                return true
            }
        }.installOn(awsTree)

        awsTree.addMouseListener(
            object : PopupHandler() {
                override fun invokePopup(comp: Component?, x: Int, y: Int) {
                    // Build a right click menu based on the selected first node
                    // All nodes must be the same type (e.g. all S3 buckets, or a service node)
                    val explorerNode = getSelectedNodesSameType<AwsExplorerNode<*>>()?.get(0) ?: return
                    val actionGroupName = (explorerNode as? ResourceActionNode)?.actionGroupName()

                    val totalActions = mutableListOf<AnAction>()

                    (actionGroupName?.let { actionManager.getAction(it) } as? ActionGroup)?.let { totalActions.addAll(it.getChildren(null)) }

                    if (explorerNode is AwsExplorerResourceNode<*>) {
                        totalActions.add(CopyArnAction())
                    }

                    totalActions.find { it is DeleteResourceAction<*> }?.let {
                        totalActions.remove(it)
                        totalActions.add(Separator.create())
                        totalActions.add(it)
                    }

                    val actionGroup = DefaultActionGroup(totalActions)
                    if (actionGroup.childrenCount > 0) {
                        val popupMenu = actionManager.createActionPopupMenu(ToolkitPlaces.EXPLORER_TOOL_WINDOW, actionGroup)
                        popupMenu.component.show(comp, x, y)
                    }
                }
            }
        )

        return awsTree
    }

    override fun getData(dataId: String) =
        when {
            SELECTED_NODES.`is`(dataId) -> getSelectedNodes<AwsExplorerNode<*>>()
            SELECTED_RESOURCE_NODES.`is`(dataId) -> getSelectedNodesSameType<AwsExplorerResourceNode<*>>()
            SELECTED_SERVICE_NODE.`is`(dataId) -> getSelectedServiceNode()
            Location.DATA_KEY.`is`(dataId) -> {
                val resourceNodes = getSelectedNodesSameType<AwsExplorerNode<*>>()
                if (resourceNodes?.size != 1) {
                    null
                } else {
                    (resourceNodes.first() as? ResourceLocationNode)?.location()
                }
            }
            else -> super.getData(dataId)
        }

    private fun getSelectedNode(): AwsExplorerNode<*>? {
        val nodes = getSelectedNodes<AwsExplorerNode<*>>()
        return if (nodes.size == 1) nodes[0] else null
    }

    private fun getSelectedServiceNode(): AwsExplorerServiceRootNode? = getSelectedNode() as? AwsExplorerServiceRootNode

    /**
     * Returns the list of selected nodes if they are all of the same type
     */
    private inline fun <reified T : AwsExplorerNode<*>> getSelectedNodesSameType(): List<T>? {
        val selectedNodes = getSelectedNodes<T>()
        if (selectedNodes.isEmpty()) {
            return null
        }

        val firstClass = selectedNodes[0]::class.java
        return if (selectedNodes.all { firstClass.isInstance(it) }) {
            selectedNodes
        } else {
            null
        }
    }

    private inline fun <reified T : AwsExplorerNode<*>> getSelectedNodes() = awsTree.selectionPaths?.let {
        it.map { it.lastPathComponent }
            .filterIsInstance<DefaultMutableTreeNode>()
            .map { it.userObject }
            .filterIsInstance<T>()
            .toList()
    } ?: emptyList<T>()

    private class AwsTreeCellRenderer : NodeRenderer() {
        override fun customizeCellRenderer(
            tree: JTree,
            value: Any,
            selected: Boolean,
            expanded: Boolean,
            leaf: Boolean,
            row: Int,
            hasFocus: Boolean
        ) {
            super.customizeCellRenderer(tree, value, selected, expanded, leaf, row, hasFocus)
            if (value is DefaultMutableTreeNode && value.userObject is NodeDescriptor<*>) {
                icon = (value.userObject as NodeDescriptor<*>).icon
            }
        }
    }

    override fun dispose() {
    }

    companion object {
        fun getInstance(project: Project): ExplorerToolWindow = project.service()
    }
}

object ExplorerDataKeys {
    /**
     * Returns all the selected resource nodes. getData() will return null if not all selected items are same type
     */
    val SELECTED_RESOURCE_NODES = DataKey.create<List<AwsExplorerResourceNode<*>>>("aws.explorer.resourceNodes")

    /**
     * Returns the selected Service node. getData() will return null if more than one item is selected
     */
    val SELECTED_SERVICE_NODE = DataKey.create<AwsExplorerNode<*>>("aws.explorer.serviceNode")

    /**
     * Returns all the explorer nodes. getData() will return null if not all selected items are same type
     */
    val SELECTED_NODES = DataKey.create<List<AwsExplorerNode<*>>>("aws.explorer.explorerNodes")
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettingsOrThrow
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.DevToolsToolWindow

fun Project.refreshAwsTree(resource: Resource<*>? = null, connectionSettings: ConnectionSettings = getConnectionSettingsOrThrow()) {
    if (resource == null) {
        AwsResourceCache.getInstance().clear(connectionSettings)
    } else {
        AwsResourceCache.getInstance().clear(resource, connectionSettings)
    }

    runInEdt {
        // redraw explorer
        ExplorerToolWindow.getInstance(this).invalidateTree()
    }
}

fun Project.refreshDevToolTree() {
    runInEdt {
        if (this.isDisposed) return@runInEdt
        DevToolsToolWindow.getInstance(this).redrawContent()
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ex.ToolWindowEx
import software.aws.toolkits.resources.message

class ToolkitToolWindowListener(project: Project) {
    private val toolWindow by lazy { AwsToolkitExplorerToolWindow.toolWindow(project) }
    private val actionManager by lazy { ActionManager.getInstance() }
    private val explorerActions by lazy { listOf(actionManager.getAction("aws.toolkit.explorer.titleBar")) }

    private val developerToolsActions by lazy {
        listOf(
            actionManager.getAction("aws.toolkit.showFeedback")
        )
    }

    fun tabChanged(tabName: String) {
        // compiler can't smart cast since property is lazy and therefore has a custom getter
        toolWindow.let {
            if (it is ToolWindowEx) {
                if (tabName == message("explorer.toolwindow.title")) {
                    it.setTitleActions(explorerActions)
                } else if (tabName == message("aws.developer.tools.tab.title")) {
                    it.setTitleActions(developerToolsActions)
                }
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.layout.GrowPolicy
import com.intellij.ui.layout.panel
import javax.swing.JComponent

class ViewResourceDialog(project: Project, val resourceType: String, actionTitle: String, checkResourceNameValidity: (resource: String?) -> Boolean) :
    DialogWrapper(project) {
    var resourceName = ""
    private val component by lazy {
        panel {
            row("$resourceType:") {
                textField(::resourceName).growPolicy(GrowPolicy.SHORT_TEXT).withErrorOnApplyIf("$resourceType must be entered") {
                    it.text.isNullOrBlank() || checkResourceNameValidity(it.text)
                }
            }
        }
    }

    init {
        title = actionTitle
        init()
    }

    override fun createCenterPanel(): JComponent = component
}


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

package software.aws.toolkits.jetbrains.core.explorer.actions

import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import software.aws.toolkits.jetbrains.core.explorer.ExplorerDataKeys
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import javax.swing.Icon

/**
 * An action from a [AwsExplorerResourceNode] that only operates on a single resource. Needed to constrain the type.
 *
 * Automatically disables the action if more than 1 node is selected.
 *
 * @see ExplorerNodeAction
 * @see SingleExplorerNodeAction
 */
abstract class SingleResourceNodeAction<in T : AwsExplorerResourceNode<*>> : SingleExplorerNodeAction<T> {
    constructor() : super()
    constructor(text: String, description: String? = null, icon: Icon? = null) : super(text, description, icon)
}

/**
 * An action from a [AwsExplorerNode] that only operates on a single resource.
 *
 * Automatically disables the action if more than 1 node is selected.
 *
 * @see ExplorerNodeAction
 */
abstract class SingleExplorerNodeAction<in T : AwsExplorerNode<*>> : ExplorerNodeAction<T> {
    constructor() : super()
    constructor(text: String, description: String? = null, icon: Icon? = null) : super(text, description, icon)

    /**
     * If only a single item is selected [update] will be invoked with that selection periodically.
     *
     * @see AnAction.update
     * @see ExplorerNodeAction.update
     */
    open fun update(selected: T, e: AnActionEvent) {}

    /**
     * If only a single item is selected [actionPerformed] will be invoked when the action is performed.
     *
     * @see AnAction.actionPerformed
     * @see ExplorerNodeAction.actionPerformed
     */
    abstract fun actionPerformed(selected: T, e: AnActionEvent)

    final override fun update(selected: List<T>, e: AnActionEvent) {
        e.presentation.isEnabled = selected.size == 1
        selected.singleOrNull()?.run { update(this, e) }
    }

    final override fun actionPerformed(selected: List<T>, e: AnActionEvent) {
        selected.singleOrNull()?.run { actionPerformed(this, e) }
    }
}

/**
 * Converts generic [ExplorerNodeAction] list into [T] typed nodes
 */
abstract class ExplorerNodeAction<in T : AwsExplorerNode<*>> : AnAction {
    constructor() : super()
    constructor(text: String, description: String? = null, icon: Icon? = null) : super(text, description, icon)

    /**
     * Invoked periodically with the selected items of type [T].
     *
     * @see AnAction.update
     */
    open fun update(selected: List<T>, e: AnActionEvent) {}

    /**
     * Invoked when the action is performed with the selected items of type [T].
     *
     * @see AnAction.actionPerformed
     */
    abstract fun actionPerformed(selected: List<T>, e: AnActionEvent)

    final override fun actionPerformed(e: AnActionEvent) {
        actionPerformed(e.selectedNodes(), e)
    }

    final override fun update(e: AnActionEvent) {
        update(e.selectedNodes(), e)
    }
}

abstract class SingleExplorerNodeActionGroup<in T : AwsExplorerNode<*>>(text: String? = null, description: String? = null, icon: Icon? = null) :
    ActionGroup(text, description, icon) {

    final override fun getChildren(e: AnActionEvent?) =
        e?.selectedNodes<T>()?.takeIf { it.size == 1 }?.first()?.let { getChildren(it, e) }?.toTypedArray() ?: emptyArray()

    abstract fun getChildren(selected: T, e: AnActionEvent): List<AnAction>
}

@Suppress("UNCHECKED_CAST")
private fun <T : AwsExplorerNode<*>> AnActionEvent?.selectedNodes(): List<T> = this?.getData(ExplorerDataKeys.SELECTED_NODES)?.mapNotNull { it as? T }
    ?: emptyList()


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

package software.aws.toolkits.jetbrains.core.explorer.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import java.awt.datatransfer.StringSelection

class CopyArnAction : SingleResourceNodeAction<AwsExplorerResourceNode<*>>(message("explorer.copy_arn"), icon = AllIcons.Actions.Copy), DumbAware {
    override fun actionPerformed(selected: AwsExplorerResourceNode<*>, e: AnActionEvent) {
        CopyPasteManager.getInstance().setContents(StringSelection(selected.resourceArn()))
        AwsTelemetry.copyArn(e.project, selected.serviceId)
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.DeleteResourceDialog
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AwsTelemetry
import software.aws.toolkits.telemetry.Result

abstract class DeleteResourceAction<in T : AwsExplorerResourceNode<*>> : SingleResourceNodeAction<T>, DumbAware {

    constructor() : super()
    constructor(text: String) : super(text, icon = AllIcons.Actions.Cancel)

    open val comment: String = ""

    final override fun actionPerformed(selected: T, e: AnActionEvent) {
        val resourceType = selected.resourceType()
        val resourceName = selected.displayName()
        val response = DeleteResourceDialog(selected.nodeProject, resourceType, resourceName, comment).showAndGet()
        if (!response) {
            AwsTelemetry.deleteResource(selected.project, selected.serviceId, Result.Cancelled)
        } else {
            ApplicationManager.getApplication().executeOnPooledThread {
                try {
                    performDelete(selected)
                    notifyInfo(project = selected.project, title = message("delete_resource.deleted", resourceType, resourceName))
                    AwsTelemetry.deleteResource(selected.project, selected.serviceId, success = true)
                } catch (e: Exception) {
                    e.notifyError(project = selected.project, title = message("delete_resource.delete_failed", resourceType, resourceName))
                    AwsTelemetry.deleteResource(selected.project, selected.serviceId, success = false)
                }
            }
        }
    }

    abstract fun performDelete(selected: T)
}


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

package software.aws.toolkits.jetbrains.core.explorer.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import software.aws.toolkits.jetbrains.core.explorer.ViewResourceDialog
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode

abstract class ViewResourceAction<in T : AwsExplorerNode<*>>(private val actionTitle: String, val resourceType: String) :
    SingleExplorerNodeAction<T>(actionTitle), DumbAware {

    override fun actionPerformed(selected: T, e: AnActionEvent) {
        val getResourceNameDialog = ViewResourceDialog(selected.nodeProject, resourceType, actionTitle, this::checkResourceNameValidity)
        if (getResourceNameDialog.showAndGet()) {
            viewResource(getResourceNameDialog.resourceName, selected)
        }
    }

    abstract fun viewResource(resourceToView: String, selected: T)

    abstract fun checkResourceNameValidity(resourceName: String?): Boolean
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab

import com.intellij.ide.ActivityTracker
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.ide.util.treeView.NodeDescriptor
import com.intellij.ide.util.treeView.NodeRenderer
import com.intellij.ide.util.treeView.TreeState
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.PopupHandler
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.TreeUIHelper
import com.intellij.ui.tree.AsyncTreeModel
import com.intellij.ui.tree.StructureTreeModel
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.concurrency.Invoker
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.tree.TreeUtil
import software.aws.toolkits.jetbrains.ToolkitPlaces
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.AbstractActionTreeNode
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.ActionGroupOnRightClick
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.PinnedConnectionNode
import java.awt.Component
import java.awt.event.MouseEvent
import javax.swing.JTree
import javax.swing.tree.DefaultMutableTreeNode

class DevToolsToolWindow(private val project: Project) : SimpleToolWindowPanel(true, true), DataProvider, Disposable {
    private val treeModel = StructureTreeModel(DevToolsTreeStructure(project), null, Invoker.forBackgroundPoolWithoutReadAction(this), this)
    private val tree = Tree(AsyncTreeModel(treeModel, true, this))

    init {
        background = UIUtil.getTreeBackground()

        TreeUIHelper.getInstance().installTreeSpeedSearch(tree)
        tree.isRootVisible = false
        tree.autoscrolls = true
        tree.cellRenderer = object : NodeRenderer() {
            override fun customizeCellRenderer(tree: JTree, value: Any?, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean) {
                super.customizeCellRenderer(tree, value, selected, expanded, leaf, row, hasFocus)
                if (value is DefaultMutableTreeNode && value.userObject is NodeDescriptor<*>) {
                    icon = (value.userObject as NodeDescriptor<*>).icon
                }
            }
        }

        object : DoubleClickListener() {
            override fun onDoubleClick(event: MouseEvent): Boolean {
                val path = tree.getPathForLocation(event.x, event.y)
                ((path?.lastPathComponent as? DefaultMutableTreeNode)?.userObject as? AbstractActionTreeNode)?.onDoubleClick(event)
                return true
            }
        }.installOn(tree)

        tree.addMouseListener(
            object : PopupHandler() {
                override fun invokePopup(comp: Component?, x: Int, y: Int) {
                    val node = getSelectedNodesSameType<AbstractTreeNode<*>>()?.get(0) ?: return
                    val actionManager = ActionManager.getInstance()
                    val totalActions = mutableListOf<AnAction>()
                    if (node is ActionGroupOnRightClick) {
                        val actionGroupName = node.actionGroupName()

                        (actionGroupName.let { groupName -> actionManager.getAction(groupName) } as? ActionGroup)?.let {
                            totalActions.addAll(it.getChildren(null))
                        }
                    }

                    if (node is PinnedConnectionNode) {
                        totalActions.add(actionManager.getAction("aws.toolkit.connection.pinning.unpin"))
                    }

                    val actionGroup = DefaultActionGroup(totalActions)
                    if (actionGroup.childrenCount > 0) {
                        val popupMenu = actionManager.createActionPopupMenu(ToolkitPlaces.DEV_TOOL_WINDOW, actionGroup)
                        popupMenu.setTargetComponent(this@DevToolsToolWindow)
                        popupMenu.component.show(comp, x, y)
                    }
                }
            }
        )

        ApplicationManager.getApplication().messageBus.connect().subscribe(
            BearerTokenProviderListener.TOPIC,
            object : BearerTokenProviderListener {
                override fun onChange(providerId: String) {
                    // redraw toolbars
                    ActivityTracker.getInstance().inc()
                    runInEdt {
                        redrawContent()
                    }
                }
            }
        )

        redrawContent()

        // TODO: does this expansion need to be conditional?
        TreeUtil.expand(tree, 2)
    }

    override fun dispose() {}

    override fun getData(dataId: String): Any? =
        when {
            DevToolsToolWindowDataKeys.SELECTED_NODES.`is`(dataId) -> getSelectedNodes<AbstractTreeNode<*>>()

            else -> null
        }

    private inline fun <reified T : AbstractTreeNode<*>> getSelectedNodesSameType(): List<T>? {
        val selectedNodes = getSelectedNodes<T>()
        if (selectedNodes.isEmpty()) {
            return null
        }

        val firstClass = selectedNodes[0]::class.java
        return if (selectedNodes.all { firstClass.isInstance(it) }) {
            selectedNodes
        } else {
            null
        }
    }

    private inline fun <reified T : AbstractTreeNode<*>> getSelectedNodes() = tree.selectionPaths?.let { treePaths ->
        treePaths.map { it.lastPathComponent }
            .filterIsInstance<DefaultMutableTreeNode>()
            .map { it.userObject }
            .filterIsInstance<T>()
            .toList()
    } ?: emptyList<T>()

    fun redrawContent() {
        setContent(
            ScrollPaneFactory.createScrollPane(tree)
        )
        // required for refresh
        val state = TreeState.createOn(tree)
        treeModel.invalidate()
        state.applyTo(tree)
    }

    companion object {
        fun getInstance(project: Project) = project.service<DevToolsToolWindow>()
    }
}

object DevToolsToolWindowDataKeys {
    val SELECTED_NODES = DataKey.create<List<AbstractTreeNode<*>>>("aws.devTools.selectedNodes")
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.explorer.devToolsTab.nodes.DevToolsServiceNode

class DevToolsTreeRootNode(private val nodeProject: Project) : AbstractTreeNode<Any>(nodeProject, Object()) {
    override fun update(presentation: PresentationData) {}

    override fun getChildren(): Collection<AbstractTreeNode<*>> = EP_NAME.extensionList
        .filter { it.enabled() }
        .map { it.buildServiceRootNode(nodeProject) }

    companion object {
        val EP_NAME = ExtensionPointName<DevToolsServiceNode>("aws.toolkit.devTools.serviceNode")
    }
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab

import com.intellij.ide.projectView.TreeStructureProvider
import com.intellij.ide.util.treeView.AbstractTreeStructureBase
import com.intellij.openapi.project.Project

class DevToolsTreeStructure(project: Project) : AbstractTreeStructureBase(project) {
    override fun getRootElement() = DevToolsTreeRootNode(myProject)

    override fun getProviders(): List<TreeStructureProvider>? = DevToolsTreeStructureProvider.EP_NAME.extensionList

    override fun commit() {}

    override fun hasSomethingToCommit(): Boolean = false
}


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

package software.aws.toolkits.jetbrains.core.explorer.devToolsTab

import com.intellij.ide.projectView.TreeStructureProvider
import com.intellij.ide.projectView.ViewSettings
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.extensions.ExtensionPointName

abstract class DevToolsTreeStructureProvider : TreeStructureProvider {
    companion object {
        val EP_NAME = ExtensionPointName<DevToolsTreeStructureProvider>("aws.toolkit.devTools.treeStructure")
    }

    final override fun modify(
        parent: AbstractTreeNode<*>,
        children: MutableCollection<AbstractTreeNode<*>>,
        settings: ViewSettings?
    ): MutableCollection<AbstractTreeNode<*>> = modify(parent, children)

    abstract fun modify(parent: AbstractTreeNode<*>, children: MutableCollection<AbstractTreeNode<*>>): MutableCollection<AbstractTreeNode<*>>
}