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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SimpleListCellRenderer
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.map
import software.aws.toolkits.jetbrains.services.ecr.CreateEcrRepoDialog
import software.aws.toolkits.jetbrains.services.ecr.resources.EcrResources
import software.aws.toolkits.jetbrains.services.ecr.resources.Repository
import software.aws.toolkits.jetbrains.services.s3.CreateS3BucketDialog
import software.aws.toolkits.jetbrains.services.s3.resources.S3Resources.LIST_BUCKETS
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import java.awt.BorderLayout
import javax.swing.JButton
import javax.swing.JLabel
import javax.swing.JPanel
import kotlin.properties.Delegates.observable

class CodeStoragePanel(private val project: Project) : JPanel(BorderLayout()) {
    lateinit var content: JPanel
        private set
    lateinit var sourceBucket: ResourceSelector<String>
        private set
    lateinit var ecrRepo: ResourceSelector<Repository>
        private set

    private lateinit var s3Label: JLabel
    private lateinit var createBucketButton: JButton
    private lateinit var ecrLabel: JLabel
    private lateinit var createEcrRepoButton: JButton

    var packagingType: PackageType by observable(PackageType.ZIP) { _, _, _ -> updateComponents() }

    init {
        createBucketButton.addActionListener {
            val bucketDialog = CreateS3BucketDialog(
                project = project,
                s3Client = project.awsClient(),
                parent = content
            )

            if (bucketDialog.showAndGet()) {
                bucketDialog.bucketName().let {
                    sourceBucket.reload(forceFetch = true)
                    sourceBucket.selectedItem = it
                }
            }
        }

        createEcrRepoButton.addActionListener {
            val ecrDialog = CreateEcrRepoDialog(
                project = project,
                ecrClient = project.awsClient(),
                parent = content
            )

            if (ecrDialog.showAndGet()) {
                ecrRepo.reload(forceFetch = true)
                ecrRepo.selectedItem { it.repositoryName == ecrDialog.repoName }
            }
        }

        content.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.deployment_settings"), false)

        updateComponents()

        add(content, BorderLayout.CENTER)
    }

    private fun createUIComponents() {
        sourceBucket = ResourceSelector.builder().resource(LIST_BUCKETS.map { it.name() }).awsConnection(project).build()
        ecrRepo = ResourceSelector.builder()
            .resource(EcrResources.LIST_REPOS)
            .customRenderer(SimpleListCellRenderer.create("") { it.repositoryName })
            .awsConnection(project)
            .build()
    }

    private fun updateComponents() {
        val isZip = packagingType == PackageType.ZIP
        s3Label.isVisible = isZip
        sourceBucket.isVisible = isZip
        createBucketButton.isVisible = isZip

        ecrLabel.isVisible = !isZip
        ecrRepo.isVisible = !isZip
        createEcrRepoButton.isVisible = !isZip
    }

    fun codeLocation() = if (packagingType == PackageType.ZIP) {
        sourceBucket.selected() as String
    } else {
        ecrRepo.selected()?.repositoryUri as String
    }

