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

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.ui.Messages
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.s3.model.NoSuchBucketException
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.utils.getRequiredData
import software.aws.toolkits.jetbrains.services.s3.editor.S3EditorDataKeys
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.S3Telemetry

class RenameObjectAction :
    SingleS3ObjectAction(message("s3.rename.object.action"), AllIcons.Actions.RefactoringBulb) {

    override fun performAction(dataContext: DataContext, node: S3TreeNode) {
        val project = dataContext.getRequiredData(CommonDataKeys.PROJECT)
        val treeTable = dataContext.getRequiredData(S3EditorDataKeys.BUCKET_TABLE)

        val newName = Messages.showInputDialog(
            project,
            message("s3.rename.object.title", node.displayName()),
            message("s3.rename.object.action"),
            null,
            node.displayName(),
            null
        )
        if (newName == null) {
            S3Telemetry.renameObject(project, Result.Cancelled)
        } else {
            val scope = projectCoroutineScope(project)
            scope.launch {
                try {
                    treeTable.bucket.renameObject(node.key, "${node.parent?.key}$newName")
                    treeTable.invalidateLevel(node)
                    treeTable.refresh()
                    S3Telemetry.renameObject(project, Result.Succeeded)
                } catch (e: NoSuchBucketException) {
                    treeTable.bucket.handleDeletedBucket()
                    S3Telemetry.renameObject(project, Result.Failed)
                } catch (e: Exception) {
                    e.notifyError(project = project, title = message("s3.rename.object.failed"))
                    S3Telemetry.renameObject(project, Result.Failed)
                }
            }
        }
    }

    override fun enabled(node: S3TreeNode): Boolean = node is S3TreeObjectNode
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.jetbrains.services.s3.editor.S3EditorDataKeys
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeContinuationNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeErrorNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeNode
import javax.swing.Icon

abstract class S3ObjectAction(title: String, icon: Icon? = null) : DumbAwareAction(title, null, icon) {
    final override fun actionPerformed(e: AnActionEvent) = performAction(e.dataContext, selected(e.dataContext).filter { it !is S3TreeContinuationNode<*> })

    protected abstract fun performAction(dataContext: DataContext, nodes: List<S3TreeNode>)

    final override fun update(e: AnActionEvent) {
        val bucketViewer = e.dataContext.getData(S3EditorDataKeys.BUCKET_TABLE)
        // Disable the action if the bucket viewer is not in our UI hierarchy
        if (bucketViewer == null) {
            e.presentation.isEnabledAndVisible = false
            return
        }

        val selected = selected(e.dataContext)
        e.presentation.isEnabled = selected.none { it is S3TreeContinuationNode<*> || it is S3TreeErrorNode } && enabled(selected)
    }

    private fun selected(dataContext: DataContext): List<S3TreeNode> = dataContext.getData(S3EditorDataKeys.SELECTED_NODES) ?: emptyList()

    protected open fun enabled(nodes: List<S3TreeNode>): Boolean = nodes.isNotEmpty()
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.actionSystem.DataContext
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeNode
import javax.swing.Icon

abstract class SingleS3ObjectAction(title: String, icon: Icon? = null) : S3ObjectAction(title, icon) {
    final override fun performAction(dataContext: DataContext, nodes: List<S3TreeNode>) {
        if (nodes.size != 1) {
            throw IllegalStateException("SingleActionNode should only have a single node, got $nodes")
        }
        performAction(dataContext, nodes.first())
    }

    final override fun enabled(nodes: List<S3TreeNode>): Boolean = nodes.size == 1 && enabled(nodes.first())

    protected abstract fun performAction(dataContext: DataContext, node: S3TreeNode)

    protected open fun enabled(node: S3TreeNode): Boolean = true
}


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

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.project.Project
import com.intellij.util.io.isDirectory
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.utils.getRequiredData
import software.aws.toolkits.jetbrains.services.s3.editor.S3EditorDataKeys
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeTable
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.S3Telemetry
import java.nio.file.Path

class UploadObjectAction : S3ObjectAction(message("s3.upload.object.action"), AllIcons.Actions.Upload) {
    override fun performAction(dataContext: DataContext, nodes: List<S3TreeNode>) {
        val project = dataContext.getRequiredData(CommonDataKeys.PROJECT)
        val treeTable = dataContext.getRequiredData(S3EditorDataKeys.BUCKET_TABLE)
        val node = nodes.firstOrNull() ?: treeTable.rootNode

        val descriptor = FileChooserDescriptorFactory.createAllButJarContentsDescriptor()
            .withDescription(message("s3.upload.object.action", treeTable.bucket.name))
        val chooserDialog = FileChooserFactory.getInstance().createFileChooser(descriptor, project, null)
        val filesChosen = chooserDialog.choose(project).toList()

        // If there are no files chosen, the user has cancelled upload
        if (filesChosen.isEmpty()) {
            S3Telemetry.uploadObject(project, Result.Cancelled)
            return
        }

        uploadObjects(project, treeTable, filesChosen.map { it.toNioPath() }, node)
    }

    override fun enabled(nodes: List<S3TreeNode>): Boolean = nodes.isEmpty() ||
        (nodes.size == 1 && nodes.first().let { it is S3TreeDirectoryNode })
}

fun uploadObjects(project: Project, treeTable: S3TreeTable, files: List<Path>, parentNode: S3TreeNode) {
    if (files.isEmpty()) {
        return
    }
    val scope = projectCoroutineScope(project, "UploadObjectAction")
    scope.launch {
        var changeMade = false
        try {
            files.forEach {
                if (it.isDirectory()) {
                    notifyError(
                        title = message("s3.upload.object.failed", it.fileName),
                        content = message("s3.upload.directory.impossible", it.fileName),
                        project = project
                    )
                } else {
                    try {
                        treeTable.bucket.upload(project, it, parentNode.directoryPath() + it.fileName)
                        changeMade = true
                    } catch (e: Exception) {
                        e.notifyError(message("s3.upload.object.failed", it.fileName), project)
                        throw e
                    }
                }
            }

            S3Telemetry.uploadObject(project = project, result = Result.Succeeded, value = files.size.toDouble())
        } catch (e: Exception) {
            S3Telemetry.uploadObject(project = project, result = Result.Failed, value = files.size.toDouble())
        } finally {
            if (changeMade) {
                treeTable.invalidateLevel(parentNode)
                treeTable.refresh()
            }
        }
    }
}


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

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.DataContext
import software.aws.toolkits.jetbrains.core.utils.getRequiredData
import software.aws.toolkits.jetbrains.services.s3.editor.S3EditorDataKeys
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.resources.message

class ViewObjectVersionAction : SingleS3ObjectAction(message("s3.version.history.view"), AllIcons.Actions.ShowAsTree) {
    override fun performAction(dataContext: DataContext, node: S3TreeNode) {
        if (node is S3TreeObjectNode) {
            node.showHistory = true

            // TODO: Can we expand the node too
            dataContext.getRequiredData(S3EditorDataKeys.BUCKET_TABLE).refresh()
        }
    }

    override fun enabled(node: S3TreeNode): Boolean = node::class == S3TreeObjectNode::class
}


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

package software.aws.toolkits.jetbrains.services.s3.resources

import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.TestOnly
import org.slf4j.event.Level
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.Bucket
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.s3.regionForBucket
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter

object S3Resources {
    private val LOG = getLogger<S3Resources>()
    private val regions by lazy { AwsRegionProvider.getInstance().allRegions() }

    @TestOnly
    val LIST_REGIONALIZED_BUCKETS = ClientBackedCachedResource(S3Client::class, "s3.list_buckets") {
        val buckets = listBuckets().buckets()
        // TODO when the resource cache is coroutine based, remove the runBlocking and withContext
        runBlocking {
            // withContext is needed to put this on a thread pool
            withContext(getCoroutineBgContext()) {
                buckets.map { bucket ->
                    async {
                        LOG.tryOrNull("Cannot determine region for ${bucket.name()}", level = Level.WARN) {
                            regionForBucket(bucket.name())
                        }?.let { regions[it] }?.let {
                            RegionalizedBucket(bucket, it)
                        }
                    }
                }.awaitAll().filterNotNull()
            }
        }
    }

    val LIST_BUCKETS: Resource<List<Bucket>> = Resource.View(LIST_REGIONALIZED_BUCKETS) { bucketList, region ->
        bucketList.filter { it.region == region }.map { it.bucket }
    }

    @JvmStatic
    fun formatDate(date: Instant): String {
        val datetime = LocalDateTime.ofInstant(date, ZoneId.systemDefault())
        return datetime.atZone(ZoneId.systemDefault())
            .format(DateTimeFormatter.ofPattern("MMM d YYYY hh:mm:ss a z"))
    }

    data class RegionalizedBucket(val bucket: Bucket, val region: AwsRegion)
}


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

package software.aws.toolkits.jetbrains.services.schemas

import com.fasterxml.jackson.annotation.JsonProperty
import software.amazon.awssdk.services.schemas.model.SchemaSummary
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.resources.message

enum class SchemaCodeLangs(
    val apiValue: String,
    val text: String,
    val extension: String,
    val runtimeGroupId: String
) {
    JAVA8("Java8", message("schemas.schema.language.java8"), "java", BuiltInRuntimeGroups.Java),
    PYTHON3_6("Python36", message("schemas.schema.language.python3_6"), "py", BuiltInRuntimeGroups.Python),
    TYPESCRIPT("TypeScript3", message("schemas.schema.language.typescript"), "ts", BuiltInRuntimeGroups.NodeJs),
    GO1("Go1", message("schemas.schema.language.go1"), "go", BuiltInRuntimeGroups.Go);

    override fun toString() = text
}

data class Schema(val name: String, val registryName: String, val arn: String?)

data class SchemaSummary(val name: String, val registryName: String) {
    fun title(): String = name.split('.', '@').last()

    override fun toString() = "$registryName/$name"
}

data class SchemaTemplateParameters(
    val schema: software.aws.toolkits.jetbrains.services.schemas.SchemaSummary,
    val schemaVersion: String,
    val templateExtraContext: SchemaTemplateExtraContext
)

// This matches the extra_content parameters to Schema-based templates in both key and value names in cookiecutter.json in the templates used by
// SamEventBridgeStarterAppMaven and SamEventBridgeStarterAppGradle
data class SchemaTemplateExtraContext(

    // Name of schema registry
    @get:JsonProperty("AWS_Schema_registry")
    val schemaRegistry: String,

    // Name of schema root event object
    @get:JsonProperty("AWS_Schema_name")
    val schemaRootEventName: String,

    // Schema root - ie package hierarchy
    @get:JsonProperty("AWS_Schema_root")
    val schemaPackageHierarchy: String,

    // Source of schema on EventBridge bus
    @get:JsonProperty("AWS_Schema_source")
    val source: String,

    // Detail type of schema on EventBridge bus
    @get:JsonProperty("AWS_Schema_detail_type")
    val detailType: String,

    // Need to provide user agent to SAM CLI so that it will enable appTemplate-based
    @get:JsonProperty("user_agent")
    val userAgent: String = "AWSToolkit"
)

fun SchemaSummary.toDataClass(registryName: String) = Schema(
    name = this.schemaName(),
    arn = this.schemaArn(),
    registryName = registryName
)


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

package software.aws.toolkits.jetbrains.services.schemas

import com.intellij.openapi.project.Project
import icons.AwsIcons
import software.amazon.awssdk.services.schemas.SchemasClient
import software.amazon.awssdk.services.schemas.model.RegistrySummary
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerEmptyNode
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.AwsExplorerServiceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.CacheBackedAwsExplorerServiceRootNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.ResourceParentNode
import software.aws.toolkits.jetbrains.core.getResourceNow
import software.aws.toolkits.jetbrains.services.schemas.resources.SchemasResources
import software.aws.toolkits.resources.message

class SchemasServiceNode(project: Project, service: AwsExplorerServiceNode) :
    CacheBackedAwsExplorerServiceRootNode<RegistrySummary>(project, service, SchemasResources.LIST_REGISTRIES) {
    override fun displayName(): String = message("explorer.node.schemas")
    override fun toNode(child: RegistrySummary): AwsExplorerNode<*> = SchemaRegistryNode(nodeProject, child)
}

open class SchemaRegistryNode(
    project: Project,
    val registry: RegistrySummary
) : AwsExplorerResourceNode<RegistrySummary>(
    project,
    SchemasClient.SERVICE_NAME,
    registry,
    AwsIcons.Resources.SCHEMA_REGISTRY
),
    ResourceParentNode {
    override fun resourceType() = "registry"

    override fun resourceArn(): String = value.registryArn() ?: value.registryName()

    override fun toString(): String = value.registryName()

    override fun displayName(): String = value.registryName()

    override fun isAlwaysLeaf(): Boolean = false

    override fun isAlwaysShowPlus(): Boolean = true

    override fun getChildren(): List<AwsExplorerNode<*>> = super<ResourceParentNode>.getChildren()

    override fun getChildrenInternal(): List<AwsExplorerNode<*>> {
        val registryName = value.registryName()
        return nodeProject
            .getResourceNow(SchemasResources.listSchemas(registryName))
            .map { schema -> SchemaNode(nodeProject, schema.toDataClass(registryName)) }
            .toList()
    }

    override fun emptyChildrenNode(): AwsExplorerEmptyNode = AwsExplorerEmptyNode(
        nodeProject,
        message("explorer.registry.no.schema.resources")
    )
}

open class SchemaNode(
    project: Project,
    val schema: Schema
) : AwsExplorerResourceNode<Schema>(
    project,
    SchemasClient.SERVICE_NAME,
    schema,
    AwsIcons.Resources.SCHEMA
) {
    override fun resourceType() = "schema"

    override fun resourceArn() = value.arn ?: value.name

    override fun toString(): String = value.name

    override fun displayName() = value.name
}


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

package software.aws.toolkits.jetbrains.services.schemas

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.ide.scratch.ScratchFileService
import com.intellij.ide.scratch.ScratchRootType
import com.intellij.json.JsonLanguage
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.util.ExceptionUtil
import software.amazon.awssdk.services.schemas.model.DescribeSchemaResponse
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.services.schemas.resources.SchemasResources
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.SchemasTelemetry
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import kotlin.math.min

class SchemaViewer(
    private val project: Project,
    private val schemaDownloader: SchemaDownloader = SchemaDownloader(),
    private val schemaFormatter: SchemaFormatter = SchemaFormatter(),
    private val schemaPreviewer: SchemaPreviewer = SchemaPreviewer()
) {
    fun downloadAndViewSchema(schemaName: String, registryName: String, connectionSettings: ConnectionSettings): CompletionStage<Void> =
        schemaDownloader.getSchemaContent(registryName, schemaName, connectionSettings = connectionSettings)
            .thenCompose { schemaContent ->
                SchemasTelemetry.download(project, success = true)
                schemaFormatter.prettySchemaContent(schemaContent.content())
                    .thenCompose { prettySchemaContent ->
                        schemaPreviewer.openFileInEditor(
                            registryName,
                            schemaName,
                            prettySchemaContent,
                            schemaContent.schemaVersion(),
                            project,
                            connectionSettings
                        )
                    }
            }
            .exceptionally { error ->
                notifyError(message("schemas.schema.could_not_open", schemaName), ExceptionUtil.getThrowableText(error), project)
                SchemasTelemetry.download(project, success = false)
                null
            }

    fun downloadPrettySchema(
        schemaName: String,
        registryName: String,
        version: String?,
        connectionSettings: ConnectionSettings
    ): CompletionStage<String> = schemaDownloader.getSchemaContent(registryName, schemaName, version, connectionSettings)
        .thenCompose { schemaContent ->
            schemaFormatter.prettySchemaContent(schemaContent.content())
        }
        .exceptionally { error ->
            notifyError(message("schemas.schema.could_not_open", schemaName), ExceptionUtil.getThrowableText(error), project)
            null
        }
}

class SchemaDownloader {
    fun getSchemaContent(
        registryName: String,
        schemaName: String,
        version: String? = null,
        connectionSettings: ConnectionSettings
    ): CompletionStage<DescribeSchemaResponse> {
        val resource = SchemasResources.getSchema(registryName, schemaName, version)
        return AwsResourceCache.getInstance().getResource(resource, connectionSettings)
    }

    fun getSchemaContentAsJson(schemaContent: DescribeSchemaResponse): JsonNode = jacksonObjectMapper().readTree(schemaContent.content())
}

class SchemaFormatter {
    fun prettySchemaContent(rawSchemaContent: String): CompletionStage<String> {
        val future = CompletableFuture<String>()
        val mapper = jacksonObjectMapper()
        try {
            val json = mapper.readValue(rawSchemaContent, Any::class.java)
            val formatted = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json)

            future.complete(formatted)
        } catch (e: Exception) {
            future.completeExceptionally(e)
        }
        return future
    }
}

class SchemaPreviewer {
    fun openFileInEditor(
        registryName: String,
        schemaName: String,
        schemaContent: String,
        version: String,
        project: Project,
        connectionSettings: ConnectionSettings
    ): CompletionStage<Void> {
        val credentialIdentifier = connectionSettings.credentials.id
        val region = connectionSettings.region.id

        val fileName = "${credentialIdentifier}_${region}_${registryName}_${schemaName}_$version"
        val sanitizedFileName = FileUtil.sanitizeFileName(fileName, false)
        val trimmedFileNameWithExtension = sanitizedFileName.substring(0, min(sanitizedFileName.length, MAX_FILE_LENGTH)) + SCHEMA_EXTENSION

        val future = CompletableFuture<Void>()

        ApplicationManager.getApplication().invokeLater {
            try {
                val vfile = ScratchRootType.getInstance()
                    .createScratchFile(project, trimmedFileNameWithExtension, JsonLanguage.INSTANCE, schemaContent, ScratchFileService.Option.create_if_missing)

                vfile?.let {
                    val fileEditorManager = FileEditorManager.getInstance(project)
                    fileEditorManager.openTextEditor(OpenFileDescriptor(project, it), true)
                        ?: throw RuntimeException(message("schemas.schema.could_not_open", schemaName))
                }
                future.complete(null)
            } catch (e: Exception) {
                future.completeExceptionally(e)
            }
        }

        return future
    }

    companion object {
        const val SCHEMA_EXTENSION = ".json"

        const val MAX_FILE_LENGTH = 255 - SCHEMA_EXTENSION.length // min(MAX_FILE_NAME_LENGTH_WINDOWS, MAX_FILE_NAME_LENGTH_MAC)
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import icons.AwsIcons
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.credentials.activeCredentialProvider
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.resources.message

class ViewSchemaAction : SingleResourceNodeAction<SchemaNode>(message("schemas.schema.view.action"), null, AwsIcons.Actions.SCHEMA_VIEW), DumbAware {
    override fun actionPerformed(selected: SchemaNode, e: AnActionEvent) {
        SchemaViewer(selected.nodeProject).downloadAndViewSchema(
            selected.value.name,
            selected.value.registryName,
            ConnectionSettings(selected.nodeProject.activeCredentialProvider(), selected.nodeProject.activeRegion())
        )
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.code

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.DumbAware
import icons.AwsIcons
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.schemas.SchemaNode
import software.aws.toolkits.resources.message

class DownloadCodeForSchemaAction :
    SingleResourceNodeAction<SchemaNode>(message("schemas.schema.download_code_bindings.action"), null, AwsIcons.Actions.SCHEMA_CODE_GEN), DumbAware {
    override fun actionPerformed(selected: SchemaNode, e: AnActionEvent) {
        val project = e.getRequiredData(PlatformDataKeys.PROJECT)

        val schema = selected.value

        DownloadCodeForSchemaDialog(project, schema).show()
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.code

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import org.apache.commons.lang.exception.ExceptionUtils
import software.amazon.awssdk.services.schemas.model.SchemaVersionSummary
import software.aws.toolkits.jetbrains.core.getResourceNow
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroup
import software.aws.toolkits.jetbrains.services.schemas.Schema
import software.aws.toolkits.jetbrains.services.schemas.SchemaCodeLangs
import software.aws.toolkits.jetbrains.services.schemas.SchemaSummary
import software.aws.toolkits.jetbrains.services.schemas.resources.SchemasResources
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.jetbrains.utils.ui.selected
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.SchemaLanguage
import software.aws.toolkits.telemetry.SchemasTelemetry
import java.awt.event.ActionEvent
import java.io.File
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import javax.swing.Action
import javax.swing.JComponent

private val NOTIFICATION_TITLE = message("schemas.service_name")

class DownloadCodeForSchemaDialog(
    private val project: Project,
    private val schemaName: String = "",
    private val registryName: String = "",
    version: String? = null,
    language: SchemaCodeLangs? = null,
    private val onClose: (() -> Unit)? = null
) : DialogWrapper(project) {

    constructor(project: Project, schema: Schema) :
        this(
            project = project,
            schemaName = schema.name,
            registryName = schema.registryName
        )

    val schemaVersions: List<String>
    val latestVersion: String

    val view = DownloadCodeForSchemaPanel(project)
    private val validator = DownloadCodeForSchemaValidator()

    private val action: OkAction = DownloadCodeForSchemaOkAction()

    init {
        super.init()
        title = message("schemas.schema.download_code_bindings.title")

        view.heading.text = message("schemas.schema.download_code_bindings.heading", schemaName, registryName)

        schemaVersions = loadSchemaVersions()

        val allVersions = ArrayList<String>(schemaVersions)
        latestVersion = allVersions.first()
        allVersions.add(0, LATEST_VERSION)

        view.setVersions(allVersions)
        view.version.selectedItem = version ?: LATEST_VERSION

        view.setLanguages(SchemaCodeLangs.values().asList())
        view.language.selectedItem = language ?: getLanguageForCurrentRuntime()

        view.location.textField.text = getContentRootOfCurrentFile() ?: ""
    }

    private fun getContentRootOfCurrentFile(): String? {
        // Get the currently open files (plural in case they are split)
        val selectedFiles = FileEditorManager.getInstance(project).getSelectedFiles()
        if (selectedFiles.isNotEmpty()) {
            // return the content root of the first selected file
            return ProjectFileIndex.getInstance(project).getContentRootForFile(selectedFiles.first())?.path
        }
        // Otherwise, find the first content root of the project, and return that
        val contentRoots = ProjectRootManager.getInstance(project).contentRoots
        if (contentRoots.isNotEmpty()) {
            return contentRoots.first().path
        }

        return null
    }

    private fun loadSchemaVersions(): List<String> = project
        .getResourceNow(SchemasResources.getSchemaVersions(registryName, schemaName))
        .map(SchemaVersionSummary::schemaVersion)
        .sortedByDescending { s -> s.toIntOrNull() }

    private fun getLanguageForCurrentRuntime(): SchemaCodeLangs? {
        val currentRuntimeGroup = RuntimeGroup.determineRuntimeGroup(project) ?: return null

        return SchemaCodeLangs.values().firstOrNull { it.runtimeGroupId == currentRuntimeGroup.id }
    }

    override fun createCenterPanel(): JComponent? = view.content

    override fun getPreferredFocusedComponent(): JComponent? = view.version

    override fun doValidate(): ValidationInfo? = validator.validate(view)

    override fun getOKAction(): Action = action

    override fun doOKAction() {
        // Do nothing, close logic is handled separately
    }

    override fun getHelpId(): String? = HelpIds.DOWNLOAD_CODE_FOR_SCHEMA_DIALOG.id

    fun downloadSchemaCode(schemaCodeDownloader: SchemaCodeDownloader) {
        if (!okAction.isEnabled) {
            return
        }
        val schemaCodeDownloadDetails = viewToSchemaCodeDownloadDetails()

        // Telemetry for download code language
        SchemasTelemetry.download(
            project = project,
            success = true,
            schemaLanguage = SchemaLanguage.from(schemaCodeDownloadDetails.language.apiValue)
        )

        val schemaName = schemaCodeDownloadDetails.schema.name
        ProgressManager.getInstance().run(
            object : Task.Backgroundable(project, message("schemas.schema.download_code_bindings.title", schemaName), false) {
                override fun run(indicator: ProgressIndicator) {
                    notifyInfo(
                        title = NOTIFICATION_TITLE,
                        content = message("schemas.schema.download_code_bindings.notification.start", schemaName),
                        project = project
                    )

                    schemaCodeDownloader.downloadCode(schemaCodeDownloadDetails, indicator)
                        .thenCompose { schemaCoreCodeFile ->
                            refreshDownloadCodeDirectory(schemaCodeDownloadDetails)
                            openSchemaCoreCodeFileInEditor(schemaCoreCodeFile, project)
                        }
                        .thenApply {
                            showDownloadCompletionNotification(schemaName, project)
                        }
                        .exceptionally { error ->
                            showDownloadCompletionErrorNotification(error, project)
                        }
                        .toCompletableFuture().get()
                }
            }
        )

        onClose?.let { it() }

        close(OK_EXIT_CODE)
    }

    private fun refreshDownloadCodeDirectory(schemaCodeDownloadDetails: SchemaCodeDownloadRequestDetails) {
        LocalFileSystem.getInstance().findFileByIoFile(schemaCodeDownloadDetails.destinationDirectory)?.let {
            VfsUtil.markDirtyAndRefresh(false, true, true, it)
        }
    }

    private fun showDownloadCompletionNotification(
        schemaName: String,
        project: Project
    ) {
        val message = message("schemas.schema.download_code_bindings.notification.finished", schemaName)
        notifyInfo(title = NOTIFICATION_TITLE, content = message, project = project)
        SchemasTelemetry.download(project, success = true)
    }

    private fun showDownloadCompletionErrorNotification(
        error: Throwable?,
        project: Project
    ) {
        when (val rootError = ExceptionUtils.getRootCause(error)) {
            is SchemaCodeDownloadFileCollisionException -> notifyError(title = NOTIFICATION_TITLE, content = rootError.message ?: "", project = project)
            is Exception -> rootError.notifyError(title = NOTIFICATION_TITLE, project = project)
        }
        SchemasTelemetry.download(project, success = false)
    }

    private fun openSchemaCoreCodeFileInEditor(
        schemaCoreCodeFile: Path?,
        project: Project
    ): CompletionStage<Void> {
        val future = CompletableFuture<Void>()
        ApplicationManager.getApplication().invokeLater {
            try {
                schemaCoreCodeFile?.let {
                    val vSchemaCoreCodeFileName = LocalFileSystem.getInstance().findFileByNioFile(schemaCoreCodeFile)
                    vSchemaCoreCodeFileName?.let {
                        val fileEditorManager = FileEditorManager.getInstance(project)
                        fileEditorManager.openTextEditor(OpenFileDescriptor(project, it), true)
                    }
                }

                future.complete(null)
            } catch (e: Exception) {
                future.completeExceptionally(e)
            }
        }

        return future
    }

    private fun viewToSchemaCodeDownloadDetails(): SchemaCodeDownloadRequestDetails = SchemaCodeDownloadRequestDetails(
        schema = SchemaSummary(this.schemaName, this.registryName),
        version = getSelectedVersion(),
        language = view.language.selected()!!,
        destinationDirectory = File(view.location.text)
    )

    private fun getSelectedVersion(): String {
        val selected = view.version.selected()!!
        if (selected == LATEST_VERSION) {
            return latestVersion
        }
        return selected
    }

    private inner class DownloadCodeForSchemaOkAction : OkAction() {
        init {
            putValue(Action.NAME, message("schemas.schema.download_code_bindings.download"))
        }

        override fun doAction(e: ActionEvent) {
            super.doAction(e)
            if (doValidateAll().isNotEmpty()) return

            downloadSchemaCode(SchemaCodeDownloader.create(project))
        }
    }

    companion object {
        val LATEST_VERSION = message("schemas.schema.download_code_bindings.latest")
    }
}

class DownloadCodeForSchemaValidator {
    fun validate(view: DownloadCodeForSchemaPanel): ValidationInfo? {
        if (view.version.selectedIndex < 0) {
            return ValidationInfo(message("schemas.schema.download_code_bindings.validation.version_required"), view.version)
        }

        if (view.language.selectedIndex < 0) {
            return ValidationInfo(message("schemas.schema.download_code_bindings.validation.language_required"), view.language)
        }

        val locationText = view.location.text
        if (locationText.isEmpty()) {
            return ValidationInfo(message("schemas.schema.download_code_bindings.validation.fileLocation_required"), view.location)
        }

        val file = File(locationText)
        if (!file.exists() || !file.isDirectory) {
            return ValidationInfo(message("schemas.schema.download_code_bindings.validation.fileLocation_invalid"), view.location)
        }

        return null
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.code

import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.SortedComboBoxModel
import software.aws.toolkits.jetbrains.services.schemas.SchemaCodeLangs
import software.aws.toolkits.jetbrains.ui.installTextFieldProjectFileBrowseListener
import java.util.Comparator
import javax.swing.DefaultComboBoxModel
import javax.swing.JComboBox
import javax.swing.JLabel
import javax.swing.JPanel

class DownloadCodeForSchemaPanel(project: Project) {
    lateinit var content: JPanel
        private set
    lateinit var heading: JLabel
        private set
    lateinit var version: JComboBox<String>
        private set
    lateinit var language: JComboBox<SchemaCodeLangs>
        private set
    lateinit var location: TextFieldWithBrowseButton
        private set
    private lateinit var versionModel: DefaultComboBoxModel<String>
    private lateinit var languageModel: SortedComboBoxModel<SchemaCodeLangs>

    private fun createUIComponents() {
        versionModel = DefaultComboBoxModel()
        version = ComboBox(versionModel)
        languageModel = SortedComboBoxModel(compareBy(Comparator.naturalOrder()) { it.toString() })
        language = ComboBox(languageModel)
    }

    init {
        installTextFieldProjectFileBrowseListener(
            project,
            location,
            FileChooserDescriptorFactory.createSingleFolderDescriptor()
        )
    }

    fun setLanguages(languages: List<SchemaCodeLangs>) {
        languageModel.setAll(languages)
    }

    fun setVersions(versions: List<String>) {
        versionModel.removeAllElements()
        versionModel.addAll(versions)
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.code

import software.aws.toolkits.jetbrains.services.schemas.SchemaCodeLangs
import software.aws.toolkits.jetbrains.services.schemas.SchemaSummary
import java.io.File
import java.nio.ByteBuffer

data class SchemaCodeDownloadRequestDetails(
    val schema: SchemaSummary,
    val version: String,
    val language: SchemaCodeLangs,
    val destinationDirectory: File
) {
    // TODO: This is far from reliable, and won't work if the schema has special characters,
    //  and should either be generated using SchemaCodeGenUtils or provided from the server via metadata in DescribeCodeBindings
    fun schemaCoreCodeFileName(): String = "${schema.title()}.${language.extension}"
}

data class DownloadedSchemaCode(
    val zipContents: ByteBuffer
)


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

package software.aws.toolkits.jetbrains.services.schemas.code

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.util.io.Decompressor
import software.amazon.awssdk.services.schemas.SchemasClient
import software.amazon.awssdk.services.schemas.model.CodeGenerationStatus
import software.amazon.awssdk.services.schemas.model.ConflictException
import software.amazon.awssdk.services.schemas.model.DescribeCodeBindingRequest
import software.amazon.awssdk.services.schemas.model.GetCodeBindingSourceRequest
import software.amazon.awssdk.services.schemas.model.NotFoundException
import software.amazon.awssdk.services.schemas.model.PutCodeBindingRequest
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.resources.message
import java.io.File
import java.io.FileOutputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Collections
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.zip.ZipFile

class SchemaCodeDownloader(
    private val generator: CodeGenerator,
    private val poller: CodeGenerationStatusPoller,
    private val downloader: CodeDownloader,
    private val extractor: CodeExtractor
) {
    fun downloadCode(
        schemaDownloadRequest: SchemaCodeDownloadRequestDetails,
        indicator: ProgressIndicator
    ): CompletionStage<Path?> {
        val schemaName = schemaDownloadRequest.schema.name
        indicator.updateProgress(message("schemas.schema.download_code_bindings.notification.start", schemaName))

        return downloader.download(schemaDownloadRequest) // Try to get code directly
            .handle { downloadedSchemaCode, exception ->
                when (exception) {
                    null -> return@handle downloadedSchemaCode // Code is available, return
                    !is NotFoundException -> throw exception // Unexpected exception, throw
                }

                indicator.updateProgress(message("schemas.schema.download_code_bindings.notification.generating", schemaName))
                generator.generate(schemaDownloadRequest) // If the code generation status wasn't previously requested, trigger it now
                    .thenCompose { initialCodeGenerationStatus ->
                        poller.pollForCompletion(schemaDownloadRequest, initialCodeGenerationStatus) // Then, poll for completion
                    }
                    .thenCompose {
                        indicator.updateProgress(message("schemas.schema.download_code_bindings.notification.downloading", schemaName))
                        downloader.download(schemaDownloadRequest) // Download the code zip file
                    }
                    .toCompletableFuture().get()
            }
            .thenCompose { code ->
                indicator.updateProgress(message("schemas.schema.download_code_bindings.notification.extracting", schemaName))
                extractor.extractAndPlace(schemaDownloadRequest, code) // Extract and place in workspace
            }
    }

    private fun ProgressIndicator.updateProgress(newStatus: String) {
        text = newStatus
        isIndeterminate = true
    }

    companion object {
        fun create(project: Project): SchemaCodeDownloader {
            val connectionSettings = AwsConnectionManager.getInstance(project).connectionSettings()
                ?: throw IllegalStateException("Attempting to use SchemaCodeDownload without valid AWS connection")
            return create(connectionSettings)
        }

        fun create(connectionSettings: ConnectionSettings): SchemaCodeDownloader {
            val clientManager = AwsClientManager.getInstance()
            return SchemaCodeDownloader(
                CodeGenerator(clientManager.getClient(connectionSettings.credentials, connectionSettings.region)),
                CodeGenerationStatusPoller(clientManager.getClient(connectionSettings.credentials, connectionSettings.region)),
                CodeDownloader(clientManager.getClient(connectionSettings.credentials, connectionSettings.region)),
                CodeExtractor()
            )
        }
    }
}

class CodeGenerator(private val schemasClient: SchemasClient) {
    fun generate(schemaDownload: SchemaCodeDownloadRequestDetails): CompletionStage<CodeGenerationStatus> {
        val future = CompletableFuture<CodeGenerationStatus>()
        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                val request = PutCodeBindingRequest.builder()
                    .language(schemaDownload.language.apiValue)
                    .registryName(schemaDownload.schema.registryName)
                    .schemaName(schemaDownload.schema.name)
                    .schemaVersion(schemaDownload.version)
                    .build()

                val result = schemasClient.putCodeBinding(request)
                future.complete(result.status())
            } catch (e: ConflictException) {
                future.complete(CodeGenerationStatus.CREATE_IN_PROGRESS)
            } catch (e: Exception) {
                future.completeExceptionally(RuntimeException(message("schemas.schema.download_code_bindings.failed_to_generate"), e))
            }
        }
        return future
    }
}

class CodeGenerationStatusPoller(private val schemasClient: SchemasClient) {
    fun pollForCompletion(
        schemaDownload: SchemaCodeDownloadRequestDetails,
        initialCodeGenerationStatus: CodeGenerationStatus = CodeGenerationStatus.CREATE_IN_PROGRESS
    ): CompletionStage<String> {
        val future = CompletableFuture<String>()
        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                if (initialCodeGenerationStatus != CodeGenerationStatus.CREATE_COMPLETE) {
                    schemasClient.waiter().waitUntilCodeBindingExists {
                        it.registryName(schemaDownload.schema.registryName)
                        it.schemaName(schemaDownload.schema.name)
                        it.schemaVersion(schemaDownload.version)
                        it.language(schemaDownload.language.apiValue)
                    }
                }

                future.complete(schemaDownload.schema.name)
            } catch (e: Exception) {
                future.completeExceptionally(RuntimeException(message("schemas.schema.download_code_bindings.failed_to_poll"), e))
            }
        }
        return future
    }

    fun getCurrentStatus(
        schemaDownload: SchemaCodeDownloadRequestDetails
    ): CompletionStage<CodeGenerationStatus> {
        val future = CompletableFuture<CodeGenerationStatus>()
        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                val request = DescribeCodeBindingRequest.builder()
                    .language(schemaDownload.language.apiValue)
                    .registryName(schemaDownload.schema.registryName)
                    .schemaName(schemaDownload.schema.name)
                    .schemaVersion(schemaDownload.version)
                    .build()

                val result = schemasClient.describeCodeBinding(request)

                future.complete(
                    result.status()
                )
            } catch (e: Exception) {
                future.completeExceptionally(RuntimeException(message("schemas.schema.download_code_bindings.failed_to_poll"), e))
            }
        }
        return future
    }
}

class CodeDownloader(private val schemasClient: SchemasClient) {
    fun download(
        schemaDownload: SchemaCodeDownloadRequestDetails
    ): CompletionStage<DownloadedSchemaCode> {
        val future = CompletableFuture<DownloadedSchemaCode>()
        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                val request = GetCodeBindingSourceRequest.builder()
                    .language(schemaDownload.language.apiValue)
                    .registryName(schemaDownload.schema.registryName)
                    .schemaName(schemaDownload.schema.name)
                    .schemaVersion(schemaDownload.version)
                    .build()

                val result = schemasClient.getCodeBindingSource(request)
                val zipContents = result.body()

                future.complete(DownloadedSchemaCode(zipContents.asByteBuffer()))
            } catch (e: NotFoundException) {
                future.completeExceptionally(e)
            } catch (e: Exception) {
                future.completeExceptionally(RuntimeException(message("schemas.schema.download_code_bindings.failed_to_download"), e))
            }
        }
        return future
    }
}

class CodeExtractor {
    fun extractAndPlace(
        request: SchemaCodeDownloadRequestDetails,
        downloadedSchemaCode: DownloadedSchemaCode
    ): CompletionStage<Path?> {
        val zipContents = downloadedSchemaCode.zipContents
        val zipFileName = "${request.schema.registryName}.${request.schema.name}.${request.version}.${request.language.apiValue}.zip"

        val schemaCoreCodeFileName = request.schemaCoreCodeFileName()
        var schemaCoreCodeFile: Path? = null

        val future = CompletableFuture<Path?>()

        try {
            val codeZipDir = createTempDir()
            val codeZipFile = File(codeZipDir.path, zipFileName)

            FileOutputStream(codeZipFile).channel.use { fileChannel ->
                fileChannel.write(zipContents)
            }

            validateNoFileCollisions(codeZipFile, request.destinationDirectory)

            val decompressor = Decompressor.Zip(codeZipFile)
                .overwrite(false)
                .postProcessor { path ->
                    if (schemaCoreCodeFile == null && path.fileName.toString() == schemaCoreCodeFileName) {
                        schemaCoreCodeFile = path
                    }
                }
            decompressor.extract(request.destinationDirectory)

            future.complete(schemaCoreCodeFile)
        } catch (e: Exception) {
            future.completeExceptionally(RuntimeException(message("schemas.schema.download_code_bindings.failed_to_extract"), e))
        }
        return future
    }

    // Ensure that the downloaded code hierarchy has no collisions with the destination directory
    private fun validateNoFileCollisions(codeZipFile: File, destinationDirectory: File) {
        ZipFile(codeZipFile).use { zipFile ->
            val zipEntries = zipFile.entries()
            Collections.list(zipEntries).forEach { zipEntry ->
                // Ignore directories because those can/will be merged
                if (!zipEntry.isDirectory) {
                    val intendedDestinationPath = Paths.get(destinationDirectory.path, zipEntry.name)
                    val intendedDestinationFile = intendedDestinationPath.toFile()
                    if (intendedDestinationFile.exists() && !intendedDestinationFile.isDirectory) {
                        throw SchemaCodeDownloadFileCollisionException(intendedDestinationFile.name)
                    }
                }
            }
        }
    }
}

class SchemaCodeDownloadFileCollisionException(fileName: String) :
    RuntimeException(message("schemas.schema.download_code_bindings.failed_to_extract_collision", fileName))


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

package software.aws.toolkits.jetbrains.services.schemas.resources

import software.amazon.awssdk.services.schemas.SchemasClient
import software.amazon.awssdk.services.schemas.model.DescribeSchemaResponse
import software.amazon.awssdk.services.schemas.model.RegistrySummary
import software.amazon.awssdk.services.schemas.model.SchemaSummary
import software.amazon.awssdk.services.schemas.model.SchemaVersionSummary
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource
import software.aws.toolkits.jetbrains.services.lambda.wizard.SchemaSelectionItem
import java.time.Duration

object SchemasResources {
    const val AWS_EVENTS_REGISTRY = "aws.events"

    val LIST_REGISTRIES: Resource.Cached<List<RegistrySummary>> =
        ClientBackedCachedResource(SchemasClient::class, "schemas.list_registries") {
            listRegistriesPaginator { it.build() }
                .registries()
                .sortedBy { it.registryName() }
                .toList()
        }

    val LIST_REGISTRIES_AND_SCHEMAS: Resource.Cached<List<SchemaSelectionItem>> =
        ClientBackedCachedResource(SchemasClient::class, "schemas.list_registries_and_schemas") {
            listRegistriesPaginator { it.build() }
                .registries()
                .sortedBy { it.registryName() }
                .map { registrySummary ->
                    val registryName = registrySummary.registryName()
                    val schemas = listSchemasPaginator { it.registryName(registryName).build() }
                        .schemas()
                        .sortedBy { it.schemaName() }
                        .toList()

                    val schemaSelectionItems = ArrayList<SchemaSelectionItem>()
                    schemaSelectionItems.add(SchemaSelectionItem.RegistryItem(registryName))
                    schemas.forEach { schemaSelectionItems.add(SchemaSelectionItem.SchemaItem(it.schemaName(), registryName)) }

                    schemaSelectionItems
                }
                .flatMap { it.toList() }
        }

    fun listSchemas(registryName: String): Resource.Cached<List<SchemaSummary>> =
        ClientBackedCachedResource(SchemasClient::class, "schemas.list_schemas.$registryName") {
            listSchemasPaginator { it.registryName(registryName).build() }
                .schemas()
                .sortedBy { it.schemaName() }
                .toList()
        }

    fun getSchema(registryName: String, schemaName: String, version: String? = null): Resource.Cached<DescribeSchemaResponse> =
        ClientBackedCachedResource(SchemasClient::class, "schemas.get_schema.$registryName.$schemaName") {
            describeSchema {
                it.registryName(registryName)
                    .schemaName(schemaName)
                    .schemaVersion(version)
                    .build()
            }
        }

    fun getSchemaVersions(registryName: String, schemaName: String): Resource.Cached<List<SchemaVersionSummary>> =
        ClientBackedCachedResource(SchemasClient::class, "schemas.list_schema_versions.$registryName.$schemaName", Duration.ofSeconds(10)) {
            listSchemaVersionsPaginator {
                it.registryName(registryName)
                    .schemaName(schemaName)
                    .build()
            }
                .schemaVersions()
                .toList()
        }
}


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

package software.aws.toolkits.jetbrains.services.schemas.search

import com.intellij.openapi.project.Project
import com.intellij.ui.components.JBLabel
import software.aws.toolkits.jetbrains.services.schemas.SchemaViewer
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SchemasTelemetry
import javax.swing.JComponent

interface SchemaSearchDialog {
    fun initializeNew()
    fun initializeFromState(state: SchemaSearchDialogState)
}

class SchemaSearchSingleRegistryDialog(
    private val registryName: String,
    project: Project,
    private val searchExecutor: SchemaSearchExecutor = SchemaSearchExecutor(project),
    schemaViewer: SchemaViewer = SchemaViewer(project),
    onCancelCallback: (SchemaSearchDialogState) -> Unit
) : SchemasSearchDialogBase(
    project,
    schemaViewer,
    message("schemas.search.header.text.singleRegistry", registryName),
    onCancelCallback
) {

    override fun createResultRenderer(): (SchemaSearchResultWithRegistry) -> JComponent = {
        JBLabel(it.name)
    }

    override fun searchSchemas(
        searchText: String,
        incrementalResultsCallback: OnSearchResultReturned,
        registrySearchErrorCallback: OnSearchResultError
    ) {
        SchemasTelemetry.search(project, Result.Succeeded)
        searchExecutor.searchSchemasInRegistry(registryName, searchText, incrementalResultsCallback, registrySearchErrorCallback)
    }
}

class SchemaSearchAllRegistriesDialog(
    project: Project,
    private val searchExecutor: SchemaSearchExecutor = SchemaSearchExecutor(project),
    schemaViewer: SchemaViewer = SchemaViewer(project),
    onCancelCallback: (SchemaSearchDialogState) -> Unit
) : SchemasSearchDialogBase(
    project,
    schemaViewer,
    message("schemas.search.header.text.allRegistries"),
    onCancelCallback
) {

    override fun createResultRenderer(): (SchemaSearchResultWithRegistry) -> JComponent = {
        JBLabel("${it.registry}/${it.name}")
    }

    override fun searchSchemas(
        searchText: String,
        incrementalResultsCallback: OnSearchResultReturned,
        registrySearchErrorCallback: OnSearchResultError
    ) {
        SchemasTelemetry.search(project, Result.Succeeded)
        searchExecutor.searchSchemasAcrossAllRegistries(searchText, incrementalResultsCallback, registrySearchErrorCallback)
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.search

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.services.schemas.search.SchemaSearchDialogManager.DialogStateCacheKey.AllRegistriesDialogStateCacheKey
import software.aws.toolkits.jetbrains.services.schemas.search.SchemaSearchDialogManager.DialogStateCacheKey.SingleRegistryDialogStateCacheKey

class SchemaSearchDialogManager {
    private val searchDialogStateCache: MutableMap<SingleRegistryDialogStateCacheKey, SchemaSearchDialogState> = mutableMapOf()
    private val allRegistriesSearchDialogStateCache: MutableMap<AllRegistriesDialogStateCacheKey, SchemaSearchDialogState> = mutableMapOf()

    fun searchRegistryDialog(registry: String, project: Project): DialogWrapper {
        val credentialId = AwsConnectionManager.getInstance(project).activeCredentialProvider.id
        val region = AwsConnectionManager.getInstance(project).activeRegion.id

        val dialog = SchemaSearchSingleRegistryDialog(
            registry,
            project,
            onCancelCallback = { state -> cacheSingleRegistryDialogStateOnCancel(registry, credentialId, region, state) }
        )

        val key = SingleRegistryDialogStateCacheKey(registry, credentialId, region)
        val dialogState = searchDialogStateCache.getOrDefault(key, null)
        if (dialogState == null) {
            dialog.initializeNew()
        } else {
            dialog.initializeFromState(dialogState)
        }

        return dialog
    }

    fun searchAllRegistriesDialog(project: Project): DialogWrapper {
        val credentialId = AwsConnectionManager.getInstance(project).activeCredentialProvider.id
        val region = AwsConnectionManager.getInstance(project).activeRegion.id

        val dialog = SchemaSearchAllRegistriesDialog(
            project,
            onCancelCallback = { state -> cacheAllRegistriesDialogStateOnCancel(credentialId, region, state) }
        )

        val key = AllRegistriesDialogStateCacheKey(credentialId, region)
        val dialogState = allRegistriesSearchDialogStateCache.getOrDefault(key, null)
        if (dialogState == null) {
            dialog.initializeNew()
        } else {
            dialog.initializeFromState(dialogState)
        }

        return dialog
    }

    private fun cacheSingleRegistryDialogStateOnCancel(registry: String, credentialId: String, region: String, state: SchemaSearchDialogState) {
        searchDialogStateCache[SingleRegistryDialogStateCacheKey(registry, credentialId, region)] = state
    }

    private fun cacheAllRegistriesDialogStateOnCancel(credentialId: String, region: String, state: SchemaSearchDialogState) {
        allRegistriesSearchDialogStateCache[AllRegistriesDialogStateCacheKey(credentialId, region)] = state
    }

    private sealed class DialogStateCacheKey {
        data class SingleRegistryDialogStateCacheKey(private val registry: String, private val credentialId: String, private val region: String) :
            DialogStateCacheKey()
        data class AllRegistriesDialogStateCacheKey(private val credentialId: String, private val region: String) : DialogStateCacheKey()
    }

    companion object {
        val INSTANCE = SchemaSearchDialogManager()
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.search

data class SchemaSearchDialogState(
    val searchText: String,
    val searchResults: List<SchemaSearchResultWithRegistry>,
    val selectedResult: SchemaSearchResultWithRegistry?,
    val selectedVersion: String?
)


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

package software.aws.toolkits.jetbrains.services.schemas.search

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import org.slf4j.LoggerFactory
import software.amazon.awssdk.services.schemas.SchemasClient
import software.amazon.awssdk.services.schemas.model.SearchSchemasRequest
import software.amazon.awssdk.services.schemas.model.SearchSchemasResponse
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.getResource
import software.aws.toolkits.jetbrains.services.schemas.resources.SchemasResources

class SchemaSearchExecutor(
    private val project: Project,
    private val schemasClient: SchemasClient = project.awsClient()
) {
    fun searchSchemasInRegistry(
        registryName: String,
        searchText: String,
        incrementalResultsCallback: OnSearchResultReturned,
        registrySearchErrorCallback: OnSearchResultError
    ) {
        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                val results = doSingleSearch(registryName, searchText)
                incrementalResultsCallback(results.map { SchemaSearchResultWithRegistry(it.name, it.versions, registryName) })
            } catch (e: Exception) {
                LOG.warn(e) { "SchemaSearchExecutor exception searching schema registry" }
                registrySearchErrorCallback(SchemaSearchError(registryName, e.message ?: ""))
            }
        }
    }

    fun searchSchemasAcrossAllRegistries(
        searchText: String,
        incrementalResultsCallback: OnSearchResultReturned,
        registrySearchErrorCallback: OnSearchResultError
    ) {
        project.getResource(SchemasResources.LIST_REGISTRIES)
            .thenApply {
                it.forEach { registry ->
                    val registryName = registry.registryName()
                    searchSchemasInRegistry(registryName, searchText, incrementalResultsCallback, registrySearchErrorCallback)
                }
            }
    }

    private fun doSingleSearch(
        registryName: String,
        searchText: String
    ): List<SchemaSearchResultWithRegistry> {
        val searchRequest = SearchSchemasRequest.builder()
            .registryName(registryName)
            .keywords(searchText)
            .build()

        val resultsResponse: SearchSchemasResponse = schemasClient.searchSchemas(searchRequest)

        return resultsResponse.schemas().mapNotNull { searchSchemaSummary ->
            val sortedVersions = searchSchemaSummary.schemaVersions()
                .map { it.schemaVersion() }
                .sortedByDescending { it.toIntOrNull() }

            if (sortedVersions.isEmpty()) {
                null
            } else {
                SchemaSearchResultWithRegistry(
                    searchSchemaSummary.schemaName(),
                    sortedVersions,
                    searchSchemaSummary.registryName()
                )
            }
        }
    }

    companion object {
        private val LOG = LoggerFactory.getLogger(SchemaSearchExecutor::class.java)
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.search

typealias OnSearchResultReturned = (List<SchemaSearchResultWithRegistry>) -> Unit

typealias OnSearchResultError = (SchemaSearchError) -> Unit

data class SchemaSearchResultWithRegistry(
    val name: String,
    val versions: List<String>,
    val registry: String
)

data class SchemaSearchError(val registryName: String, val errorMessage: String)


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

package software.aws.toolkits.jetbrains.services.schemas.search

import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.JBSplitter
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBList
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.Alarm
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.jetbrains.core.credentials.activeCredentialProvider
import software.aws.toolkits.jetbrains.core.credentials.activeRegion
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.schemas.SchemaViewer
import software.aws.toolkits.jetbrains.services.schemas.code.DownloadCodeForSchemaDialog
import software.aws.toolkits.jetbrains.utils.ui.selected
import software.aws.toolkits.resources.message
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
import java.awt.event.ActionEvent
import java.util.concurrent.CompletionStage
import java.util.concurrent.locks.ReentrantLock
import java.util.stream.IntStream
import javax.swing.Action
import javax.swing.DefaultComboBoxModel
import javax.swing.DefaultListModel
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.ListSelectionModel
import javax.swing.border.EmptyBorder
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.text.DefaultHighlighter
import kotlin.concurrent.withLock
import kotlin.streams.toList

abstract class SchemasSearchDialogBase(
    protected val project: Project,
    private val schemaViewer: SchemaViewer,
    private val headerText: String,
    private val onCancelCallback: (SchemaSearchDialogState) -> Unit
) : SchemaSearchDialog, DialogWrapper(project) {
    val searchTextField = JTextField()
    private val searchTextAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, this.disposable)

    val resultsModel = DefaultListModel<SchemaSearchResultWithRegistry>()
    val resultsList = JBList(resultsModel)
    private val resultsLock = ReentrantLock()

    val versionsModel: DefaultComboBoxModel<String> = DefaultComboBoxModel()
    val versionsCombo = ComboBox(versionsModel)

    val previewText = JTextArea()

    private val previewScrollPane = JBScrollPane()

    private val openDownloadDialogAction = OpenCodeDownloadDialogAction()
    private val closeDialogAction = CloseSearchDialogAction()

    private val contentPanel = JPanel(BorderLayout(0, DEFAULT_PADDING))

    val currentSearchErrors = mutableListOf<SchemaSearchError>()

    init {
        title = message("schemas.search.title")

        resultsList.selectionMode = ListSelectionModel.SINGLE_SELECTION
        resultsList.setEmptyText(message("schemas.search.no_results"))

        versionsCombo.isEditable = false
        versionsCombo.isEnabled = false

        val editorColorsScheme = EditorColorsManager.getInstance().globalScheme
        previewText.font = Font(editorColorsScheme.editorFontName, Font.PLAIN, editorColorsScheme.editorFontSize)
        previewText.isEditable = false

        initLayout()

        super.init()

        getDownloadButton()?.isEnabled = false
    }

    private fun initListeners() {
        resultsList.addListSelectionListener {
            if (it.valueIsAdjusting) return@addListSelectionListener

            getDownloadButton()?.isEnabled = true

            val selectedSchema = selectedSchema()
            if (selectedSchema != null) {
                updateSchemaVersions(selectedSchema)
                previewSchema(selectedSchema)
            }
        }

        versionsCombo.addActionListener {
            val selectedSchema = selectedSchema()

            if (selectedSchema != null && selectedSchemaVersion() != null) {
                previewSchema(selectedSchema)
            }
        }

        searchTextField.document.addDocumentListener(
            object : DocumentListener {
                override fun changedUpdate(e: DocumentEvent?) = search()
                override fun insertUpdate(e: DocumentEvent?) = search()
                override fun removeUpdate(e: DocumentEvent?) = search()

                private fun search() {
                    if (searchTextAlarm.isDisposed) return

                    searchTextAlarm.cancelAllRequests()

                    searchTextAlarm.addRequest(
                        {
                            val searchText = searchTextField.text

                            if (searchText.isNullOrEmpty()) {
                                clearState()
                                return@addRequest
                            }

                            clearState()
                            resultsList.setEmptyText(message("schemas.search.searching"))
                            searchSchemas(searchText, { onSearchResultsReturned(it) }, { onErrorSearchingRegistry(it) })
                        },
                        SEARCH_DELAY_MS
                    )
                }

                private fun clearState() {
                    previewText.text = ""
                    resultsList.setEmptyText(message("schemas.search.no_results"))
                    getDownloadButton()?.isEnabled = false
                    resultsModel.removeAllElements()
                    versionsModel.removeAllElements()
                    currentSearchErrors.clear()
                }
            }
        )
    }

    private fun onSearchResultsReturned(searchResults: List<SchemaSearchResultWithRegistry>) {
        runInEdt(ModalityState.any()) {
            resultsLock.withLock {
                resultsList.setEmptyText(message("schemas.search.no_results"))
                searchResults.forEach { result -> resultsModel.addElement(result) }
                resultsList.revalidate()
            }
        }
    }

    private fun onErrorSearchingRegistry(searchError: SchemaSearchError) {
        runInEdt(ModalityState.any()) {
            resultsLock.withLock {
                resultsList.setEmptyText(message("schemas.search.no_results"))
                currentSearchErrors.add(searchError)
            }
        }
    }

    private fun updateSchemaVersions(selectedSchema: SchemaSearchResultWithRegistry, selectedSchemaVersion: String? = null) {
        versionsModel.removeAllElements()

        val newVersions = selectedSchema.versions
        newVersions.forEach { version -> versionsModel.addElement(version) }

        versionsModel.selectedItem = selectedSchemaVersion ?: versionsCombo.getItemAt(0)
        versionsCombo.isEnabled = newVersions.size > 1
    }

    override fun doValidate(): ValidationInfo? {
        if (currentSearchErrors.isEmpty()) {
            return null
        }

        val registriesInError = currentSearchErrors.map { message("schemas.search.error.registry", it.registryName, it.errorMessage) }
        val registriesInErrorString = registriesInError.joinToString(", ")

        return ValidationInfo(message("schemas.search.error", registriesInErrorString))
    }

    private fun previewSchema(selectedSchema: SchemaSearchResultWithRegistry) {
        val selectedSchemaVersion = selectedSchemaVersion()
        selectedSchemaVersion?.let {
            downloadSchemaContent(selectedSchema, it)
                .thenApply { schemaText ->
                    runInEdt(ModalityState.any()) {
                        previewScrollPane.verticalScrollBar.value = previewScrollPane.verticalScrollBar.minimum
                        previewScrollPane.horizontalScrollBar.value = previewScrollPane.horizontalScrollBar.minimum

                        previewText.text = schemaText
                        previewText.caretPosition = 0

                        val searchText = searchTextField.text
                        if (!searchText.isNullOrEmpty()) {
                            val resultsInSchema = findAllOccurrencesInString(schemaText, searchText)

                            if (resultsInSchema.isNotEmpty()) {
                                val highlighter = previewText.highlighter
                                val painter = DefaultHighlighter.DefaultHighlightPainter(HIGHLIGHT_COLOR)

                                resultsInSchema.forEach { startIndex ->
                                    val endIndex = startIndex + searchText.length
                                    highlighter.addHighlight(startIndex, endIndex, painter)
                                }
                                previewText.caretPosition = resultsInSchema[0]
                            }
                        }
                    }
                }
        }
    }

    private fun findAllOccurrencesInString(string: String, search: String): List<Int> {
        val occurrences = ArrayList<Int>()
        var index = string.indexOf(search)
        while (index >= 0) {
            occurrences.add(index)
            index = string.indexOf(search, index + 1)
        }
        return occurrences
    }

    override fun initializeNew() {
        initListeners()
        initValidation()
    }

    override fun initializeFromState(state: SchemaSearchDialogState) {
        searchTextField.text = state.searchText

        resultsLock.withLock {
            resultsModel.removeAllElements()
            state.searchResults.forEach { result -> resultsModel.addElement(result) }

            val selectedResult = state.selectedResult
            if (selectedResult != null) {
                resultsList.setSelectedValue(selectedResult, true)
                updateSchemaVersions(selectedResult, state.selectedVersion)
                previewSchema(selectedResult)

                getDownloadButton()?.isEnabled = true
            }

            initListeners()
            initValidation()
        }
    }

    private fun getCurrentState(): SchemaSearchDialogState {
        val searchResults = IntStream.range(0, resultsModel.size()).mapToObj(resultsModel::get).toList()
        return SchemaSearchDialogState(currentSearchText(), searchResults, selectedSchema(), selectedSchemaVersion())
    }

    private fun currentSearchText(): String = searchTextField.text

    private fun selectedSchema(): SchemaSearchResultWithRegistry? = resultsList.selectedValue

    fun selectedSchemaName() = selectedSchema()?.name

    fun selectedSchemaVersion(): String? = versionsCombo.selected()

    fun selectedSchemaRegistry() = selectedSchema()?.registry

    private fun initLayout() {
        val top = JPanel(BorderLayout(DEFAULT_PADDING, 0))
        top.add(JBLabel(headerText), BorderLayout.WEST)
        top.add(searchTextField, BorderLayout.CENTER)

        resultsList.installCellRenderer(createResultRenderer())
        versionsCombo.renderer = SimpleListCellRenderer.create("") { message("schemas.search.version.prefix", it) }

        val resultsScrollPane = JBScrollPane()
        resultsScrollPane.setViewportView(resultsList)
        resultsScrollPane.preferredSize = Dimension(350, 600)

        previewScrollPane.setViewportView(previewText)
        previewScrollPane.preferredSize = Dimension(450, 600)

        val right = JPanel(BorderLayout(0, DEFAULT_PADDING))
        right.add(versionsCombo, BorderLayout.NORTH)
        right.add(previewScrollPane, BorderLayout.CENTER)

        val previewResultsSplitter = JBSplitter()
        previewResultsSplitter.firstComponent = resultsScrollPane
        previewResultsSplitter.secondComponent = right
        previewResultsSplitter.splitterProportionKey = selectedSchemaRegistry()

        val center = JPanel(BorderLayout(DEFAULT_PADDING, 0))
        center.add(previewResultsSplitter, BorderLayout.CENTER)

        contentPanel.setSize(700, 800)
        contentPanel.add(top, BorderLayout.PAGE_START)
        contentPanel.add(center, BorderLayout.CENTER)
        contentPanel.border = EmptyBorder(DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING)
    }

    abstract fun createResultRenderer(): (SchemaSearchResultWithRegistry) -> JComponent

    private fun downloadSchemaContent(schema: SchemaSearchResultWithRegistry, version: String): CompletionStage<String> =
        schemaViewer.downloadPrettySchema(schema.name, schema.registry, version, ConnectionSettings(project.activeCredentialProvider(), project.activeRegion()))

    abstract fun searchSchemas(
        searchText: String,
        incrementalResultsCallback: OnSearchResultReturned,
        registrySearchErrorCallback: OnSearchResultError
    )

    override fun createCenterPanel(): JComponent? = contentPanel

    override fun getPreferredFocusedComponent(): JComponent? = searchTextField

    override fun getHelpId(): String? = HelpIds.SCHEMA_SEARCH_DIALOG.id

    override fun getOKAction(): Action = openDownloadDialogAction

    override fun doOKAction() {
        // Intercept OK action as it is actually triggers code download dialog independently
    }

    override fun getCancelAction(): Action = closeDialogAction

    override fun doCancelAction() {
        onCancelCallback(getCurrentState())
        super.doCancelAction()
    }

    fun getDownloadButton(): JButton? = getButton(openDownloadDialogAction)

    private inner class OpenCodeDownloadDialogAction : OkAction() {
        init {
            putValue(Action.NAME, message("schemas.schema.download_code_bindings.action"))
        }

        override fun doAction(e: ActionEvent) {
            val currentSchema = selectedSchemaName()
            val currentSchemaRegistry = selectedSchemaRegistry()

            if (currentSchema != null && currentSchemaRegistry != null) {
                DownloadCodeForSchemaDialog(
                    project,
                    currentSchema,
                    currentSchemaRegistry,
                    selectedSchemaVersion(),
                    onClose = { close(OK_EXIT_CODE) }
                ).show()
            }
        }
    }

    private inner class CloseSearchDialogAction : DialogWrapperAction(message("general.close_button")) {
        override fun doAction(e: ActionEvent) {
            doCancelAction()
        }
    }

    private companion object {
        private const val DEFAULT_PADDING = 10
        private const val SEARCH_DELAY_MS = 300L
        private val HIGHLIGHT_COLOR = Color.YELLOW
    }
}


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

package software.aws.toolkits.jetbrains.services.schemas.search

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.DumbAwareAction
import icons.AwsIcons
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.services.schemas.SchemaRegistryNode
import software.aws.toolkits.telemetry.SchemasTelemetry

class SearchSchemasAction : DumbAwareAction("Search Schemas", null, AwsIcons.Actions.SCHEMA_SEARCH) {
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(PlatformDataKeys.PROJECT)

        val dialog = SchemaSearchDialogManager.INSTANCE.searchAllRegistriesDialog(project)
        dialog.show()

        SchemasTelemetry.search(project, success = true)
    }
}

class SearchSchemasInRegistryAction :
    SingleResourceNodeAction<SchemaRegistryNode>("Search Schemas in Registry", null, AwsIcons.Actions.SCHEMA_SEARCH), DumbAware {
    override fun actionPerformed(selected: SchemaRegistryNode, e: AnActionEvent) {
        val project = e.getRequiredData(PlatformDataKeys.PROJECT)
        val registry = selected.value.registryName()

        val dialog = SchemaSearchDialogManager.INSTANCE.searchRegistryDialog(registry, project)
        dialog.show()
    }
}


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

package software.aws.toolkits.jetbrains.services.secretsmanager

import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource

object SecretsManagerResources {
    val secrets = ClientBackedCachedResource(SecretsManagerClient::class, "secretsmanager.secrets") {
        listSecretsPaginator().toList().flatMap { it.secretList() }
    }
}

fun String.arnToName() = this.substringAfterLast(':')


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

package software.aws.toolkits.jetbrains.services.sns.resources

import software.amazon.awssdk.services.sns.SnsClient
import software.amazon.awssdk.services.sns.model.Topic
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource

object SnsResources {
    val LIST_TOPICS: Resource.Cached<List<Topic>> = ClientBackedCachedResource(SnsClient::class, "sns.list_topics") {
        listTopicsPaginator().topics().toList()
    }
}

// SNS topic ARNs are in the format: arn:<partition>:sns:<region>:<accountId>:<name> and cannot contain ':'
fun Topic.getName(): String = topicArn().substringAfterLast(':')


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

package software.aws.toolkits.jetbrains.services.sqs

import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.iam.IamClient
import software.amazon.awssdk.services.lambda.LambdaClient
import software.amazon.awssdk.services.lambda.model.InvalidParameterValueException
import software.amazon.awssdk.services.lambda.model.LambdaException
import software.amazon.awssdk.services.lambda.model.ResourceConflictException
import software.amazon.awssdk.services.lambda.model.ResourceNotFoundException
import software.amazon.awssdk.services.lambda.model.ServiceException
import software.aws.toolkits.core.utils.WaiterTimeoutException
import software.aws.toolkits.core.utils.Waiters.waitUntil
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.SqsTelemetry
import java.time.Duration
import javax.swing.JComponent

class ConfigureLambdaDialog(
    private val project: Project,
    private val queue: Queue
) : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    private val lambdaClient: LambdaClient = project.awsClient()
    private val iamClient: IamClient = project.awsClient()
    val view = ConfigureLambdaPanel(project)

    init {
        title = message("sqs.configure.lambda")
        setOKButtonText(message("general.configure_button"))
        setOKButtonTooltip(message("sqs.configure.lambda.configure.tooltip"))

        init()
    }

    override fun createCenterPanel(): JComponent? = view.component

    override fun getPreferredFocusedComponent(): JComponent? = view.lambdaFunction

    override fun doValidate(): ValidationInfo? {
        if (functionSelected().isEmpty()) {
            return ValidationInfo(message("sqs.configure.lambda.validation.function"), view.lambdaFunction)
        }
        return null
    }

    override fun doCancelAction() {
        SqsTelemetry.configureLambdaTrigger(project, Result.Cancelled, queue.telemetryType())
        super.doCancelAction()
    }

    override fun doOKAction() {
        if (!isOKActionEnabled) {
            return
        }

        isOKActionEnabled = false
        setOKButtonText(message("sqs.configure.lambda.in_progress"))

        coroutineScope.launch {
            try {
                configureLambda(functionSelected())
                runInEdt(ModalityState.any()) {
                    close(OK_EXIT_CODE)
                }
                notifyInfo(message("sqs.service_name"), message("sqs.configure.lambda.success", functionSelected()), project)
                SqsTelemetry.configureLambdaTrigger(project, Result.Succeeded, queue.telemetryType())
            } catch (e: InvalidParameterValueException) { // Exception thrown for invalid permission
                // DO NOT change to withCoroutineUiContext, it breaks the panel with the wrong state
                runInEdt(ModalityState.any()) {
                    if (ConfirmIamPolicyDialog(project, iamClient, lambdaClient, functionSelected(), queue, view.component).showAndGet()) {
                        retryConfiguration(functionSelected())
                    } else {
                        setOKButtonText(message("general.configure_button"))
                        isOKActionEnabled = true
                    }
                }
            } catch (e: Exception) {
                LOG.warn(e) { message("sqs.configure.lambda.error", functionSelected()) }
                setErrorText(e.message)
                setOKButtonText(message("general.configure_button"))
                isOKActionEnabled = true
                SqsTelemetry.configureLambdaTrigger(project, Result.Failed, queue.telemetryType())
            }
        }
    }

    private fun functionSelected(): String = view.lambdaFunction.selected()?.functionName() ?: ""

    internal fun configureLambda(functionName: String) {
        lambdaClient.createEventSourceMapping {
            it.functionName(functionName)
            it.eventSourceArn(queue.arn)
        }
    }

    // It takes a few seconds for the role policy to update, so this function will attempt configuration for a duration of time until it succeeds.
    internal suspend fun waitUntilConfigured(functionName: String): String? {
        var identifier: String? = null
        try {
            waitUntil(
                succeedOn = {
                    it.eventSourceArn().isNotEmpty()
                },
                exceptionsToIgnore = setOf(InvalidParameterValueException::class),
                exceptionsToStopOn = setOf(LambdaException::class, ResourceConflictException::class, ResourceNotFoundException::class, ServiceException::class),
                maxDuration = Duration.ofSeconds(CONFIGURATION_WAIT_TIME),
                call = {
                    lambdaClient.createEventSourceMapping {
                        it.functionName(functionName)
                        it.eventSourceArn(queue.arn)
                    }.apply {
                        identifier = this.uuid()
                    }
                }
            )
        } catch (e: WaiterTimeoutException) {
            identifier = null
        }
        return identifier
    }

    private fun retryConfiguration(functionName: String) {
        coroutineScope.launch {
            val identifier = waitUntilConfigured(functionName)
            if (!identifier.isNullOrEmpty()) {
                runInEdt(ModalityState.any()) {
                    close(OK_EXIT_CODE)
                }
                notifyInfo(message("sqs.service_name"), message("sqs.configure.lambda.success", functionName), project)
                SqsTelemetry.configureLambdaTrigger(project, Result.Succeeded, queue.telemetryType())
            } else {
                setErrorText(message("sqs.configure.lambda.error", functionName))
                setOKButtonText(message("general.configure_button"))
                isOKActionEnabled = true
                SqsTelemetry.configureLambdaTrigger(project, Result.Failed, queue.telemetryType())
            }
        }
    }

    private companion object {
        val LOG = getLogger<ConfigureLambdaDialog>()
        const val CONFIGURATION_WAIT_TIME: Long = 30
    }
}


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

import com.intellij.icons.AllIcons
import com.intellij.ide.HelpTooltip
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleListCellRenderer
import software.amazon.awssdk.services.lambda.model.FunctionConfiguration
import software.aws.toolkits.jetbrains.services.lambda.resources.LambdaResources
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import software.aws.toolkits.resources.message
import javax.swing.JLabel
import javax.swing.JPanel

class ConfigureLambdaPanel(private val project: Project) {
    lateinit var component: JPanel
        private set
    lateinit var lambdaFunction: ResourceSelector<FunctionConfiguration>
        private set
    lateinit var functionContextHelp: JLabel
        private set

    init {
        functionContextHelp.icon = AllIcons.General.ContextHelp
        HelpTooltip().apply {
            setDescription(message("sqs.configure.lambda.tooltip"))
            installOn(functionContextHelp)
        }
    }

    private fun createUIComponents() {
        lambdaFunction = ResourceSelector.builder()
            .resource(LambdaResources.LIST_FUNCTIONS)
            .customRenderer(SimpleListCellRenderer.create("") { it.functionName() })
            .awsConnection(project)
            .build()
    }
}


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

package software.aws.toolkits.jetbrains.services.sqs

import com.intellij.json.JsonLanguage
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.iam.IamClient
import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.lambda.upload.createSqsPollerPolicy
import software.aws.toolkits.jetbrains.ui.ConfirmPolicyPanel
import software.aws.toolkits.jetbrains.utils.ui.formatAndSet
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.JComponent

class ConfirmIamPolicyDialog(
    project: Project,
    private val iamClient: IamClient,
    private val lambdaClient: LambdaClient,
    private val functionName: String,
    private val queue: Queue,
    parent: Component? = null
) : DialogWrapper(project, parent, false, IdeModalityType.PROJECT) {
    private val coroutineScope = projectCoroutineScope(project)
    private val rolePolicy: String by lazy { createSqsPollerPolicy(queue.arn) }
    private val policyName: String by lazy { "AWSLambdaSQSPollerExecutionRole-$functionName-${queue.queueName}-${queue.region.id}" }
    val view = ConfirmPolicyPanel(project, message("sqs.confirm.iam.warning.text"))

    init {
        title = message("sqs.confirm.iam")
        setOKButtonText(message("sqs.confirm.iam.create"))
        view.policyDocument.formatAndSet(rolePolicy, JsonLanguage.INSTANCE)
        init()
    }

    override fun createCenterPanel(): JComponent? = view.component

    override fun doOKAction() {
        if (!isOKActionEnabled) {
            return
        }

        setOKButtonText(message("general.create_in_progress"))
        isOKActionEnabled = false
        coroutineScope.launch {
            try {
                val policyArn = createPolicy()
                attachPolicy(policyArn)
                runInEdt(ModalityState.any()) {
                    close(OK_EXIT_CODE)
                }
            } catch (e: Exception) {
                LOG.warn(e) { message("sqs.confirm.iam.failed") }
                setErrorText(e.message)
                setOKButtonText(message("sqs.confirm.iam.create"))
                isOKActionEnabled = true
            }
        }
    }

    private fun createPolicy(): String {
        val policy = iamClient.createPolicy {
            it.policyName(policyName)
            it.policyDocument(rolePolicy)
        }.policy()
        return policy.arn()
    }

    private fun attachPolicy(policyArn: String) {
        // getFunctionConfiguration().role() returns the ARN of the role like this: arn:aws:iam::123456789012:role/service-role/ROLE-NAME.
        // We must use substringAfterLast to extract only the role name.
        val role = lambdaClient.getFunctionConfiguration { it.functionName(functionName) }.role().substringAfterLast('/')
        iamClient.attachRolePolicy {
            it.policyArn(policyArn)
            it.roleName(role)
        }
    }

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


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

package software.aws.toolkits.jetbrains.services.sqs

import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.json.JsonLanguage
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import kotlinx.coroutines.launch
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.QueueAttributeName
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.ui.ConfirmPolicyPanel
import software.aws.toolkits.jetbrains.utils.ui.formatAndSet
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.JComponent

class ConfirmQueuePolicyDialog(
    project: Project,
    private val sqsClient: SqsClient,
    private val queue: Queue,
    topicArn: String,
    private val existingPolicy: String?,
    parent: Component? = null
) : DialogWrapper(project, parent, false, IdeModalityType.PROJECT) {
    private val coroutineScope = projectCoroutineScope(project)
    private val policyStatement = createSqsSnsSubscribePolicyStatement(queue.arn, topicArn)

    val view = ConfirmPolicyPanel(project, message("sqs.confirm.iam.warning.sqs_queue_permissions"))

    init {
        title = message("sqs.confirm.iam.create")
        setOKButtonText(message("sqs.confirm.iam.create"))
        view.policyDocument.formatAndSet(policyStatement, JsonLanguage.INSTANCE)
        init()
    }

    override fun createCenterPanel(): JComponent? = view.component

    override fun doOKAction() {
        if (!isOKActionEnabled) {
            return
        }

        setOKButtonText(message("sqs.confirm.iam.in_progress"))
        isOKActionEnabled = false
        coroutineScope.launch {
            try {
                addPolicy()
                runInEdt(ModalityState.any()) {
                    close(OK_EXIT_CODE)
                }
            } catch (e: Exception) {
                LOG.warn(e) { message("sqs.confirm.iam.failed") }
                setErrorText(e.message)
                setOKButtonText(message("sqs.confirm.iam.create"))
                isOKActionEnabled = true
            }
        }
    }

    private fun addPolicy() {
        val document = mapper.readTree(existingPolicy ?: createSqsPolicy(queue.arn)) as ObjectNode
        val policyArray = document[sqsPolicyStatementArray] as? ArrayNode ?: document.putArray(sqsPolicyStatementArray)
        policyArray.add(mapper.readTree(policyStatement))
        sqsClient.setQueueAttributes {
            it.queueUrl(queue.queueUrl)
            it.attributes(
                mutableMapOf(
                    QueueAttributeName.POLICY to document.toPrettyString()
                )
            )
        }
    }

    private companion object {
        val mapper = jacksonObjectMapper()
        val LOG = getLogger<ConfirmQueuePolicyDialog>()
    }
}