    fun validatePanel(): ValidationInfo? {
        if (packagingType == PackageType.ZIP) {
            if (sourceBucket.isLoading) {
                return sourceBucket.validationInfo(message("serverless.application.deploy.validation.s3.bucket.loading"))
            }

            if (sourceBucket.selected() == null) {
                return sourceBucket.validationInfo(message("lambda.upload_validation.source_bucket"))
            }
        } else {
            if (ecrRepo.isLoading) {
                return ecrRepo.validationInfo(message("serverless.application.deploy.validation.ecr.repo.loading"))
            }

            if (ecrRepo.selected() == null) {
                return ecrRepo.validationInfo(message("lambda.upload_validation.repo"))
            }
        }

        return null
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.upload

import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.util.text.nullize
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.lambda.model.PackageType
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.core.lambda.validOrNull
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.lambda.Lambda.findPsiElementsForHandler
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.DEFAULT_MEMORY_SIZE
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.DEFAULT_TIMEOUT
import software.aws.toolkits.jetbrains.services.lambda.resources.LambdaResources
import software.aws.toolkits.jetbrains.services.lambda.runtimeGroup
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions
import software.aws.toolkits.jetbrains.services.lambda.steps.CreateLambda.Companion.FUNCTION_ARN
import software.aws.toolkits.jetbrains.services.lambda.steps.createLambdaWorkflowForImage
import software.aws.toolkits.jetbrains.services.lambda.steps.createLambdaWorkflowForZip
import software.aws.toolkits.jetbrains.settings.UpdateLambdaSettings
import software.aws.toolkits.jetbrains.utils.execution.steps.BuildViewWorkflowEmitter
import software.aws.toolkits.jetbrains.utils.execution.steps.StepExecutor
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.LambdaPackageType
import software.aws.toolkits.telemetry.LambdaTelemetry
import software.aws.toolkits.telemetry.Result
import java.nio.file.Paths
import javax.swing.JComponent

class CreateFunctionDialog(private val project: Project, private val initialRuntime: Runtime?, private val handlerName: String?) : DialogWrapper(project) {
    private val view = CreateFunctionPanel(project)

    init {
        super.init()
        title = message("lambda.upload.create.title")
        setOKButtonText(message("lambda.upload.create.title"))

        with(view.configSettings) {
            timeoutSlider.value = DEFAULT_TIMEOUT
            memorySlider.value = DEFAULT_MEMORY_SIZE

            // show a filtered list of runtimes to only ones we can build (since we have to build)
            runtimeModel.setAll(LambdaBuilder.supportedRuntimeGroups().flatMap { it.supportedSdkRuntimes })

            handlerName?.let { handler ->
                handlerPanel.handler.text = handler
            }
            initialRuntime?.validOrNull?.let {
                runtimeModel.selectedItem = it
                handlerPanel.setRuntime(it)
            }
        }
    }

    override fun createCenterPanel(): JComponent = view.content

    override fun getPreferredFocusedComponent(): JComponent = view.name

    override fun doValidate(): ValidationInfo? = view.validatePanel()

    override fun doCancelAction() {
        LambdaTelemetry.deploy(
            project,
            result = Result.Cancelled,
            lambdaPackageType = LambdaPackageType.from(view.configSettings.packageType().toString()),
            initialDeploy = true
        )
        super.doCancelAction()
    }

    override fun doOKAction() {
        upsertLambdaCode()
    }

    override fun getHelpId(): String = HelpIds.CREATE_FUNCTION_DIALOG.id

    private fun upsertLambdaCode() {
        if (!okAction.isEnabled) {
            return
        }

        val functionName = view.name.text
        val workflow = createWorkflow()
        val packageType = LambdaPackageType.from(view.configSettings.packageType().toString())

        workflow.onSuccess = {
            saveSettings(it.getRequiredAttribute(FUNCTION_ARN))

            notifyInfo(
                project = project,
                title = message("lambda.service_name"),
                content = message("lambda.function.created.notification", functionName)
            )
            LambdaTelemetry.deploy(
                project,
                result = Result.Succeeded,
                lambdaPackageType = packageType,
                initialDeploy = true
            )
            project.refreshAwsTree(LambdaResources.LIST_FUNCTIONS)
        }

        workflow.onError = {
            it.notifyError(project = project, title = message("lambda.service_name"))
            LambdaTelemetry.deploy(
                project,
                result = Result.Failed,
                lambdaPackageType = packageType,
                initialDeploy = true
            )
        }

        workflow.startExecution()

        close(OK_EXIT_CODE)
    }

    @TestOnly
    fun createWorkflow(): StepExecutor {
        FileDocumentManager.getInstance().saveAllDocuments()

        val samOptions = SamOptions(
            buildInContainer = view.buildSettings.buildInContainerCheckbox.isSelected
        )

        val workflow = when (val packageType = view.configSettings.packageType()) {
            PackageType.ZIP -> {
                val runtime = view.configSettings.runtime.selected() ?: throw IllegalStateException("Runtime is missing when package type is Zip")
                val handler = view.configSettings.handlerPanel.handler.text

                val functionDetails = viewToFunctionDetails(runtime, handler)

                // TODO: Move this so we can share it with CreateFunctionDialog, but don't move it lower since passing PsiElement lower needs to go away since
                // it is causing customer complaints. We need to prompt for baseDir and try to infer it if we can but only as a default value...
                val element = findPsiElementsForHandler(project, runtime, handler).first()
                val module = ModuleUtil.findModuleForPsiElement(element) ?: throw IllegalStateException("Failed to locate module for $element")
                val lambdaBuilder = runtime.runtimeGroup?.let { LambdaBuilder.getInstanceOrNull(it) }
                    ?: throw IllegalStateException("LambdaBuilder for $runtime not found")

                val codeDetails = ZipBasedCode(
                    baseDir = lambdaBuilder.handlerBaseDirectory(module, element),
                    handler = handler,
                    runtime = runtime
                )

                createLambdaWorkflowForZip(
                    project = project,
                    functionDetails = functionDetails,
                    codeDetails = codeDetails,
                    buildDir = lambdaBuilder.getBuildDirectory(module),
                    buildEnvVars = lambdaBuilder.additionalBuildEnvironmentVariables(project, module, samOptions),
                    codeStorageLocation = view.codeStorage.codeLocation(),
                    samOptions = samOptions
                )
            }
            PackageType.IMAGE -> {
                val functionDetails = viewToFunctionDetails()
                val codeDetails = ImageBasedCode(
                    dockerfile = Paths.get(view.configSettings.dockerFile.text)
                )

                createLambdaWorkflowForImage(
                    project = project,
                    functionDetails = functionDetails,
                    codeDetails = codeDetails,
                    codeStorageLocation = view.codeStorage.codeLocation(),
                    samOptions = samOptions
                )
            }
            else -> throw UnsupportedOperationException("$packageType is not supported")
        }

        val emitter = BuildViewWorkflowEmitter.createEmitter(
            project,
            message("lambda.workflow.create_new.name"),
            view.name.text
        )
        return StepExecutor(project, workflow, emitter)
    }

    private fun viewToFunctionDetails(runtime: Runtime? = null, handler: String? = null): FunctionDetails = FunctionDetails(
        name = view.name.text.trim(),
        description = view.description.text,
        packageType = view.configSettings.packageType(),
        runtime = runtime,
        handler = handler,
        iamRole = view.configSettings.iamRole.selected()!!,
        envVars = view.configSettings.envVars.envVars,
        timeout = view.configSettings.timeoutSlider.value,
        memorySize = view.configSettings.memorySlider.value,
        xrayEnabled = view.configSettings.xrayEnabled.isSelected
    )

    private fun saveSettings(arn: String) {
        val settings = UpdateLambdaSettings.getInstance(arn)
        settings.bucketName = view.codeStorage.sourceBucket.selected()
        settings.ecrRepo = view.codeStorage.ecrRepo.selected()?.repositoryArn
        settings.dockerfile = view.configSettings.dockerFile.text.nullize()
        settings.useContainer = view.buildSettings.buildInContainerCheckbox.isSelected
    }

    @TestOnly
    fun getViewForTestAssertions() = view
}


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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.IdeBorderFactory
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.FUNCTION_NAME_PATTERN
import software.aws.toolkits.jetbrains.services.lambda.LambdaLimits.MAX_FUNCTION_NAME_LENGTH
import software.aws.toolkits.jetbrains.utils.ui.blankAsNull
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import javax.swing.JPanel
import javax.swing.JTextField

class CreateFunctionPanel(private val project: Project) {
    lateinit var name: JTextField
        private set
    lateinit var description: JTextField
        private set
    lateinit var content: JPanel
        private set
    lateinit var buildSettings: BuildSettingsPanel
        private set
    lateinit var configSettings: LambdaConfigPanel
        private set
    lateinit var codeStorage: CodeStoragePanel
        private set

    init {
        configSettings.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.configuration_settings"), false)
        codeStorage.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.code_location_settings"), false)
        buildSettings.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.build_settings"), false)
    }

    private fun createUIComponents() {
        configSettings = LambdaConfigPanel(project, isUpdate = false)
        codeStorage = CodeStoragePanel(project)

        configSettings.packageZip.addChangeListener {
            val packageType = configSettings.packageType()
            codeStorage.packagingType = packageType
            buildSettings.packagingType = packageType
        }
    }

    fun validatePanel(): ValidationInfo? {
        val nameValue = name.blankAsNull()
            ?: return name.validationInfo(message("lambda.upload_validation.function_name"))

        if (!FUNCTION_NAME_PATTERN.matches(nameValue)) {
            return name.validationInfo(message("lambda.upload_validation.function_name_invalid"))
        }

        if (nameValue.length > MAX_FUNCTION_NAME_LENGTH) {
            return name.validationInfo(message("lambda.upload_validation.function_name_too_long", 64))
        }

        return configSettings.validatePanel() ?: codeStorage.validatePanel()
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.upload

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.actionSystem.UpdateInBackground
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPsiElementPointer
import icons.AwsIcons
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationTemplateIndex
import software.aws.toolkits.jetbrains.services.lambda.LambdaHandlerResolver
import software.aws.toolkits.jetbrains.services.lambda.runtime
import software.aws.toolkits.jetbrains.utils.notifyNoActiveCredentialsError
import software.aws.toolkits.resources.message

class CreateLambdaFunctionAction(
    private val handlerName: String?,
    private val elementPointer: SmartPsiElementPointer<PsiElement>?,
    private val lambdaHandlerResolver: LambdaHandlerResolver?
) : AnAction(message("lambda.create_new"), null, AwsIcons.Actions.LAMBDA_FUNCTION_NEW), UpdateInBackground {

    @Suppress("unused") // Used by ActionManager in plugin.xml
    constructor() : this(null, null, null)

    init {
        if (handlerName != null) {
            elementPointer ?: throw IllegalArgumentException("elementPointer must be provided if handlerName is provided")
            lambdaHandlerResolver
                ?: throw IllegalArgumentException("lambdaHandlerResolver must be provided if handlerName is provided")
        }
    }

    override fun actionPerformed(e: AnActionEvent) {
        val project = e.getRequiredData(LangDataKeys.PROJECT)
        val runtime = e.runtime()

        if (!AwsConnectionManager.getInstance(project).isValidConnectionSettings()) {
            notifyNoActiveCredentialsError(project = project)
            return
        }

        CreateFunctionDialog(project = project, initialRuntime = runtime?.toSdkRuntime(), handlerName = handlerName).show()
    }

    override fun update(e: AnActionEvent) {
        super.update(e)

        val element: PsiElement? = elementPointer?.element
        if (handlerName == null || element == null || lambdaHandlerResolver == null) {
            // It was created from ActionManager, so only show it if we have supported runtime groups
            e.presentation.isVisible = LambdaHandlerResolver.supportedRuntimeGroups().isNotEmpty()
            return
        }

        val templateFunctionHandlers = CloudFormationTemplateIndex.listFunctions(element.project)
            .mapNotNull { it.handler() }
            .toSet()

        val allowAction = lambdaHandlerResolver.determineHandlers(element, element.containingFile.virtualFile)
            .none { it in templateFunctionHandlers }

        e.presentation.isVisible = allowAction
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.upload

import software.amazon.awssdk.services.lambda.LambdaClient
import software.amazon.awssdk.services.lambda.model.PackageType
import software.amazon.awssdk.services.lambda.model.Runtime
import software.amazon.awssdk.services.lambda.model.TracingMode
import software.amazon.awssdk.services.lambda.model.UpdateFunctionConfigurationResponse
import software.aws.toolkits.jetbrains.services.iam.IamRole

data class FunctionDetails(
    val name: String,
    val description: String?,
    val packageType: PackageType,
    val handler: String?,
    val iamRole: IamRole,
    val runtime: Runtime?,
    val envVars: Map<String, String>,
    val timeout: Int,
    val memorySize: Int,
    val xrayEnabled: Boolean
) {
    val tracingMode: TracingMode =
        if (xrayEnabled) {
            TracingMode.ACTIVE
        } else {
            TracingMode.PASS_THROUGH
        }
}

fun LambdaClient.updateFunctionConfiguration(config: FunctionDetails): UpdateFunctionConfigurationResponse = this.updateFunctionConfiguration {
    it.functionName(config.name)
    it.description(config.description)
    if (config.packageType == PackageType.ZIP) {
        it.runtime(config.runtime)
        it.handler(config.handler)
    }
    it.role(config.iamRole.arn)
    it.timeout(config.timeout)
    it.memorySize(config.memorySize)
    it.environment { env ->
        env.variables(config.envVars)
    }
    it.tracingConfig { tracing ->
        tracing.mode(config.tracingMode)
    }
}


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

import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextBrowseFolderListener
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SortedComboBoxModel
import com.intellij.util.io.isFile
import software.amazon.awssdk.services.lambda.model.PackageType
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.services.iam.CreateIamRoleDialog
import software.aws.toolkits.jetbrains.services.iam.IamResources
import software.aws.toolkits.jetbrains.services.iam.IamRole
import software.aws.toolkits.jetbrains.services.lambda.LambdaWidgets.lambdaMemory
import software.aws.toolkits.jetbrains.services.lambda.LambdaWidgets.lambdaTimeout
import software.aws.toolkits.jetbrains.ui.HandlerPanel
import software.aws.toolkits.jetbrains.ui.KeyValueTextField
import software.aws.toolkits.jetbrains.ui.ResourceSelector
import software.aws.toolkits.jetbrains.ui.ResourceSelector.Companion.builder
import software.aws.toolkits.jetbrains.ui.SliderPanel
import software.aws.toolkits.jetbrains.utils.lambdaTracingConfigIsAvailable
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import java.awt.BorderLayout
import java.nio.file.Paths
import java.util.function.Function
import javax.swing.JButton
import javax.swing.JCheckBox
import javax.swing.JComboBox
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JRadioButton

class LambdaConfigPanel(private val project: Project, private val isUpdate: Boolean) : JPanel(BorderLayout()) {
    lateinit var handlerPanel: HandlerPanel
        private set
    lateinit var createRole: JButton
        private set
    lateinit var iamRole: ResourceSelector<IamRole>
        private set
    lateinit var runtime: JComboBox<Runtime>
        private set
    lateinit var runtimeLabel: JLabel
        private set
    lateinit var envVars: KeyValueTextField
        private set
    lateinit var memorySlider: SliderPanel
        private set
    lateinit var timeoutSlider: SliderPanel
        private set
    lateinit var handlerLabel: JLabel
        private set
    lateinit var xrayEnabled: JCheckBox
        private set
    lateinit var packageZip: JRadioButton
        private set
    lateinit var packageImage: JRadioButton
        private set
    lateinit var dockerFileLabel: JLabel
        private set
    lateinit var dockerFile: TextFieldWithBrowseButton
        private set
    private lateinit var content: JPanel

    var runtimeModel = SortedComboBoxModel(compareBy(Comparator.naturalOrder(), Runtime::toString))
        private set
    private var lastSelectedRuntime: Runtime? = null

    init {
        runtimeModel.setAll(Runtime.knownValues())

        runtime.model = runtimeModel
        runtime.addActionListener {
            val selectedRuntime = runtime.selectedItem as? Runtime?
            if (selectedRuntime == lastSelectedRuntime) {
                return@addActionListener
            }
            lastSelectedRuntime = selectedRuntime
            handlerPanel.setRuntime(selectedRuntime)
        }

        createRole.addActionListener {
            val iamRoleDialog = CreateIamRoleDialog(
                project = project,
                iamClient = project.awsClient(),
                parent = createRole,
                defaultAssumeRolePolicyDocument = DEFAULT_LAMBDA_ASSUME_ROLE_POLICY,
                defaultPolicyDocument = DEFAULT_POLICY
            )
            if (iamRoleDialog.showAndGet()) {
                iamRoleDialog.iamRole?.let { newRole ->
                    iamRole.reload(forceFetch = true)
                    iamRole.selectedItem { role -> role.arn == newRole.arn() }
                }
            }
        }

        content.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.configuration_settings"), false)

        packageZip.addChangeListener {
            updateVisibility()
        }
        updateVisibility()

        dockerFile.addBrowseFolderListener(TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor()))

        setXrayControlVisibility(lambdaTracingConfigIsAvailable(AwsConnectionManager.getInstance(project).activeRegion))

        add(content, BorderLayout.CENTER)
    }

    private fun createUIComponents() {
        handlerPanel = HandlerPanel(project)
        runtimeModel = SortedComboBoxModel(Comparator.comparing(Function { obj: Runtime -> obj.toString() }, Comparator.naturalOrder()))
        runtime = ComboBox(runtimeModel)
        envVars = KeyValueTextField()
        memorySlider = lambdaMemory()
        timeoutSlider = lambdaTimeout()
        iamRole = builder().resource(IamResources.LIST_LAMBDA_ROLES).awsConnection(project).build()
    }

    private fun updateVisibility() {
        val isZip = packageZip.isSelected

        handlerLabel.isVisible = isZip
        handlerPanel.isVisible = isZip
        handlerLabel.isVisible = isZip

        runtime.isVisible = isZip
        runtimeLabel.isVisible = isZip

        // If updating, do not allow to set the docker file, todo: should we allow setting an ECR URI?
        dockerFileLabel.isVisible = !isZip && !isUpdate
        dockerFile.isVisible = !isZip && !isUpdate
    }

    private fun setXrayControlVisibility(visible: Boolean) {
        xrayEnabled.isVisible = visible
        if (!visible) {
            xrayEnabled.isSelected = false
        }
    }

    fun validatePanel(): ValidationInfo? {
        when (packageType()) {
            PackageType.ZIP -> {
                // Check runtime first, since handler panel needs it to be there
                runtimeModel.selectedItem ?: return runtime.validationInfo(message("lambda.upload_validation.runtime"))

                // When we do a create, we must make sure the we can find the handler, update we dont have to since we dont do a build
                handlerPanel.validateHandler(handlerMustExist = !isUpdate)?.let { return it }
            }
            PackageType.IMAGE -> {
                if (dockerFile.isVisible && (dockerFile.text.isEmpty() || !Paths.get(dockerFile.text).isFile())) {
                    return dockerFile.validationInfo(message("lambda.upload_validation.dockerfile_not_found"))
                }
            }
            else -> {
                throw IllegalStateException("Unsupported package type ${packageType()}")
            }
        }

        iamRole.selected() ?: return iamRole.validationInfo(message("lambda.upload_validation.iam_role"))

        return timeoutSlider.validate() ?: memorySlider.validate()
    }

    fun packageType(): PackageType = when {
        packageZip.isSelected -> PackageType.ZIP
        else -> PackageType.IMAGE
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.upload

import com.intellij.codeInsight.daemon.LineMarkerInfo
import com.intellij.codeInsight.daemon.LineMarkerProviderDescriptor
import com.intellij.execution.lineMarker.ExecutorAction
import com.intellij.execution.lineMarker.LineMarkerActionWrapper
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.editor.markup.GutterIconRenderer
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPointerManager
import icons.AwsIcons
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationTemplateIndex.Companion.listFunctions
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
import software.aws.toolkits.jetbrains.services.lambda.LambdaHandlerResolver
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroup
import software.aws.toolkits.jetbrains.services.lambda.runtimeGroup
import software.aws.toolkits.jetbrains.settings.LambdaSettings
import software.aws.toolkits.jetbrains.utils.isTestOrInjectedText
import software.aws.toolkits.resources.message
import javax.swing.Icon

class LambdaLineMarker : LineMarkerProviderDescriptor() {

    override fun getName(): String = message("lambda.service_name")

    override fun getIcon(): Icon = AwsIcons.Resources.LAMBDA_FUNCTION

    override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
        // Only process leaf elements
        if (element.firstChild != null) {
            return null
        }

        val runtimeGroup = element.language.runtimeGroup ?: return null

        if (element.isTestOrInjectedText()) {
            return null
        }

        val handlerResolver = LambdaHandlerResolver.getInstanceOrNull(runtimeGroup) ?: return null
        val handler = handlerResolver.determineHandler(element) ?: return null

        return if (handlerResolver.shouldShowLineMarker(handler) || shouldShowLineMarker(element.containingFile, handler, runtimeGroup)) {
            val actionGroup = DefaultActionGroup()

            val smartPsiElementPointer = SmartPointerManager.createPointer(element)

            if (element.language in LambdaBuilder.supportedLanguages()) {
                val executorActions = ExecutorAction.getActions(1)
                executorActions.forEach {
                    actionGroup.add(LineMarkerActionWrapper(element, it))
                }

                actionGroup.add(CreateLambdaFunctionAction(handler, smartPsiElementPointer, handlerResolver))
            }

            object : LineMarkerInfo<PsiElement>(
                element,
                element.textRange,
                icon,
                null,
                null,
                GutterIconRenderer.Alignment.CENTER
            ) {
                override fun createGutterRenderer(): GutterIconRenderer = LambdaGutterIcon(this, actionGroup)
            }
        } else {
            null
        }
    }

    private fun shouldShowLineMarker(psiFile: PsiFile, handler: String, runtimeGroup: RuntimeGroup): Boolean {
        val project = psiFile.project
        return LambdaSettings.getInstance(project).showAllHandlerGutterIcons ||
            handlerInTemplate(project, handler, runtimeGroup)
    }

    // Handler defined in template with the same runtime group is valid
    private fun handlerInTemplate(project: Project, handler: String, runtimeGroup: RuntimeGroup): Boolean =
        listFunctions(project).any {
            Runtime.fromValue(it.runtime())?.runtimeGroup == runtimeGroup &&
                // if user has a custom makefile, assume they know what they're doing with the handler since we don't have enough information
                (it.handler() == handler || it.buildMethod() == "makefile")
        }

    class LambdaGutterIcon(markerInfo: LineMarkerInfo<PsiElement>, private val actionGroup: ActionGroup) :
        LineMarkerInfo.LineMarkerGutterIconRenderer<PsiElement>(markerInfo) {
        override fun getClickAction(): AnAction? = null

        override fun isNavigateAction(): Boolean = true

        override fun getPopupMenuActions(): ActionGroup = actionGroup
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.upload

import org.intellij.lang.annotations.Language

const val LAMBDA_PRINCIPAL = "lambda.amazonaws.com"

@Language("JSON")
val DEFAULT_LAMBDA_ASSUME_ROLE_POLICY =
    """
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Service": "lambda.amazonaws.com"
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }
    """.trim()

@Language("JSON")
val DEFAULT_POLICY =
    """
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          "Resource": "*"
        }
      ]
    }
    """.trim()

@Language("JSON")
fun createSqsPollerPolicy(arn: String): String =
    """
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "sqs:DeleteMessage",
            "sqs:GetQueueAttributes",
            "sqs:ReceiveMessage"
          ],
          "Resource": "$arn"
        }
      ]
    }
    """.trim()


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

package software.aws.toolkits.jetbrains.services.lambda.upload

import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.util.text.nullize
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.lambda.Lambda.findPsiElementsForHandler
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
import software.aws.toolkits.jetbrains.services.lambda.LambdaFunction
import software.aws.toolkits.jetbrains.services.lambda.runtimeGroup
import software.aws.toolkits.jetbrains.services.lambda.sam.SamOptions
import software.aws.toolkits.jetbrains.services.lambda.steps.updateLambdaCodeWorkflowForImage
import software.aws.toolkits.jetbrains.services.lambda.steps.updateLambdaCodeWorkflowForZip
import software.aws.toolkits.jetbrains.settings.UpdateLambdaSettings
import software.aws.toolkits.jetbrains.utils.execution.steps.BuildViewWorkflowEmitter
import software.aws.toolkits.jetbrains.utils.execution.steps.StepExecutor
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.LambdaPackageType
import software.aws.toolkits.telemetry.LambdaTelemetry
import software.aws.toolkits.telemetry.Result
import java.nio.file.Paths
import javax.swing.JComponent

class UpdateFunctionCodeDialog(private val project: Project, private val initialSettings: LambdaFunction) : DialogWrapper(project) {
    private val view = UpdateFunctionCodePanel(project, initialSettings.packageType)
    private val updateSettings = UpdateLambdaSettings.getInstance(initialSettings.arn)

    init {
        super.init()
        title = message("lambda.upload.updateCode.title", initialSettings.name)
        setOKButtonText(message("general.update_button"))

        initialSettings.handler?.let {
            view.handlerPanel.handler.text = it
        }
        view.handlerPanel.setRuntime(initialSettings.runtime)

        loadSettings()
    }

    override fun createCenterPanel(): JComponent = view.content

    override fun getPreferredFocusedComponent(): JComponent = view.handlerPanel.handler

    override fun doValidate(): ValidationInfo? = view.validatePanel()

    override fun doCancelAction() {
        LambdaTelemetry.deploy(
            project,
            result = Result.Cancelled,
            lambdaPackageType = LambdaPackageType.from(initialSettings.packageType.toString()),
            initialDeploy = false
        )
        super.doCancelAction()
    }

    override fun doOKAction() {
        saveSettings()
        upsertLambdaCode()
    }

    override fun getHelpId(): String = HelpIds.UPDATE_FUNCTION_CODE_DIALOG.id

    private fun upsertLambdaCode() {
        if (!okAction.isEnabled) {
            return
        }

        val workflow = createWorkflow()

        workflow.onSuccess = {
            notifyInfo(
                project = project,
                title = message("lambda.service_name"),
                content = message("lambda.function.code_updated.notification", initialSettings.name)
            )
            LambdaTelemetry.deploy(
                project,
                result = Result.Succeeded,
                lambdaPackageType = LambdaPackageType.from(initialSettings.packageType.toString()),
                initialDeploy = false
            )
        }

        workflow.onError = {
            it.notifyError(project = project, title = message("lambda.service_name"))
            LambdaTelemetry.deploy(
                project,
                result = Result.Failed,
                lambdaPackageType = LambdaPackageType.from(initialSettings.packageType.toString()),
                initialDeploy = false
            )
        }

        workflow.startExecution()

        close(OK_EXIT_CODE)
    }

    @TestOnly
    fun createWorkflow(): StepExecutor {
        FileDocumentManager.getInstance().saveAllDocuments()

        val samOptions = SamOptions(
            buildInContainer = view.buildSettings.buildInContainerCheckbox.isSelected
        )

        val workflow = when (val packageType = initialSettings.packageType) {
            PackageType.ZIP -> {
                val runtime = initialSettings.runtime ?: throw IllegalStateException("Runtime is missing when package type is Zip")
                val handler = view.handlerPanel.handler.text

                // TODO: Move this so we can share it with CreateFunctionDialog, but don't move it lower since passing PsiElement lower needs to go away since
                // it is causing customer complaints. We need to prompt for baseDir and try to infer it if we can but only as a default value...
                val element = findPsiElementsForHandler(project, runtime, handler).first()
                val module = ModuleUtil.findModuleForPsiElement(element) ?: throw IllegalStateException("Failed to locate module for $element")
                val lambdaBuilder = initialSettings.runtime.runtimeGroup?.let { LambdaBuilder.getInstanceOrNull(it) }
                    ?: throw IllegalStateException("LambdaBuilder for ${initialSettings.runtime} not found")

                val codeDetails = ZipBasedCode(
                    baseDir = lambdaBuilder.handlerBaseDirectory(module, element),
                    handler = handler,
                    runtime = runtime
                )

                updateLambdaCodeWorkflowForZip(
                    project = project,
                    functionName = initialSettings.name,
                    codeDetails = codeDetails,
                    buildDir = lambdaBuilder.getBuildDirectory(module),
                    buildEnvVars = lambdaBuilder.additionalBuildEnvironmentVariables(project, module, samOptions),
                    codeStorageLocation = view.codeStorage.codeLocation(),
                    samOptions = samOptions,
                    updatedHandler = handler.takeIf { it != initialSettings.handler }
                )
            }
            PackageType.IMAGE -> {
                val codeDetails = ImageBasedCode(
                    dockerfile = Paths.get(view.dockerFile.text)
                )

                updateLambdaCodeWorkflowForImage(
                    project = project,
                    functionName = initialSettings.name,
                    codeDetails = codeDetails,
                    codeStorageLocation = view.codeStorage.codeLocation(),
                    samOptions = samOptions
                )
            }
            else -> throw UnsupportedOperationException("$packageType is not supported")
        }

        val emitter = BuildViewWorkflowEmitter.createEmitter(project, message("lambda.workflow.update_code.name"), initialSettings.name)
        return StepExecutor(project, workflow, emitter)
    }

    private fun loadSettings() {
        view.codeStorage.sourceBucket.selectedItem = updateSettings.bucketName
        updateSettings.ecrRepo?.let { savedArn ->
            view.codeStorage.ecrRepo.selectedItem { it.repositoryArn == savedArn }
        }
        updateSettings.dockerfile?.let {
            view.dockerFile.text = it
        }
        view.buildSettings.buildInContainerCheckbox.isSelected = updateSettings.useContainer ?: false
    }

    private fun saveSettings() {
        updateSettings.bucketName = view.codeStorage.sourceBucket.selected()
        updateSettings.ecrRepo = view.codeStorage.ecrRepo.selected()?.repositoryArn
        updateSettings.dockerfile = view.dockerFile.text.nullize()
        updateSettings.useContainer = view.buildSettings.buildInContainerCheckbox.isSelected
    }

    @TestOnly
    fun getViewForTestAssertions() = view
}


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

import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.TextBrowseFolderListener
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.IdeBorderFactory
import com.intellij.util.io.isFile
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.jetbrains.ui.HandlerPanel
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import java.nio.file.Paths
import javax.swing.JLabel
import javax.swing.JPanel

class UpdateFunctionCodePanel internal constructor(private val project: Project, private val packageType: PackageType) {
    lateinit var content: JPanel
        private set
    lateinit var buildSettings: BuildSettingsPanel
        private set
    lateinit var codeStorage: CodeStoragePanel
        private set
    lateinit var handlerLabel: JLabel
        private set
    lateinit var handlerPanel: HandlerPanel
        private set
    lateinit var dockerFileLabel: JLabel
        private set
    lateinit var dockerFile: TextFieldWithBrowseButton
        private set
    private lateinit var lambdaConfigurationPanel: JPanel

    init {
        dockerFile.addBrowseFolderListener(TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor()))

        updateVisibility()

        lambdaConfigurationPanel.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.configuration_settings"), false)
    }

    private fun createUIComponents() {
        codeStorage = CodeStoragePanel(project)
        handlerPanel = HandlerPanel(project)
    }

    private fun updateVisibility() {
        val isZip = packageType == PackageType.ZIP

        handlerLabel.isVisible = isZip
        handlerPanel.isVisible = isZip

        dockerFileLabel.isVisible = !isZip
        dockerFile.isVisible = !isZip

        codeStorage.packagingType = packageType
        buildSettings.packagingType = packageType
    }

    fun validatePanel(): ValidationInfo? = when (packageType) {
        PackageType.ZIP -> {
            handlerPanel.validateHandler(handlerMustExist = true) ?: codeStorage.validatePanel()
        }
        PackageType.IMAGE -> {
            if (dockerFile.text.isEmpty() || !Paths.get(dockerFile.text).isFile()) {
                dockerFile.validationInfo(message("lambda.upload_validation.dockerfile_not_found"))
            } else {
                codeStorage.validatePanel()
            }
        }
        else -> {
            throw IllegalStateException("Unsupported package type $packageType")
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.upload

import com.intellij.openapi.application.ApplicationManager
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 com.intellij.util.ExceptionUtil
import software.amazon.awssdk.services.lambda.LambdaClient
import software.amazon.awssdk.services.lambda.model.PackageType
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.lambda.LambdaFunction
import software.aws.toolkits.jetbrains.services.lambda.waitForUpdatableState
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.LambdaPackageType
import software.aws.toolkits.telemetry.LambdaTelemetry
import software.aws.toolkits.telemetry.Result
import javax.swing.JComponent

class UpdateFunctionConfigDialog(private val project: Project, private val initialSettings: LambdaFunction) : DialogWrapper(project) {
    private val view = UpdateFunctionConfigPanel(project)

    init {
        super.init()
        title = message("lambda.upload.updateConfiguration.title", initialSettings.name)
        setOKButtonText(message("general.update_button"))

        view.name.text = initialSettings.name
        view.description.text = initialSettings.description

        with(view.configSettings) {
            if (initialSettings.packageType == PackageType.IMAGE) {
                packageImage.isSelected = true
            } else {
                packageZip.isSelected
                runtimeModel.selectedItem = initialSettings.runtime
                handlerPanel.setRuntime(initialSettings.runtime)
                initialSettings.handler?.let {
                    handlerPanel.handler.text = initialSettings.handler
                }
            }
            envVars.envVars = initialSettings.envVariables ?: emptyMap()
            timeoutSlider.value = initialSettings.timeout
            memorySlider.value = initialSettings.memorySize
            iamRole.selectedItem = initialSettings.role
            xrayEnabled.isSelected = initialSettings.xrayEnabled
        }
    }

    override fun createCenterPanel(): JComponent = view.content

    override fun getPreferredFocusedComponent(): JComponent = view.configSettings.handlerPanel.handler

    override fun doValidate(): ValidationInfo? = view.validatePanel()

    override fun doCancelAction() {
        LambdaTelemetry.editFunction(
            project,
            update = true,
            lambdaPackageType = LambdaPackageType.from(view.configSettings.packageType().toString()),
            result = Result.Cancelled
        )
        super.doCancelAction()
    }

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

        setOKButtonText(message("general.in_progress_button"))
        isOKActionEnabled = false

        val functionDetails = viewToFunctionDetails()
        val lambdaClient: LambdaClient = project.awsClient()

        ApplicationManager.getApplication().executeOnPooledThread {
            try {
                lambdaClient.waitForUpdatableState(functionDetails.name)
                lambdaClient.updateFunctionConfiguration(functionDetails)

                notifyInfo(
                    project = project,
                    title = message("lambda.service_name"),
                    content = message("lambda.function.configuration_updated.notification", functionDetails.name)
                )
                runInEdt(ModalityState.any()) { close(OK_EXIT_CODE) }
                LambdaTelemetry.editFunction(
                    project,
                    update = true,
                    lambdaPackageType = LambdaPackageType.from(functionDetails.packageType.toString()),
                    result = Result.Succeeded
                )
            } catch (e: Exception) {
                setErrorText(ExceptionUtil.getNonEmptyMessage(e, ExceptionUtil.getNonEmptyMessage(e, e::class.java.simpleName)))
                LambdaTelemetry.editFunction(
                    project,
                    update = true,
                    lambdaPackageType = LambdaPackageType.from(functionDetails.packageType.toString()),
                    result = Result.Failed
                )
                setOKButtonText(message("general.update_button"))
                isOKActionEnabled = true
            }
        }
    }

    private fun viewToFunctionDetails(): FunctionDetails = FunctionDetails(
        name = initialSettings.name,
        description = view.description.text,
        packageType = view.configSettings.packageType(),
        runtime = if (view.configSettings.packageType() == PackageType.ZIP) view.configSettings.runtime.selectedItem as Runtime else null,
        handler = view.configSettings.handlerPanel.handler.text,
        iamRole = view.configSettings.iamRole.selected()!!,
        envVars = view.configSettings.envVars.envVars,
        timeout = view.configSettings.timeoutSlider.value,
        memorySize = view.configSettings.memorySlider.value,
        xrayEnabled = view.configSettings.xrayEnabled.isSelected
    )

    override fun getHelpId(): String = HelpIds.UPDATE_FUNCTION_CONFIGURATION_DIALOG.id
}


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

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.IdeBorderFactory
import software.aws.toolkits.resources.message
import javax.swing.JPanel
import javax.swing.JTextField

class UpdateFunctionConfigPanel(private val project: Project) {
    lateinit var content: JPanel
        private set
    lateinit var name: JTextField
        private set
    lateinit var description: JTextField
        private set
    lateinit var configSettings: LambdaConfigPanel
        private set

    init {
        configSettings.border = IdeBorderFactory.createTitledBorder(message("lambda.upload.configuration_settings"), false)
    }

    private fun createUIComponents() {
        configSettings = LambdaConfigPanel(project, isUpdate = true)
    }

    fun validatePanel(): ValidationInfo? = configSettings.validatePanel()
}


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

package software.aws.toolkits.jetbrains.services.lambda.upload

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

// TODO we can add `Filter`s to the step executor which will allow us to take
// actions on certain text, so we can put it in the output window to click to continue
class UploadFunctionContinueDialog(private val project: Project, private val changeSet: String) : DialogWrapper(project) {
    init {
        super.init()
        title = message("serverless.application.deploy.change_set.title")
        setOKButtonText(message("serverless.application.deploy.execute_change_set"))
        setCancelButtonText(message("general.close_button"))
    }

    override fun createCenterPanel(): JComponent = panel {
        row {
            JBLabel(message("serverless.application.deploy.change_set"))()
            JBTextField(changeSet).apply { this.isEditable = false }()
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.validation

import com.intellij.util.messages.Topic
import java.util.EventListener

/**
 * This class represents a topic for Lambda Handler validation events.
 */
interface LambdaHandlerEvaluationListener : EventListener {
    companion object {
        val TOPIC = Topic("Lambda handler evaluation listener", LambdaHandlerEvaluationListener::class.java)
    }

    /**
     * Lambda handler evaluation finished for the chosen profile, and it may be validated synchronously now.
     */
    fun handlerValidationFinished(handlerName: String, isHandlerExists: Boolean) {}
}


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

package software.aws.toolkits.jetbrains.services.lambda.validation

import com.intellij.openapi.project.Project
import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.jetbrains.services.lambda.Lambda
import software.aws.toolkits.jetbrains.utils.CachingAsyncEvaluator

class LambdaHandlerValidator : CachingAsyncEvaluator<LambdaHandlerValidator.LambdaEntry, Boolean>() {

    override fun getValue(entry: LambdaEntry): Boolean =
        Lambda.isHandlerValid(entry.project, entry.runtime, entry.handler)

    data class LambdaEntry(val project: Project, val runtime: Runtime, val handler: String)
}


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

package software.aws.toolkits.jetbrains.services.lambda.validation

import com.intellij.util.messages.Topic
import com.intellij.util.text.SemVer
import java.util.EventListener

/**
 * This class represents a topic for SAM CLI version validation events.
 */
interface SamCliVersionEvaluationListener : EventListener {
    companion object {
        val TOPIC = Topic("SAM CLI version evaluation listener", SamCliVersionEvaluationListener::class.java)
    }

    /**
     * SAM CLI version evaluation finished for the chosen profile, and it may be validated synchronously now.
     */
    fun samVersionValidationFinished(path: String, version: SemVer) {}
}


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

package software.aws.toolkits.jetbrains.services.lambda.wizard

import com.intellij.ide.util.projectWizard.EmptyModuleBuilder
import com.intellij.ide.util.projectWizard.SdkSettingsStep
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ValidationInfo
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroup
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import javax.swing.JComponent
import javax.swing.JLabel

class IntelliJSdkSelectionPanel(private val runtimeGroupId: String) : SdkSelector {
    private val dummyContext = object : WizardContext(null, {}) {}
    private val currentSdkPanel: SdkSettingsStep = buildSdkSettingsPanel()

    private var currentSdk: Sdk? = null

    override fun sdkSelectionPanel(): JComponent = currentSdkPanel.component

    override fun sdkSelectionLabel(): JLabel? = JLabel(message("sam.init.sdk.label"))

    override fun validateSelection(): ValidationInfo? {
        if (!currentSdkPanel.validate()) {
            return currentSdkPanel.component.validationInfo(message("sam.init.sdk.error"))
        }
        return null
    }

    override fun getSdk(): Sdk? = currentSdk

    // don't validate on init of the SettingsStep or weird things will happen if the user has no SDK
    private fun buildSdkSettingsPanel(): SdkSettingsStep =
        object : SdkSettingsStep(
            dummyContext,
            EmptyModuleBuilder(), // not used
            { it == RuntimeGroup.getById(runtimeGroupId).getIdeSdkType() },
            null
        ) {
            override fun onSdkSelected(sdk: Sdk?) {
                currentSdk = sdk
            }
        }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
@file:JvmName("SamInitProjectBuilderCommon")

package software.aws.toolkits.jetbrains.services.lambda.wizard

import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.DefaultProjectFactory
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.getExecutable
import software.aws.toolkits.jetbrains.core.executables.getExecutableIfPresent
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.settings.AwsSettingsConfigurable
import software.aws.toolkits.resources.message
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JTextField

@JvmOverloads
fun setupSamSelectionElements(samExecutableField: JTextField, editButton: JButton, label: JComponent, postEditCallback: Runnable? = null) {
    fun getSamExecutable(): ExecutableInstance.ExecutableWithPath? =
        ExecutableManager.getInstance().getExecutableIfPresent<SamExecutable>().let { it as? ExecutableInstance.ExecutableWithPath }

    fun updateUi(validSamPath: Boolean) {
        runInEdt(ModalityState.any()) {
            samExecutableField.isVisible = !validSamPath
            editButton.isVisible = !validSamPath
            label.isVisible = !validSamPath
        }
    }

    samExecutableField.text = getSamExecutable()?.executablePath?.toString()

    editButton.addActionListener {
        ShowSettingsUtil.getInstance().showSettingsDialog(DefaultProjectFactory.getInstance().defaultProject, AwsSettingsConfigurable::class.java)
        samExecutableField.text = getSamExecutable()?.executablePath?.toString()
        postEditCallback?.run()
    }

    val toolTipText = message("aws.settings.find.description", "SAM")
    label.toolTipText = toolTipText
    samExecutableField.toolTipText = toolTipText
    editButton.toolTipText = toolTipText

    ProgressManager.getInstance().runProcessWithProgressSynchronously(
        {
            try {
                val validSamPath = when (ExecutableManager.getInstance().getExecutable<SamExecutable>().toCompletableFuture().get()) {
                    is ExecutableInstance.Executable -> true
                    else -> false
                }
                updateUi(validSamPath)
            } catch (e: Throwable) {
                updateUi(validSamPath = false)
            }
        },
        message("lambda.run_configuration.sam.validating"),
        false,
        null
    )
}


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

package software.aws.toolkits.jetbrains.services.lambda.wizard

import com.fasterxml.jackson.module.kotlin.convertValue
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.getExecutable
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.services.lambda.sam.samInitCommand
import software.aws.toolkits.jetbrains.services.schemas.SchemaTemplateParameters
import software.aws.toolkits.resources.message

object SamInitRunner {
    private val LOG = getLogger<SamInitRunner>()

    fun execute(
        outputDir: VirtualFile,
        templateParameters: TemplateParameters,
        schemaParameters: SchemaTemplateParameters?
    ) {
        // set output to a temp dir
        val tempDir = createTempDir()

        ExecutableManager.getInstance().getExecutable<SamExecutable>().thenApply {
            val samExecutable = when (it) {
                is ExecutableInstance.Executable -> it
                else -> throw RuntimeException((it as? ExecutableInstance.BadExecutable)?.validationError)
            }

            val extraContent = if (schemaParameters?.templateExtraContext != null) {
                jacksonObjectMapper().convertValue<Map<String, String>>(schemaParameters.templateExtraContext)
            } else {
                emptyMap()
            }

            val commandLine = samExecutable.getCommandLine().samInitCommand(
                tempDir.toPath(),
                templateParameters,
                extraContent
            )

            LOG.info { "Running SAM command ${commandLine.commandLineString}" }

            val process = CapturingProcessHandler(commandLine).runProcess()
            if (process.exitCode != 0) {
                throw RuntimeException("${message("sam.init.execution_error")}: ${process.stderrLines}")
            } else {
                LOG.info { "SAM init output stdout:\n${process.stdout}" }
                LOG.info { "SAM init output stderr:\n${process.stderr}" }
            }

            val subFolders = tempDir.listFiles()?.toList() ?: emptyList()

            assert(subFolders.size == 1 && subFolders.first().isDirectory) {
                message("sam.init.error.subfolder_not_one", tempDir.name)
            }

            FileUtil.copyDirContent(subFolders.first(), VfsUtil.virtualToIoFile(outputDir))
            FileUtil.delete(tempDir)
        }.toCompletableFuture().join()
    }
}


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

import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.Wrapper
import com.intellij.ui.layout.panel
import com.intellij.util.text.SemVer
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.core.lambda.LambdaArchitecture
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.core.executables.ExecutableInstance.BadExecutable
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.ExecutableType.Companion.getExecutable
import software.aws.toolkits.jetbrains.services.lambda.minSamInitVersion
import software.aws.toolkits.jetbrains.services.lambda.minSamVersion
import software.aws.toolkits.jetbrains.services.lambda.runtimeGroup
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.utils.ui.validationInfo
import software.aws.toolkits.resources.message
import java.awt.BorderLayout
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JRadioButton
import javax.swing.JTextField

class SamInitSelectionPanel(
    wizardFragmentList: List<WizardFragment>,
    private val projectLocation: TextFieldWithBrowseButton? = null, /* Only available in PyCharm! */
    private val runtimeFilter: (LambdaRuntime) -> Boolean = { true },
    private val wizardUpdateCallback: () -> Unit = {} /* Used in Rider to refresh the validation */
) {
    lateinit var mainPanel: JPanel

    private lateinit var runtimeComboBox: ComboBox<LambdaRuntime>
    private lateinit var architectureComboBox: ComboBox<LambdaArchitecture>
    private lateinit var samExecutableField: JTextField
    private lateinit var editSamExecutableButton: JButton
    private lateinit var samLabel: JBLabel
    private lateinit var packageZip: JRadioButton
    private lateinit var templateComboBox: ComboBox<SamProjectTemplate>
    private lateinit var fragments: Wrapper

    private val wizardFragments: Map<WizardFragment, JComponent>
    private val runtimes = CollectionComboBoxModel(supportedRuntimes())
    private val architectures = CollectionComboBoxModel(supportedArchitectures())

    init {
        setupSamSelectionElements(samExecutableField, editSamExecutableButton, samLabel)

        runtimeComboBox.model = runtimes
        runtimeComboBox.addActionListener {
            runtimeUpdate()
            architectureUpdate()
            wizardUpdate()
        }

        architectureComboBox.model = architectures

        templateComboBox.addActionListener { wizardUpdate() }
        templateComboBox.renderer = SimpleListCellRenderer.create { label, value, _ ->
            label.text = value?.displayName()
            label.toolTipText = value?.description()
        }

        packageZip.addChangeListener {
            runtimeUpdate()
        }

        wizardFragments = wizardFragmentList.associateWith {
            val panel = JPanel(BorderLayout())
            val fragmentTitle = it.title()
            if (fragmentTitle != null) {
                panel.border = IdeBorderFactory.createTitledBorder(it.title(), false)
            }
            panel.add(it.component(), BorderLayout.CENTER)
            panel
        }

        fragments.setContent(
            panel {
                wizardFragments.values.forEach {
                    row {
                        it(grow)
                    }
                }
            }
        )

        // this will also fire wizardUpdate since templateComboBox will change
        // otherwise we make 2 of them
        runtimeUpdate()
        architectureUpdate()
    }

    // Source all templates, find all the runtimes they support, then filter those by what the IDE supports
    private fun supportedRuntimes(): MutableList<LambdaRuntime> = SamProjectTemplate.supportedTemplates().asSequence()
        .flatMap {
            when (packageType()) {
                PackageType.ZIP -> it.supportedZipRuntimes().asSequence()
                else -> it.supportedImageRuntimes().asSequence()
            }
        }
        .filter(runtimeFilter)
        .distinct()
        .sorted()
        .toMutableList()

    private fun supportedArchitectures(): MutableList<LambdaArchitecture> = runtimes.selected?.architectures?.toMutableList()
        ?: mutableListOf(LambdaArchitecture.X86_64)

    private fun packageType() = when {
        packageZip.isSelected -> PackageType.ZIP
        else -> PackageType.IMAGE
    }

    fun setRuntime(runtime: LambdaRuntime) {
        runtimeComboBox.selectedItem = runtime
    }

    private fun runtimeUpdate() {
        // Refresh the runtimes list since zip and image differ
        runtimes.removeAll()
        runtimes.add(supportedRuntimes())

        val selectedTemplate = templateComboBox.selectedItem as? SamProjectTemplate
        templateComboBox.removeAllItems()
        val selectedRuntime = runtimes.selected ?: return

        val packagingType = packageType()
        SamProjectTemplate.supportedTemplates().asSequence()
            .filter {
                when (packagingType) {
                    PackageType.ZIP -> it.supportedZipRuntimes().contains(selectedRuntime)
                    else -> it.supportedImageRuntimes().contains(selectedRuntime)
                }
            }
            .forEach { templateComboBox.addItem(it) }

        // repopulate template after runtime updates
        // this should no-op if the previous selection is not applicable to the current runtime
        if (selectedTemplate != null) {
            templateComboBox.selectedItem = selectedTemplate
        }
    }

    private fun architectureUpdate() {
        val selectedArchitecture = architectureComboBox.selectedItem as? LambdaArchitecture

        architectures.removeAll()
        architectures.add(supportedArchitectures())
        architectureComboBox.isEnabled = architectures.size > 1

        if (selectedArchitecture != null) {
            templateComboBox.selectedItem = selectedArchitecture
        }
    }

    /**
     * Updates UI fragments in the wizard after a combobox update
     */
    private fun wizardUpdate() {
        val selectedRuntime = runtimes.selected
        val selectedTemplate = templateComboBox.selectedItem as? SamProjectTemplate
        wizardFragments.forEach { (wizardFragment, jComponent) ->
            val isApplicable = wizardFragment.isApplicable(selectedTemplate)
            if (isApplicable) {
                wizardFragment.updateUi(projectLocation, selectedRuntime?.runtimeGroup, selectedTemplate)
            }
            jComponent.isVisible = isApplicable
        }
        wizardUpdateCallback()
    }

    fun validate(): ValidationInfo? {
        val samExecutable = ExecutableManager.getInstance().getExecutableIfPresent(getExecutable(SamExecutable::class.java))
        if (samExecutable is BadExecutable) {
            return ValidationInfo(samExecutable.validationError, samExecutableField)
        }

        val samVersion = SemVer.parseFromText(samExecutable.version)
            ?: throw IllegalStateException("SemVer is invalid even with valid SAM executable")

        if (packageType() == PackageType.IMAGE && samVersion < SamCommon.minImageVersion) {
            return ValidationInfo(message("lambda.image.sam_version_too_low", samVersion, SamCommon.minImageVersion))
        }

        val selectedRuntime = runtimes.selected ?: return templateComboBox.validationInfo(message("sam.init.error.no.runtime.selected"))

        val minRuntimeSamVersion = selectedRuntime.minSamInitVersion()
        if (samVersion < minRuntimeSamVersion) {
            return ValidationInfo(message("sam.executable.minimum_too_low_runtime", selectedRuntime, minRuntimeSamVersion), runtimeComboBox)
        }

        val selectedArchitecture = architectures.selected ?: return templateComboBox.validationInfo(message("sam.init.error.no.architecture.selected"))
        val minArchitectureSamVersion = selectedArchitecture.minSamVersion()
        if (samVersion < minArchitectureSamVersion) {
            return ValidationInfo(message("sam.executable.minimum_too_low_architecture", selectedArchitecture, minArchitectureSamVersion), runtimeComboBox)
        }

        val samProjectTemplate = templateComboBox.selectedItem as? SamProjectTemplate
            ?: return templateComboBox.validationInfo(message("sam.init.error.no.template.selected"))

        return wizardFragments.keys
            .filter { it.isApplicable(samProjectTemplate) }
            .mapNotNull { it.validateFragment() }
            .firstOrNull()
    }

    fun getNewProjectSettings(): SamNewProjectSettings {
        val lambdaRuntime = runtimes.selected
            ?: throw RuntimeException("No Runtime is supported in this Platform.")
        val lambdaArchitecture = architectures.selected
            ?: throw RuntimeException("No architecture is supported for this runtime: $lambdaRuntime")
        val samProjectTemplate = templateComboBox.selectedItem as? SamProjectTemplate
            ?: throw RuntimeException("No SAM template is supported for this runtime: $lambdaRuntime")

        return SamNewProjectSettings(template = samProjectTemplate, runtime = lambdaRuntime, architecture = lambdaArchitecture, packagingType = packageType())
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.wizard

import com.intellij.ide.util.projectWizard.AbstractNewProjectStep
import com.intellij.ide.util.projectWizard.CustomStepProjectGenerator
import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase
import com.intellij.ide.util.projectWizard.SettingsStep
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.impl.welcomeScreen.AbstractActionWithPanel
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.platform.DirectoryProjectGeneratorBase
import com.intellij.platform.HideableProjectGenerator
import com.intellij.platform.ProjectGeneratorPeer
import com.intellij.platform.ProjectTemplate
import icons.AwsIcons
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.resources.message
import javax.swing.Icon
import javax.swing.JComponent

/**
 * [DirectoryProjectGeneratorBase] so it shows up in Light IDEs
 * [ProjectTemplate] To allow for us to shim it into ProjectTemplatesFactory and use this in IntelliJ
 * [CustomStepProjectGenerator] so we have full control over the panel
 * [HideableProjectGenerator] so that we can hide it if the IDE doesnt support any of our runtimes
 */
class SamProjectGenerator :
    DirectoryProjectGeneratorBase<SamNewProjectSettings>(),
    ProjectTemplate,
    CustomStepProjectGenerator<SamNewProjectSettings>,
    HideableProjectGenerator {

    // Public for the metrics..is there a better way?
    val schemaPanel = SchemaSelectionPanel()

    val wizardFragments = listOf(
        SdkSelectionPanel(),
        schemaPanel
    )

    private val builder = SamProjectBuilder(this)
    val peer = SamProjectGeneratorSettingsPeer(this, wizardFragments)

    // Only show our wizard if we have SAM templates to show
    override fun isHidden(): Boolean = SamProjectTemplate.supportedTemplates().isEmpty()

    override fun createStep(
        projectGenerator: DirectoryProjectGenerator<SamNewProjectSettings>?,
        callback: AbstractNewProjectStep.AbstractCallback<SamNewProjectSettings>?
    ): AbstractActionWithPanel = SamProjectRuntimeSelectionStep(this)

    // entry point for the Wizard, both light and heavy IDEs eventually hit this spot through our shims
    override fun generateProject(
        project: Project,
        baseDir: VirtualFile,
        settings: SamNewProjectSettings,
        module: Module
    ) {
        val rootModel = ModuleRootManager.getInstance(module).modifiableModel
        builder.contentEntryPath = baseDir.path
        builder.setupRootModel(rootModel)

        runWriteAction {
            rootModel.commit()
        }
    }

    // the peer is in control of the first pane
    override fun createPeer(): ProjectGeneratorPeer<SamNewProjectSettings> = peer

    // these overrides will give us a section for non-IntelliJ IDEs
    override fun getName() = message("sam.init.name")

    override fun getDescription(): String = message("sam.init.description")

    override fun getLogo(): Icon = AwsIcons.Resources.SERVERLESS_APP

    override fun getIcon(): Icon = logo

    override fun createModuleBuilder(): SamProjectBuilder = builder

    // validation is done in the peer
    override fun validateSettings(): ValidationInfo? = null

    override fun getHelpId(): String = HelpIds.NEW_SERVERLESS_PROJECT_DIALOG.id
}

/**
 * Used to overwrite the entire panel in the "light" IDEs so we don't put our settings under "More Settings"
 */
class SamProjectRuntimeSelectionStep(projectGenerator: SamProjectGenerator) :
    ProjectSettingsStepBase<SamNewProjectSettings>(projectGenerator, AbstractNewProjectStep.AbstractCallback<SamNewProjectSettings>())

class SamProjectGeneratorSettingsPeer(val generator: SamProjectGenerator, private val wizardFragments: List<WizardFragment>) :
    ProjectGeneratorPeer<SamNewProjectSettings> {
    private lateinit var samInitSelectionPanel: SamInitSelectionPanel

    /**
     * This hook is used in PyCharm and is called via {@link SamProjectBuilder#modifySettingsStep} for IntelliJ
     */
    override fun validate(): ValidationInfo? = samInitSelectionPanel.validate()

    override fun getSettings(): SamNewProjectSettings = samInitSelectionPanel.getNewProjectSettings()

    // "Deprecated" but required to implement. Not importing to avoid the import deprecation warning.
    @Suppress("OverridingDeprecatedMember", "DEPRECATION")
    override fun addSettingsStateListener(listener: com.intellij.platform.WebProjectGenerator.SettingsStateListener) {
    }

    // we sacrifice a lot of convenience so we can build the UI here...
    override fun buildUI(settingsStep: SettingsStep) {
        // delegate to another panel instead of trying to write UI as code
        settingsStep.addSettingsComponent(component)
    }

    override fun isBackgroundJobRunning(): Boolean = false

    // PyCharm uses this
    override fun getComponent(locationField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent = getPanel(locationField).mainPanel

    // IntelliJ uses this
    override fun getComponent(): JComponent = getPanel(null).mainPanel

    private fun getPanel(locationField: TextFieldWithBrowseButton?): SamInitSelectionPanel {
        if (!::samInitSelectionPanel.isInitialized) {
            samInitSelectionPanel = SamInitSelectionPanel(wizardFragments, locationField)
        }

        return samInitSelectionPanel
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.wizard

import com.intellij.execution.RunManager
import com.intellij.ide.util.projectWizard.ModuleBuilder
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.SettingsStep
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.fileEditor.TextEditorWithPreview
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.options.ConfigurationException
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.ModifiableRootModel
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.ProjectTemplatesFactory
import icons.AwsIcons
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.services.lambda.execution.local.LocalLambdaRunConfiguration
import software.aws.toolkits.jetbrains.services.lambda.execution.local.LocalLambdaRunConfigurationProducer
import software.aws.toolkits.jetbrains.services.lambda.runtimeGroup
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateUtils
import software.aws.toolkits.jetbrains.services.schemas.SchemaTemplateParameters
import software.aws.toolkits.jetbrains.services.schemas.resources.SchemasResources
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.LambdaPackageType
import software.aws.toolkits.telemetry.SamTelemetry
import software.aws.toolkits.telemetry.Runtime.Companion as TelemetryRuntime

// Meshing of two worlds. IntelliJ wants validation errors to be thrown exceptions. Non-IntelliJ wants validation errors
// to be returned as a ValidationInfo object. We have a shim to convert thrown exceptions into objects,
// but then we lose the ability in IntelliJ to fail validation without showing an error. This is a workaround for that case.
class ValidationException : Exception()

// IntelliJ shim requires a ModuleBuilder
// UI is centralized in generator and is passed in to have access to UI elements
// TODO: Does this need to be a module builder, or can we decouple it?
class SamProjectBuilder(private val generator: SamProjectGenerator) : ModuleBuilder() {
    // hide this from the new project menu
    override fun isAvailable() = false

    // dummy type to fulfill the interface, will be replaced in setupRootModel()
    override fun getModuleType(): ModuleType<*>? = ModuleType.EMPTY

    // IntelliJ create commit step
    override fun setupRootModel(rootModel: ModifiableRootModel) {
        val settings = generator.peer.settings

        // Set module type
        val selectedRuntime = settings.runtime
        // TODO luckily this works for dotnet5.0 but if we ever need a module type for a runtime that is
        // not supported by zip and image we will need ot reexamine this
        val moduleType = selectedRuntime.toSdkRuntime()?.runtimeGroup?.getModuleType() ?: ModuleType.EMPTY
        rootModel.module.setModuleType(moduleType.id)

        val contentEntry = doAddContentEntry(rootModel) ?: throw Exception(message("sam.init.error.no.project.basepath"))
        val outputDir = contentEntry.file ?: throw Exception(message("sam.init.error.no.virtual.file"))

        val project = rootModel.project
        ProgressManager.getInstance().run(
            object : Task.Backgroundable(project, message("sam.init.generating.template"), false) {
                override fun run(indicator: ProgressIndicator) {
                    ModuleRootModificationUtil.updateModel(rootModel.module) { model ->
                        runSamInit(project, rootModel.module.name, settings, generator.schemaPanel.schemaInfo(), outputDir)

                        runPostSamInit(project, model, indicator, settings, outputDir)
                    }
                }
            }
        )
    }

    fun runSamInit(
        project: Project?,
        name: String,
        settings: SamNewProjectSettings,
        schemaParameters: SchemaTemplateParameters?,
        outputDir: VirtualFile
    ) {
        var success = true
        try {
            SamInitRunner.execute(
                outputDir,
                settings.template.templateParameters(name, settings.runtime, settings.architecture, settings.packagingType),
                schemaParameters?.takeIf { settings.template.supportsDynamicSchemas() }
            )
        } catch (e: Throwable) {
            success = false
            throw e
        } finally {
            SamTelemetry.init(
                project = project,
                success = success,
                runtime = TelemetryRuntime.from(settings.runtime.toString()),
                version = SamCommon.getVersionString(),
                templateName = getName(),
                lambdaPackageType = LambdaPackageType.from(settings.packagingType.toString()),
                eventBridgeSchema = if (schemaParameters?.schema?.registryName == SchemasResources.AWS_EVENTS_REGISTRY) schemaParameters.schema.name else null
            )
        }
    }

    fun runPostSamInit(
        project: Project,
        model: ModifiableRootModel,
        indicator: ProgressIndicator,
        settings: SamNewProjectSettings,
        outputDir: VirtualFile
    ) {
        generator.wizardFragments.forEach { it.postProjectGeneration(model, settings.template, settings.runtime, indicator) }

        settings.template.postCreationAction(settings, outputDir, model, indicator)

        // Perform a refresh to load any generated files
        outputDir.refresh(false, true)

        openReadmeFile(project, outputDir)
        createRunConfigurations(project, outputDir, settings.runtime)
    }

    private fun openReadmeFile(project: Project, contentRoot: VirtualFile) {
        VfsUtil.findRelativeFile(contentRoot, "README.md")?.let { readme ->
            readme.putUserData(TextEditorWithPreview.DEFAULT_LAYOUT_FOR_FILE, TextEditorWithPreview.Layout.SHOW_PREVIEW)

            val fileEditorManager = FileEditorManager.getInstance(project)
            runInEdt {
                fileEditorManager.openTextEditor(OpenFileDescriptor(project, readme), true) ?: LOG.warn { "Failed to open README.md" }
            }
        }
    }

    private fun createRunConfigurations(project: Project, contentRoot: VirtualFile, runtime: LambdaRuntime) {
        val template = SamCommon.getTemplateFromDirectory(contentRoot) ?: return

        val factory = LocalLambdaRunConfigurationProducer.getFactory()
        val runManager = RunManager.getInstance(project)
        SamTemplateUtils.findFunctionsFromTemplate(project, template).forEach {
            val runConfigurationAndSettings = runManager.createConfiguration(it.logicalName, factory)

            val runConfiguration = runConfigurationAndSettings.configuration as LocalLambdaRunConfiguration
            runConfiguration.useTemplate(template.path, it.logicalName, runtime.toString())
            runConfiguration.setGeneratedName()

            runManager.addConfiguration(runConfigurationAndSettings)

            if (runManager.selectedConfiguration == null) {
                runManager.selectedConfiguration = runConfigurationAndSettings
            }
        }
    }

    override fun modifySettingsStep(settingsStep: SettingsStep): ModuleWizardStep {
        generator.peer.buildUI(settingsStep)

        // need to return an object with validate() implemented for validation
        return object : ModuleWizardStep() {
            override fun getComponent() = null

            override fun updateDataModel() {}

            @Throws(ConfigurationException::class)
            override fun validate(): Boolean {
                try {
                    val info = generator.peer.validate()
                    if (info != null) throw ConfigurationException(info.message)
                } catch (_: ValidationException) {
                    return false
                }

                return true
            }
        }
    }

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

class SamProjectGeneratorIntelliJAdapter : ProjectTemplatesFactory() {
    override fun createTemplates(group: String?, context: WizardContext) = arrayOf(SamProjectGenerator())

    override fun getGroupIcon(group: String?) = AwsIcons.Logos.AWS

    override fun getGroups() = arrayOf("AWS")
}


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

package software.aws.toolkits.jetbrains.services.lambda.wizard

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.core.lambda.LambdaArchitecture
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroup
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroupExtensionPointObject
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon
import software.aws.toolkits.jetbrains.services.lambda.sam.SamTemplateUtils
import java.nio.file.Paths

/**
 * Used to manage SAM project information for different [RuntimeGroup]s
 */
interface SamProjectWizard {
    /**
     * Return a collection of templates supported by the [RuntimeGroup]
     */
    fun listTemplates(): Collection<SamProjectTemplate>

    /**
     * Return an instance of UI section for selecting SDK for the [RuntimeGroup]
     */
    fun createSdkSelectionPanel(projectLocation: TextFieldWithBrowseButton?): SdkSelector?

    companion object : RuntimeGroupExtensionPointObject<SamProjectWizard>(ExtensionPointName("aws.toolkit.lambda.sam.projectWizard"))
}

data class SamNewProjectSettings(
    val template: SamProjectTemplate,
    val runtime: LambdaRuntime,
    val architecture: LambdaArchitecture,
    val packagingType: PackageType
)

abstract class SamProjectTemplate {
    abstract fun displayName(): String

    open fun description(): String? = null

    override fun toString() = displayName()

    abstract fun supportedZipRuntimes(): Set<LambdaRuntime>

    abstract fun supportedImageRuntimes(): Set<LambdaRuntime>

    // Gradual opt-in for Schema support on a template by-template basis.
    // All SAM templates should support schema selection, but for launch include only EventBridge for most optimal customer experience
    open fun supportsDynamicSchemas(): Boolean = false

    abstract fun templateParameters(
        projectName: String,
        runtime: LambdaRuntime,
        architecture: LambdaArchitecture,
        packagingType: PackageType
    ): TemplateParameters

    open fun postCreationAction(
        settings: SamNewProjectSettings,
        contentRoot: VirtualFile,
        rootModel: ModifiableRootModel,
        indicator: ProgressIndicator
    ) {
        excludeSamDirectory(rootModel, contentRoot)
    }

    protected fun addSourceRoots(project: Project, modifiableModel: ModifiableRootModel, projectRoot: VirtualFile) {
        val template = SamCommon.getTemplateFromDirectory(projectRoot) ?: return
        val templatePath = Paths.get(template.parent.path)
        runInEdt {
            val functions = SamTemplateUtils.findFunctionsFromTemplate(project, template)
            val functionLocations = functions.map {
                val codeLocation = SamTemplateUtils.getCodeLocation(template.toNioPath().toAbsolutePath(), it.logicalName)
                templatePath.parent.resolve(codeLocation)
            }

            val localFileSystem = LocalFileSystem.getInstance()
            val function = functionLocations.mapNotNull { localFileSystem.refreshAndFindFileByIoFile(it.toFile()) }
                .filter { it.isDirectory }

            modifiableModel.contentEntries.forEach { contentEntry ->
                if (contentEntry.file == projectRoot) {
                    function.forEach { contentEntry.addSourceFolder(it, false) }
                }
            }
        }
    }

    private fun excludeSamDirectory(modifiableModel: ModifiableRootModel, projectRoot: VirtualFile) {
        modifiableModel.contentEntries.forEach { contentEntry ->
            if (contentEntry.file == projectRoot) {
                contentEntry.addExcludeFolder(
                    VfsUtilCore.pathToUrl(
                        Paths.get(projectRoot.path, SamCommon.SAM_BUILD_DIR).toString()
                    )
                )
            }
        }
    }

    // defined so that we can restore the template selection when the runtime selection changes
    override fun equals(other: Any?) = if (other is SamProjectTemplate) {
        displayName() == other.displayName()
    } else {
        false
    }

    override fun hashCode() = displayName().hashCode()

    companion object {
        // Dont cache this since it is not compatible in a dynamic plugin world / waste memory if no longer needed
        fun supportedTemplates() = SamProjectWizard.supportedRuntimeGroups().flatMap {
            SamProjectWizard.getInstance(it).listTemplates()
        }
    }
}

abstract class SamAppTemplateBased : SamProjectTemplate() {
    abstract val dependencyManager: String
    abstract val appTemplateName: String
    open val appTemplateNameImage: String = "hello-world-lambda-image"

    override fun templateParameters(
        projectName: String,
        runtime: LambdaRuntime,
        architecture: LambdaArchitecture,
        packagingType: PackageType
    ): TemplateParameters = when (packagingType) {
        PackageType.IMAGE -> AppBasedImageTemplate(
            name = projectName,
            baseImage = "amazon/$runtime-base",
            architecture = architecture,
            dependencyManager = dependencyManager,
            appTemplate = appTemplateNameImage
        )
        PackageType.ZIP -> AppBasedZipTemplate(
            name = projectName,
            runtime = runtime,
            architecture = architecture,
            dependencyManager = dependencyManager,
            appTemplate = appTemplateName
        )
        else -> throw IllegalStateException("Unknown packaging type: $packagingType")
    }
}


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

package software.aws.toolkits.jetbrains.services.lambda.wizard

// TODO: This is fragile. Very fragile. But it is necessary to get Schemas service launched, and we've evaluated all other trade offs
// This will be done on the server-side as soon as we can, but for now the client needs to do this
class SchemaCodeGenUtils {
    companion object {
        private const val SCHEMA_PACKAGE_PREFIX = "schema"
        private const val AWS = "aws"
        private const val PARTNER = "partner"

        // dash suffix because of 3p partner registry name format
        private const val AWS_PARTNER_PREFIX = "$AWS.$PARTNER-"

        // . suffix because of 1p event registry schema format
        private const val AWS_EVENTS_PREFIX = "$AWS."

        fun buildSchemaPackageName(schemaName: String): String {
            val builder = CodeGenPackageBuilder()
            builder.append(SCHEMA_PACKAGE_PREFIX)
            buildPackageName(builder, schemaName)
            return builder.toString()
        }

        private fun buildPackageName(builder: CodeGenPackageBuilder, schemaName: String) {
            if (isAwsPartnerEvent(schemaName)) {
                buildPartnerEventPackageName(builder, schemaName)
            } else if (isAwsEvent(schemaName)) {
                buildAwsEventPackageName(builder, schemaName)
            } else {
                buildCustomPackageName(builder, schemaName)
            }
        }

        private fun isAwsPartnerEvent(schemaName: String): Boolean = schemaName.startsWith(AWS_PARTNER_PREFIX)

        private fun buildPartnerEventPackageName(builder: CodeGenPackageBuilder, schemaName: String) {
            val partnerSchemaString = schemaName.substring(AWS_PARTNER_PREFIX.length)

            builder
                .append(AWS)
                .append(PARTNER)
                .append(partnerSchemaString)
        }

        private fun isAwsEvent(name: String): Boolean = name.startsWith(AWS_EVENTS_PREFIX)

        private fun buildAwsEventPackageName(builder: CodeGenPackageBuilder, schemaName: String) {
            val awsEventSchemaParts = schemaName.split(".")
            for (part in awsEventSchemaParts) {
                builder.append(part)
            }
        }

        private fun buildCustomPackageName(builder: CodeGenPackageBuilder, schemaName: String) = builder.append(schemaName)
    }

    class CodeGenPackageBuilder {
        private val builder: StringBuilder = StringBuilder()

        fun append(segment: String): CodeGenPackageBuilder {
            if (builder.isNotEmpty()) {
                builder.append(IdentifierFormatter.PACKAGE_SEPARATOR)
            }
            builder.append(IdentifierFormatter.toValidIdentifier(segment.toLowerCase()))
            return this
        }

        override fun toString(): String = builder.toString()
    }

    object IdentifierFormatter {
        private const val POTENTIAL_PACKAGE_SEPARATOR = "@"

        private const val NOT_VALID_IDENTIFIER_CHARACTER = "[^a-zA-Z0-9_$POTENTIAL_PACKAGE_SEPARATOR]"
        private val NOT_VALID_IDENTIFIER_REGEX = Regex(NOT_VALID_IDENTIFIER_CHARACTER)

        const val PACKAGE_SEPARATOR = "."

        private const val UNDERSCORE = "_"

        fun toValidIdentifier(name: String): String = name
            .replace(NOT_VALID_IDENTIFIER_REGEX, UNDERSCORE)
            .replace(POTENTIAL_PACKAGE_SEPARATOR, PACKAGE_SEPARATOR)
    }
}