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

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

import com.intellij.openapi.project.Project
import com.intellij.ui.PopupMenuListenerAdapter
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.credentials.profiles.DEFAULT_PROFILE_ID
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import javax.swing.JComponent
import javax.swing.event.PopupMenuEvent

class AwsConnectionSettingsSelector(
    project: Project?,
    serviceId: String? = null,
    private val settingsChangedListener: (ConnectionSettings?) -> Unit = { _ -> },
) {
    private val regionProvider = AwsRegionProvider.getInstance()
    private val credentialManager = CredentialManager.getInstance()
    val view = AwsConnectionSettings()

    init {
        val regions = if (serviceId != null) {
            regionProvider.allRegionsForService(serviceId).values.toMutableList()
        } else {
            regionProvider.allRegions().values.toMutableList()
        }
        view.region.setRegions(regions)
        view.credentialProvider.setCredentialsProviders(credentialManager.getCredentialIdentifiers())

        // nullable; unfortunately we can't rely on connection manager instance being retrievable from the default project
        if (project != null) {
            val accountSettingsManager = AwsConnectionManager.getInstance(project)
            if (accountSettingsManager.isValidConnectionSettings()) {
                view.region.selectedRegion = accountSettingsManager.activeRegion
                accountSettingsManager.selectedCredentialIdentifier?.let {
                    view.credentialProvider.setSelectedCredentialsProvider(it)
                }
            }
        } else {
            view.region.selectedRegion = regionProvider.defaultRegion()

            // pick default or the first one
            (credentialManager.getCredentialIdentifierById(DEFAULT_PROFILE_ID) ?: credentialManager.getCredentialIdentifiers().firstOrNull())?.let {
                view.credentialProvider.setSelectedCredentialsProvider(it)
            }
        }
        fireChange()

        view.credentialProvider.addPopupMenuListener(
            object : PopupMenuListenerAdapter() {
                override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
                    fireChange()
                }
            }
        )

        view.region.addPopupMenuListener(
            object : PopupMenuListenerAdapter() {
                override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
                    fireChange()
                }
            }
        )
    }

    private fun fireChange() {
        settingsChangedListener(connectionSettings())
    }

    fun selectorPanel(): JComponent = view.panel

    fun resetAwsConnectionOptions(regionId: String?, credentialProviderId: String?) {
        if (regionId != null) {
            view.region.selectedRegion = regionProvider[regionId]
        }
        if (credentialProviderId == null) {
            return
        }

        try {
            val credentialIdentifier = credentialManager.getCredentialIdentifierById(credentialProviderId)
            if (credentialIdentifier != null) {
                view.credentialProvider.setSelectedCredentialsProvider(credentialIdentifier)
            }
        } catch (_: Exception) {
            view.credentialProvider.setSelectedInvalidCredentialsProvider(credentialProviderId)
        }
        fireChange()
    }

    fun selectedCredentialProvider(): String? = view.credentialProvider.getSelectedCredentialsProvider()

    fun selectedRegion(): AwsRegion? = view.region.selectedRegion

    fun connectionSettings() = view.region.selectedRegion?.let { region ->
        view.credentialProvider.getSelectedCredentialsProvider()?.let { credId ->
            val manager = CredentialManager.getInstance()
            manager.getCredentialIdentifierById(credId)?.let {
                ConnectionSettings(manager.getAwsCredentialProvider(it, region), region)
            }
        }
    }
}


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

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

import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.openapi.Disposable
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.VerticalFlowLayout
import com.intellij.openapi.util.Disposer
import com.intellij.ui.AppIcon
import com.intellij.ui.components.panels.NonOpaquePanel
import com.intellij.ui.components.panels.Wrapper
import com.intellij.ui.layout.applyToComponent
import com.intellij.ui.layout.panel
import com.intellij.util.ui.JBFont
import icons.AwsIcons
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.jetbrains.core.credentials.sono.SonoCredentialManager
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.services.caws.CawsEndpoints
import software.aws.toolkits.resources.message
import javax.swing.JComponent

open class SonoLoginOverlay(
    private val project: Project?,
    private val disposable: Disposable,
    private val drawPostLoginContent: (SonoLoginOverlay.(ClientConnectionSettings<*>) -> JComponent)
) :
    NonOpaquePanel(VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, true)) {

    private val contentWrapper = Wrapper()
    private val loginSubpanel by lazy {
        // TODO: pending final UX
        panel {
            row {
                label(message("code.aws"), JBFont.h2()).applyToComponent {
                    icon = AwsIcons.Logos.AWS_SMILE_LARGE
                }
            }

            row {
                label(message("code.aws.value_prop_text"))
            }

            row {
                browserLink("Learn more", CawsEndpoints.ConsoleFactory.marketing())
            }

            row {
                button(message("caws.login")) {
                    ApplicationManager.getApplication().executeOnPooledThread {
                        SonoCredentialManager.loginSono(project)
                    }
                }.apply {
                    applyToComponent {
                        putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
                    }
                }
            }
        }.andTransparent()
    }

    init {
        border = null
        add(contentWrapper)

        redraw()

        ApplicationManager.getApplication().messageBus.connect(disposable).subscribe(
            BearerTokenProviderListener.TOPIC,
            object : BearerTokenProviderListener {
                override fun onChange(providerId: String) {
                    redraw()
                }
            }
        )
    }

    open fun initBorders() {}

    fun redraw() {
        with(contentWrapper.targetComponent) {
            if (this is Disposable) {
                Disposer.dispose(this)
            }
        }

        val connectionSettings = SonoCredentialManager.getInstance().getConnectionSettings()

        // specify 'any' because if we're currently in a modal dialog, we noop until the dialog is closed
        runInEdt(ModalityState.any()) {
            initBorders()
            if (connectionSettings != null) {
                AppIcon.getInstance().requestAttention(null, false)
                contentWrapper.setContent(drawPostLoginContent(connectionSettings))
            } else {
                contentWrapper.setContent(loginSubpanel)
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.ui.feedback

const val FEEDBACK_SOURCE = "source"
const val ENABLED_EXPERIMENTS = "experimentsEnabled"


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

package software.aws.toolkits.jetbrains.ui.feedback

import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.ColorUtil
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.bind
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.columns
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.rows
import com.intellij.util.IconUtil
import com.intellij.util.ui.UIUtil
import icons.AwsIcons
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.FeedbackTelemetry
import software.aws.toolkits.telemetry.Result
import java.net.URLEncoder

class FeedbackDialog(val project: Project, initialSentiment: Sentiment = Sentiment.POSITIVE, initialComment: String = "") : DialogWrapper(project) {
    private val coroutineScope = projectCoroutineScope(project)
    private var sentiment = initialSentiment
    private val smileIcon = IconUtil.scale(AwsIcons.Misc.SMILE, null, 3f)
    private val sadIcon = IconUtil.scale(AwsIcons.Misc.FROWN, null, 3f)
    private var commentText: String = initialComment
    private lateinit var comment: Cell<JBTextArea>
    private var lengthLimitLabel = JBLabel(message("feedback.comment.textbox.initial.length")).also { it.foreground = UIUtil.getLabelInfoForeground() }

    private val dialogPanel = panel {
        row {
            text(message("feedback.initial.help.text"))
        }
        group(message("feedback.connect.with.github.title")) {
            row {
                icon(AllIcons.Toolwindows.ToolWindowDebugger)
                link(message("feedback.report.issue.link")) {
                    BrowserUtil.browse("${GITHUB_LINK_BASE}${URLEncoder.encode("${comment.component.text}\n\n$toolkitMetadata", Charsets.UTF_8.name())}")
                }
            }
            row {
                icon(AllIcons.Actions.IntentionBulbGrey)

                link(message("feedback.request.feature.link")) {
                    BrowserUtil.browse("${GITHUB_LINK_BASE}${URLEncoder.encode("${comment.component.text}\n\n$toolkitMetadata", Charsets.UTF_8.name())}")
                }
            }
            row {
                icon(AllIcons.Nodes.Tag)
                link(message("feedback.view.source.code.link")) {
                    BrowserUtil.browse(TOOLKIT_REPOSITORY_LINK)
                }
            }
        }

        group(message("feedback.share.feedback.title")) {
            buttonsGroup {
                row {
                    radioButton("", value = Sentiment.POSITIVE).applyToComponent {
                        icon(smileIcon)
                    }

                    radioButton("", value = Sentiment.NEGATIVE).applyToComponent {
                        icon(sadIcon)
                    }
                }
            }.bind({ sentiment }, { sentiment = it })

            row(message("feedback.comment.textbox.title")) {}
            row { comment(message("feedback.customer.alert.info")) }
            row {
                comment = textArea().rows(6).columns(52).bindText(::commentText).applyToComponent {
                    this.emptyText.text = message("feedback.comment.emptyText")
                    this.lineWrap = true

                    this.document.addUndoableEditListener {
                        onTextAreaUpdate(this.text)
                        commentText = this.text
                    }
                }
            }.comment(commentText)
            row {
                cell(lengthLimitLabel)
            }
        }
    }
    override fun createCenterPanel() = dialogPanel

    override fun doCancelAction() {
        super.doCancelAction()
        // kill any remaining coroutines
        coroutineScope.coroutineContext.cancel()
        FeedbackTelemetry.result(project, result = Result.Cancelled)
    }

    override fun doOKAction() {
        if (okAction.isEnabled) {
            dialogPanel.apply()
            setOKButtonText(message("feedback.submitting"))
            isOKActionEnabled = false
            var result = Result.Succeeded
            coroutineScope.launch {
                val edtContext = getCoroutineUiContext()
                try {
                    TelemetryService.getInstance().sendFeedback(sentiment, commentText)
                    withContext(edtContext) {
                        close(OK_EXIT_CODE)
                    }
                    notifyInfo(message("aws.notification.title"), message("feedback.submit_success"), project)
                } catch (e: Exception) {
                    withContext(edtContext) {
                        Messages.showMessageDialog(message("feedback.submit_failed", e), message("feedback.submit_failed_title"), null)
                        setOKButtonText(message("feedback.submit_button"))
                        isOKActionEnabled = true
                    }
                    result = Result.Failed
                } finally {
                    FeedbackTelemetry.result(project, result = result)
                }
            }
        }
    }

    override fun doValidate(): ValidationInfo? {
        super.doValidate()
        val comment = commentText

        return when {
            comment.isEmpty() -> null
            comment.length >= MAX_LENGTH -> ValidationInfo(message("feedback.validation.comment_too_long"))
            else -> null
        }
    }

    private fun onTextAreaUpdate(commentText: String) {
        this.commentText = commentText
        val currentLength = this.commentText.length
        val lengthText = message("feedback.limit.label", MAX_LENGTH - currentLength)
        lengthLimitLabel.text = if (currentLength >= MAX_LENGTH) {
            "$lengthText"
        } else {
            lengthText
        }
    }

    init {
        super.init()
        title = message("feedback.title")
        setOKButtonText(message("feedback.submit_button"))
    }

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

    @TestOnly
    fun getFeedbackDialog() = dialogPanel

    companion object {
        const val MAX_LENGTH = 2000 // backend restriction
        private const val GITHUB_LINK_BASE = "https://github.com/aws/aws-toolkit-jetbrains/issues/new?body="
        private const val TOOLKIT_REPOSITORY_LINK = "https://github.com/aws/aws-toolkit-jetbrains"
        private val toolkitMetadata = ClientMetadata.DEFAULT_METADATA.let {
            """
                ---
                Toolkit: ${it.productName} ${it.productVersion}
                OS: ${it.os} ${it.osVersion}
                IDE: ${it.parentProduct} ${it.parentProductVersion}
            """.trimIndent()
        }
    }
}

class ShowFeedbackDialogAction : DumbAwareAction(message("feedback.title"), message("feedback.description"), AwsIcons.Misc.SMILE_GREY) {
    override fun actionPerformed(e: AnActionEvent) {
        runInEdt {
            FeedbackDialog(e.getRequiredData(LangDataKeys.PROJECT)).show()
        }
    }

    override fun update(e: AnActionEvent) {
        super.update(e)
        e.presentation.icon = AwsIcons.Misc.SMILE_GREY
    }
}


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

package software.aws.toolkits.jetbrains.ui.feedback

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.DefaultProjectFactory
import com.intellij.openapi.project.DumbAwareAction
import software.aws.toolkits.resources.message

class SubmitFeedbackInGateway : DumbAwareAction(message("feedback.title")) {
    override fun actionPerformed(e: AnActionEvent) {
        runInEdt {
            FeedbackDialog(DefaultProjectFactory.getInstance().defaultProject).show()
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.progress.ProcessCanceledException
import org.jetbrains.annotations.TestOnly
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.isPending
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import software.aws.toolkits.core.utils.info
import java.util.concurrent.TimeUnit

abstract class CachingAsyncEvaluator<TEntry, TReturn> {

    companion object {
        private const val EVALUATE_BLOCKING_TIMEOUT_MS = 1000
    }

    private val logger: Logger = LoggerFactory.getLogger(this::class.java)
    private val requests = hashMapOf<TEntry, Promise<TReturn>>()
    private val lock = Object()

    /**
     * @return A promise for the requested file info. Promise will be resolved when the info is ready.
     */
    fun evaluate(entry: TEntry): Promise<TReturn> {
        logger.info { "Evaluating $entry" }

        val asyncPromise = AsyncPromise<TReturn>()
        asyncPromise
            .onSuccess { result ->
                logger.info { "File info evaluation is completed: '$result'" }
            }
            .onError { error ->
                // Need to set an error handler early, else the setError call will throw AssertionError
                if (error !is ProcessCanceledException) {
                    logger.info(error) { "Failed to evaluate $entry" }
                }
                clearRequest(entry)
            }

        val promise = synchronized(lock) {
            val cachePromise = requests.getOrPut(entry) { asyncPromise }
            if (!cachePromise.isPending && cachePromise.isSucceeded) {
                if (isInvalidated(entry, cachePromise.blockingGet(0)!!)) {
                    requests[entry] = asyncPromise
                    return@synchronized asyncPromise
                }
            }
            cachePromise
        }

        if (promise == asyncPromise) {
            ApplicationManager.getApplication().executeOnPooledThread {
                try {
                    val result = getValue(entry)
                    asyncPromise.setResult(result)
                } catch (t: Throwable) {
                    asyncPromise.setError(t)
                }
            }
        }

        return promise
    }

    abstract fun getValue(entry: TEntry): TReturn

    open fun isInvalidated(entry: TEntry, value: TReturn): Boolean = false

    fun evaluateBlocking(entry: TEntry, blockingTime: Int = EVALUATE_BLOCKING_TIMEOUT_MS, blockingUnit: TimeUnit = TimeUnit.MILLISECONDS): TReturn {
        val promise = evaluate(entry)
        return promise.blockingGet(blockingTime, blockingUnit)!!
    }

    fun clearRequests() {
        synchronized(lock) {
            requests.clear()
        }
    }

    private fun clearRequest(entry: TEntry) {
        synchronized(lock) {
            requests.remove(entry)
        }
    }

    @TestOnly
    fun testOnlyGetRequestCache() = requests
}


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

package software.aws.toolkits.jetbrains.utils

import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider

private const val CLASSIC_PARTITION = "aws"

private fun availableInClassic(activeRegion: AwsRegion): Boolean = activeRegion.partitionId == CLASSIC_PARTITION

// technically available in govcloud but the api/console is broken
fun lambdaTracingConfigIsAvailable(activeRegion: AwsRegion) = availableInClassic(activeRegion) &&
    AwsRegionProvider.getInstance().isServiceSupported(activeRegion, "xray")


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.util.io.FileUtil
import software.aws.toolkits.resources.message
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Paths
import java.time.Instant

/**
 * Stores data related to a file path. Cache is invalidated when the cache entry is detected as stale.  Errors are
 * cached until the underlying path is detected as stale. Stale is defined as the cache entries (file modification time)[Files.getLastModifiedTime]
 * is older than the path's current modification time.
 */
abstract class FileInfoCache<T> : CachingAsyncEvaluator<String, FileInfoCache.InfoResult<T>>() {

    override fun getValue(entry: String): InfoResult<T> {
        if (!FileUtil.exists(entry)) {
            throw IllegalStateException(message("general.file_not_found", entry))
        }

        return InfoResult(getFileInfo(entry), getLastModificationDate(entry))
    }

    override fun isInvalidated(entry: String, value: InfoResult<T>): Boolean =
        getLastModificationDate(entry).isAfter(value.timestamp)

    protected abstract fun getFileInfo(path: String): T

    private fun getLastModificationDate(path: String): Instant {
        try {
            return Files.getLastModifiedTime(Paths.get(path)).toInstant()
        } catch (e: NoSuchFileException) {
            // If unable to get the current time, override the cache entry that the file can't be found
            throw IllegalStateException(message("general.file_not_found", path))
        }
    }

    data class InfoResult<T>(val result: T, internal val timestamp: Instant)
}


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

package software.aws.toolkits.jetbrains.utils

class MRUList<T>(private val maxSize: Int) {
    private val internalList = mutableListOf<T>()

    fun add(element: T) {
        internalList.remove(element)
        internalList.add(0, element)
        trimToSize()
    }

    fun elements(): List<T> = internalList.toList()

    fun clear() {
        internalList.clear()
    }

    private fun trimToSize() {
        while (internalList.size > maxSize) {
            internalList.removeAt(internalList.size - 1)
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationListener
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications.Bus.notify
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.ScrollPaneFactory
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.core.credentials.ChangeSettingsMode
import software.aws.toolkits.jetbrains.core.credentials.ConfigureAwsConnectionAction
import software.aws.toolkits.jetbrains.core.help.HelpIds
import software.aws.toolkits.jetbrains.settings.AwsSettingsConfigurable
import software.aws.toolkits.resources.message
import javax.swing.JLabel
import javax.swing.JTextArea

private const val GROUP_DISPLAY_ID = "AWS Toolkit"
private val LOG = getLogger<AwsToolkit>()

fun Throwable.notifyError(title: String = "", project: Project? = null, stripHtml: Boolean = true) {
    val message = getCleanedContent(this.message ?: "${this::class.java.name}${this.stackTrace?.joinToString("\n", prefix = "\n")}", stripHtml)
    LOG.warn(this) { title.takeIf { it.isNotBlank() }?.let { "$it ($message)" } ?: message }
    notify(
        Notification(
            GROUP_DISPLAY_ID,
            title,
            message,
            NotificationType.ERROR
        ),
        project
    )
}

private fun notify(type: NotificationType, title: String, content: String = "", project: Project? = null, notificationActions: Collection<AnAction>) {
    val notification = Notification(GROUP_DISPLAY_ID, title, content, type)
    notificationActions.forEach {
        notification.addAction(if (it !is NotificationAction) createNotificationExpiringAction(it) else it)
    }
    notify(notification, project)
}

fun notifyInfo(title: String, content: String = "", project: Project? = null, listener: NotificationListener? = null, stripHtml: Boolean = true) =
    notify(Notification(GROUP_DISPLAY_ID, title, getCleanedContent(content, stripHtml), NotificationType.INFORMATION, listener), project)

fun notifyInfo(title: String, content: String = "", project: Project? = null, notificationActions: Collection<AnAction>, stripHtml: Boolean = true) =
    notify(NotificationType.INFORMATION, title, getCleanedContent(content, stripHtml), project, notificationActions)

fun notifyWarn(title: String, content: String = "", project: Project? = null, notificationActions: Collection<AnAction>, stripHtml: Boolean = true) =
    notify(NotificationType.WARNING, title, getCleanedContent(content, stripHtml), project, notificationActions)

fun notifyWarn(title: String, content: String = "", project: Project? = null, listener: NotificationListener? = null, stripHtml: Boolean = true) =
    notify(Notification(GROUP_DISPLAY_ID, title, getCleanedContent(content, stripHtml), NotificationType.WARNING, listener), project)

fun notifyError(title: String, content: String = "", project: Project? = null, action: AnAction, stripHtml: Boolean = true) =
    notify(NotificationType.ERROR, title, getCleanedContent(content, stripHtml), project, listOf(action))

fun notifyError(title: String, content: String = "", project: Project? = null, notificationActions: Collection<AnAction>, stripHtml: Boolean = true) =
    notify(NotificationType.ERROR, title, getCleanedContent(content, stripHtml), project, notificationActions)

fun notifyError(
    title: String = message("aws.notification.title"),
    content: String = "",
    project: Project? = null,
    listener: NotificationListener? = null,
    stripHtml: Boolean = true
) = notify(Notification(GROUP_DISPLAY_ID, title, getCleanedContent(content, stripHtml), NotificationType.ERROR, listener), project)

/**
 * Notify error that AWS credentials are not configured.
 */
fun notifyNoActiveCredentialsError(
    project: Project,
    title: String = message("aws.notification.title"),
    content: String = message("aws.notification.credentials_missing")
) {
    notifyError(
        title = title,
        content = content,
        project = project,
        action = ConfigureAwsConnectionAction(ChangeSettingsMode.CREDENTIALS)
    )
}

/**
 * Notify error that AWS SAM CLI is not valid.
 */
fun notifySamCliNotValidError(
    project: Project,
    title: String = message("aws.notification.title"),
    content: String
) {
    notifyError(
        title = title,
        content = message("aws.notification.sam_cli_not_valid", content),
        project = project,
        listener = NotificationListener { notification, _ ->
            ShowSettingsUtil.getInstance().showSettingsDialog(project, AwsSettingsConfigurable::class.java)
            notification.expire()
        },
        stripHtml = false
    )
}

fun <T> tryNotify(message: String, block: () -> T): T? = try {
    block()
} catch (e: Exception) {
    e.notifyError(message)
    null
}

/**
 * Creates a Notification Action that will expire a notification after performing some AnAction
 */
fun createNotificationExpiringAction(action: AnAction): NotificationAction = NotificationAction.create(
    action.templatePresentation.text
) { actionEvent, notification ->
    action.actionPerformed(actionEvent)
    notification.expire()
}

fun createShowMoreInfoDialogAction(actionName: String?, title: String?, message: String?, moreInfo: String?) =
    object : AnAction(actionName) {
        override fun isDumbAware() = true

        override fun actionPerformed(e: AnActionEvent) {
            val dialogTitle = title ?: ""

            val textArea = JTextArea(moreInfo).apply {
                columns = 50
                rows = 5
                lineWrap = true
                wrapStyleWord = true
                isEditable = false
            }

            val dialogBuilder = DialogBuilder().apply {
                setTitle(dialogTitle)
                setNorthPanel(JLabel(message))
                setCenterPanel(ScrollPaneFactory.createScrollPane(textArea))
                setPreferredFocusComponent(textArea)
                setHelpId(HelpIds.SETUP_CREDENTIALS.id)
                removeAllActions()
                addOkAction()
            }

            dialogBuilder.show()
        }
    }

private fun getCleanedContent(content: String, stripHtml: Boolean): String = if (stripHtml) StringUtil.stripHtml(content, true) else content


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.execution.process.ProcessOutput
import org.slf4j.Logger
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.info

fun ProcessOutput.checkSuccess(logger: Logger): Boolean {
    val code = exitCode
    if (code == 0 && !isTimeout) {
        return true
    }
    logger.info { if (isTimeout) "Timed out" else "Exit code $code" }
    logger.debug { stderr.takeIf { it.isNotEmpty() } ?: stdout }

    return false
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.util.io.size
import java.io.FileInputStream
import java.io.FilterInputStream
import java.io.FilterOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path

class ProgressMonitorInputStream(
    indicator: ProgressIndicator,
    delegate: InputStream,
    length: Long,
    cancelable: Boolean = true
) : FilterInputStream(delegate) {

    private val monitor = ProgressMonitor(indicator, length, cancelable)
    private var marked: Long = 0

    override fun read(): Int = super.read().also { monitor.updateProgress(if (it >= 0) 1 else 0) }

    override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, len).also { monitor.updateProgress(it.toLong()) }

    override fun skip(n: Long): Long = super.skip(n).also { monitor.updateProgress(it) }

    override fun mark(readlimit: Int) {
        super.mark(readlimit)
        marked = monitor.completed
    }

    override fun reset() {
        super.reset()
        monitor.completed = marked
        monitor.updateProgress(0)
    }

    companion object {
        fun fromFile(indicator: ProgressIndicator, path: Path): InputStream = ProgressMonitorInputStream(indicator, FileInputStream(path.toFile()), path.size())
    }
}

class ProgressMonitorOutputStream(indicator: ProgressIndicator, private val delegate: OutputStream, length: Long, cancelable: Boolean = true) :
    FilterOutputStream(delegate) {

    private val monitor = ProgressMonitor(indicator, length, cancelable)

    override fun write(b: Int) {
        super.write(b)
        monitor.updateProgress(1)
    }

    override fun write(b: ByteArray, off: Int, len: Int) {
        delegate.write(b, off, len)
        monitor.updateProgress(len.toLong())
    }
}

private class ProgressMonitor(private val indicator: ProgressIndicator, private val length: Long, private val cancelable: Boolean) {
    var completed: Long = 0

    fun updateProgress(increment: Long) {
        if (cancelable) {
            indicator.checkCanceled()
        }
        completed += increment
        if (!indicator.isIndeterminate) {
            indicator.fraction = completed.toDouble() / length.toDouble()
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.injected.editor.VirtualFileWindow
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.psi.PsiElement

fun PsiElement.isTestOrInjectedText(): Boolean {
    val project = this.project
    val virtualFile = this.containingFile.virtualFile ?: return false
    if (this.isInjectedText() || ProjectRootManager.getInstance(project).fileIndex.isInTestSourceContent(virtualFile)) {
        return true
    }

    return false
}

fun PsiElement.isInjectedText(): Boolean {
    val virtualFile = this.containingFile.virtualFile ?: return false
    if (virtualFile is VirtualFileWindow) {
        return true
    }

    return false
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.extensions.ExtensionNotApplicableException

fun isRunningOnRemoteBackend() = System.getenv("REMOTE_DEV_LAUNCHER_NAME_FOR_USAGE") != null

fun disableExtensionIfRemoteBackend() {
    if (isRunningOnRemoteBackend()) {
        throw ExtensionNotApplicableException.create()
    }
}


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

package software.aws.toolkits.jetbrains.utils

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.time.Duration
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicReference

/**
 * Keeps checking the condition until the max duration as been reached. Checks every 100ms
 */
fun spinUntil(duration: Duration, interval: Duration = Duration.ofMillis(100), condition: () -> Boolean) {
    val start = System.nanoTime()
    runBlocking {
        while (!condition()) {
            if (System.nanoTime() - start > duration.toNanos()) {
                throw TimeoutException("Condition not reached within $duration")
            }
            delay(interval.toMillis())
        }
    }
}

/**
 * Keeps checking the block until the max duration as been reached or a non-null value has been returned. Checks every 100ms
 */
fun <T> spinUntilValue(duration: Duration, interval: Duration = Duration.ofMillis(100), block: () -> T?): T {
    val ref = AtomicReference<T>()
    spinUntil(duration, interval) {
        val value = block()
        if (value == null) {
            return@spinUntil false
        } else {
            ref.set(value)
            return@spinUntil true
        }
    }
    return ref.get()
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.lang.Language
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.codeStyle.CodeStyleManager
import org.commonmark.node.Node
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer

fun formatText(project: Project, language: Language, content: String): String {
    var result = content
    CommandProcessor.getInstance().runUndoTransparentAction {
        PsiFileFactory.getInstance(project)
            .createFileFromText("foo.bar", language, content, false, true)?.let {
                result = CodeStyleManager.getInstance(project).reformat(it).text
            }
    }

    return result
}

fun convertMarkdownToHTML(markdown: String): String {
    val parser: Parser = Parser.builder().build()
    val document: Node = parser.parse(markdown)
    val htmlRenderer: HtmlRenderer = HtmlRenderer.builder().build()
    return htmlRenderer.render(document)
}

/**
 * Designed to convert underscore separated words (e.g. UPDATE_COMPLETE) into title cased human readable text
 * (e.g. Update Complete)
 */
fun String.toHumanReadable() = StringUtil.toTitleCase(toLowerCase().replace('_', ' '))


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.util.ProgressIndicatorUtils
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.util.ExceptionUtil
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.concurrency.Semaphore
import java.time.Duration
import java.util.concurrent.TimeUnit

// There is a new/experimental API in IJ SDK, but replicate a simpler one here till we can use it
fun assertIsNonDispatchThread() {
    if (!ApplicationManager.getApplication().isDispatchThread) return
    throw RuntimeException("Access from event dispatch thread is not allowed.")
}

fun <T> runUnderProgressIfNeeded(project: Project?, title: String, cancelable: Boolean, task: () -> T): T =
    if (ApplicationManager.getApplication().isDispatchThread) {
        ProgressManager.getInstance().runProcessWithProgressSynchronously(ThrowableComputable { task.invoke() }, title, cancelable, project)
    } else {
        task.invoke()
    }

fun <T> computeOnEdt(modalityState: ModalityState = ModalityState.any(), supplier: () -> T): T {
    val application = ApplicationManager.getApplication()
    if (application.isDispatchThread) {
        return supplier.invoke()
    }
    val indicator = ProgressManager.getInstance().progressIndicator
    val semaphore = Semaphore(1)
    val result = Ref.create<T>()
    val error = Ref.create<Throwable>()
    val runnable = Runnable {
        try {
            if (indicator == null || !indicator.isCanceled) {
                result.set(supplier.invoke())
            }
        } catch (ex: Throwable) {
            error.set(ex)
        } finally {
            semaphore.up()
        }
    }

    ApplicationManager.getApplication().invokeLater(runnable, modalityState)

    ProgressIndicatorUtils.awaitWithCheckCanceled(semaphore, indicator)
    ExceptionUtil.rethrowAllAsUnchecked(error.get())

    return result.get()
}

fun sleepWithCancellation(sleepAmount: Duration, indicator: ProgressIndicator?) {
    val semaphore = Semaphore(1)
    val future = AppExecutorUtil.getAppScheduledExecutorService().schedule(
        { semaphore.up() },
        sleepAmount.toMillis(),
        TimeUnit.MILLISECONDS
    )
    try {
        ProgressIndicatorUtils.awaitWithCheckCanceled(semaphore, indicator)
    } finally {
        future.cancel(true)
    }
}


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

package software.aws.toolkits.jetbrains.utils

class YamlWriter internal constructor() {
    private val stringBuilder = StringBuilder()
    private var indentLevel = 0

    fun mapping(key: String, block: YamlWriter.() -> Unit) {
        appendIndent().append("$key:").append('\n')
        indentLevel++

        block()

        indentLevel--
    }

    fun keyValue(key: String, value: String) {
        appendIndent().append("$key: $value\n")
    }

    fun listValue(value: String) {
        appendIndent().append("- $value\n")
    }

    private fun appendIndent() = stringBuilder.append("  ".repeat(indentLevel))

    override fun toString() = stringBuilder.toString().trimEnd()
}

fun yaml(init: YamlWriter.() -> Unit): String {
    val yaml = YamlWriter()
    yaml.init()
    return yaml.toString()
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipFile
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermission

// TODO: Write tests
class ZipDecompressor(sourceBytes: ByteArray) : AutoCloseable {
    private val zipFile = ZipFile(SeekableInMemoryByteChannel(sourceBytes))
    private val zipEntries = zipFile.entries.toList()
    private val directorySplitRegex = Regex.fromLiteral("""[/\\]""")

    fun extract(destination: File) {
        zipEntries.forEach {
            val outputFile = outputFile(destination, it.name)
            // TODO: Handle symlink if we ever need it
            when {
                it.isDirectory -> FileUtil.createDirectory(outputFile)
                else -> createFile(outputFile, it)
            }
        }
    }

    private fun createFile(outputFile: File, zipEntry: ZipArchiveEntry) {
        zipFile.getInputStream(zipEntry).use { zipStream ->
            FileUtil.createParentDirs(outputFile)

            FileOutputStream(outputFile).use { outputStream ->
                zipStream.copyTo(outputStream)
            }

            if (SystemInfo.isUnix) {
                Files.setPosixFilePermissions(outputFile.toPath(), convertPermissions(zipEntry.unixMode))
            }
        }
    }

    private fun outputFile(outputDir: File, entryName: String): File {
        if (entryName.split(directorySplitRegex).contains("..")) {
            throw IOException("Entry name attempting to traverse up directory: $entryName")
        }

        return File(outputDir, entryName)
    }

    private fun convertPermissions(mode: Int): Set<PosixFilePermission> {
        val permissions = mutableSetOf<PosixFilePermission>()
        if ((mode and 400) > 0) {
            permissions.add(PosixFilePermission.OWNER_READ)
        }
        if ((mode and 200) > 0) {
            permissions.add(PosixFilePermission.OWNER_WRITE)
        }
        if ((mode and 100) > 0) {
            permissions.add(PosixFilePermission.OWNER_EXECUTE)
        }
        if ((mode and 40) > 0) {
            permissions.add(PosixFilePermission.GROUP_READ)
        }
        if ((mode and 20) > 0) {
            permissions.add(PosixFilePermission.GROUP_WRITE)
        }
        if ((mode and 10) > 0) {
            permissions.add(PosixFilePermission.GROUP_EXECUTE)
        }
        if ((mode and 4) > 0) {
            permissions.add(PosixFilePermission.OTHERS_READ)
        }
        if ((mode and 2) > 0) {
            permissions.add(PosixFilePermission.OTHERS_WRITE)
        }
        if ((mode and 1) > 0) {
            permissions.add(PosixFilePermission.OTHERS_EXECUTE)
        }
        return permissions
    }

    override fun close() {
        zipFile.close()
    }
}


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

package software.aws.toolkits.jetbrains.utils.actions

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware
import org.jetbrains.annotations.NotNull
import javax.swing.Icon

class OpenBrowserAction(title: String, icon: Icon? = null, private val url: String) : AnAction(title, null, icon), DumbAware {
    override fun actionPerformed(@NotNull e: AnActionEvent) {
        try {
            BrowserUtil.browse(url)
        } catch (_: Exception) {
            // ignore
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.build.BuildProgressListener
import com.intellij.build.BuildViewManager
import com.intellij.build.DefaultBuildDescriptor
import com.intellij.build.events.impl.FailureResultImpl
import com.intellij.build.events.impl.FinishBuildEventImpl
import com.intellij.build.events.impl.FinishEventImpl
import com.intellij.build.events.impl.OutputBuildEventImpl
import com.intellij.build.events.impl.SkippedResultImpl
import com.intellij.build.events.impl.StartBuildEventImpl
import com.intellij.build.events.impl.StartEventImpl
import com.intellij.build.events.impl.SuccessResultImpl
import com.intellij.openapi.components.service
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.Project
import com.intellij.util.ExceptionUtil
import software.aws.toolkits.resources.message

class BuildViewWorkflowEmitter private constructor(
    private val buildListener: BuildProgressListener,
    private val workflowTitle: String,
    private val workflowId: String
) : WorkflowEmitter {
    override fun createStepEmitter(): StepEmitter = BuildViewStepEmitter.createRoot(buildListener, workflowId)

    override fun workflowStarted() {
        val descriptor = DefaultBuildDescriptor(
            workflowId,
            workflowTitle,
            "/unused/working/directory",
            System.currentTimeMillis()
        ).apply {
            isActivateToolWindowWhenAdded = true
            withProcessHandler(processHandler, null)
        }

        buildListener.onEvent(workflowId, StartBuildEventImpl(descriptor, message("general.execution.running")))
    }

    override fun workflowFailed(e: Throwable) {
        val message = if (e is ProcessCanceledException) {
            message("general.execution.canceled")
        } else {
            message("general.execution.failed")
        }

        buildListener.onEvent(
            workflowId,
            FinishBuildEventImpl(
                workflowId,
                null,
                System.currentTimeMillis(),
                message,
                if (e is ProcessCanceledException) SkippedResultImpl() else FailureResultImpl()
            )
        )
    }

    override fun workflowCompleted() {
        buildListener.onEvent(
            workflowId,
            FinishBuildEventImpl(
                workflowId,
                null,
                System.currentTimeMillis(),
                message("general.execution.success"),
                SuccessResultImpl()
            )
        )
    }

    companion object {
        fun createEmitter(buildListener: BuildProgressListener, workflowTitle: String, workflowId: String) =
            BuildViewWorkflowEmitter(buildListener, workflowTitle, workflowId)

        fun createEmitter(project: Project, workflowTitle: String, workflowId: String) =
            BuildViewWorkflowEmitter(project.service<BuildViewManager>(), workflowTitle, workflowId)
    }
}

class BuildViewStepEmitter private constructor(
    private val buildListener: BuildProgressListener,
    private val rootObject: Any,
    private val parentId: String,
    private val stepName: String,
    private val hidden: Boolean,
    private val parent: StepEmitter?
) : StepEmitter {
    override fun createChildEmitter(stepName: String, hidden: Boolean): StepEmitter {
        val (childParent, childStepName) = if (hidden) {
            parentId to this.stepName
        } else {
            this.stepName to stepName
        }
        return BuildViewStepEmitter(buildListener, rootObject, childParent, childStepName, hidden, this)
    }

    override fun stepStarted() {
        if (hidden) return

        buildListener.onEvent(
            rootObject,
            StartEventImpl(
                stepName,
                parentId,
                System.currentTimeMillis(),
                stepName
            )
        )
    }

    override fun stepSkipped() {
        if (hidden) return

        buildListener.onEvent(
            rootObject,
            FinishEventImpl(
                stepName,
                parentId,
                System.currentTimeMillis(),
                stepName,
                SkippedResultImpl()
            )
        )
    }

    override fun stepFinishSuccessfully() {
        if (hidden) return

        buildListener.onEvent(
            rootObject,
            FinishEventImpl(
                stepName,
                parentId,
                System.currentTimeMillis(),
                stepName,
                SuccessResultImpl()
            )
        )
    }

    override fun stepFinishExceptionally(e: Throwable) {
        if (e is ProcessCanceledException) {
            emitMessage(message("general.step.canceled", stepName), true)
        } else {
            emitMessage(
                message(
                    "general.step.failed",
                    stepName,
                    ExceptionUtil.getNonEmptyMessage(e, ExceptionUtil.getThrowableText(e))
                ),
                true
            )
        }
        if (hidden) return

        buildListener.onEvent(
            rootObject,
            FinishEventImpl(
                stepName,
                parentId,
                System.currentTimeMillis(),
                stepName,
                if (e is ProcessCanceledException) SkippedResultImpl() else FailureResultImpl()
            )
        )
    }

    override fun emitMessage(message: String, isError: Boolean) {
        parent?.emitMessage(message, isError)
        if (hidden) return
        buildListener.onEvent(
            rootObject,
            OutputBuildEventImpl(
                stepName,
                message,
                !isError
            )
        )
    }

    companion object {
        // TODO: Decouple step name from the build ID
        fun createRoot(buildListener: BuildProgressListener, rootStepName: String): StepEmitter =
            BuildViewStepEmitter(
                buildListener,
                rootStepName,
                rootStepName,
                rootStepName,
                hidden = false,
                parent = null
            )
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessAdapter
import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.util.Key
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.Result
import java.time.Instant

abstract class CliBasedStep : Step() {
    protected abstract fun constructCommandLine(context: Context): GeneralCommandLine?
    protected open fun recordTelemetry(context: Context, startTime: Instant, result: Result) {}
    protected open fun onProcessStart(context: Context, processHandler: ProcessHandler) {}
    protected open fun createProcessEmitter(stepEmitter: StepEmitter): ProcessListener = CliOutputEmitter(stepEmitter)

    protected open fun handleSuccessResult(output: String, stepEmitter: StepEmitter, context: Context) {}

    /**
     * Processes the command's stdout and throws an exception after the CLI exits with failure.
     * @return null if the failure should be ignored. You're probably doing something wrong if you want this.
     */
    protected open fun handleErrorResult(exitCode: Int, output: String, stepEmitter: StepEmitter) {
        throw IllegalStateException(message("general.execution.cli_error", exitCode))
    }

    final override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        val startTime = Instant.now()
        var result = Result.Succeeded
        try {
            val commandLine = constructCommandLine(context)
            if (commandLine == null) {
                LOG.debug { "Command line not built, skipping step" }
                throw SkipStepException()
            }

            LOG.debug { "Built command line: ${commandLine.commandLineString}" }

            val processHandler = createProcessHandler(commandLine, context)
            val processCapture = CapturingProcessAdapter()
            processHandler.addProcessListener(processCapture)
            processHandler.addProcessListener(createProcessEmitter(stepEmitter))
            stepEmitter.attachProcess(processHandler)
            processHandler.startNotify()

            monitorProcess(processHandler, context, ignoreCancellation)

            if (!ignoreCancellation) {
                context.throwIfCancelled()
            }

            if (processHandler.exitCode == 0) {
                handleSuccessResult(processCapture.output.stdout, stepEmitter, context)
                return
            }

            handleErrorResult(processCapture.output.exitCode, processCapture.output.stdout, stepEmitter)
        } catch (e: SkipStepException) {
            LOG.debug(e) { """Step "$stepName" skipped!""" }
        } catch (e: ProcessCanceledException) {
            LOG.debug(e) { """Step "$stepName" cancelled!""" }
            result = Result.Cancelled
        } catch (e: Exception) {
            result = Result.Failed
            throw e
        } finally {
            recordTelemetry(context, startTime, result)
        }
    }

    protected open fun createProcessHandler(commandLine: GeneralCommandLine, context: Context): ProcessHandler =
        object : KillableColoredProcessHandler(commandLine) {
            override fun startNotify() {
                super.startNotify()
                onProcessStart(context, this)
            }

            override fun doDestroyProcess() {
                // send signal only if user explicitly requests termination
                if (this.getUserData(ProcessHandler.TERMINATION_REQUESTED) == true) {
                    super.doDestroyProcess()
                } else {
                    detachProcess()
                }
            }
        }

    protected class CliOutputEmitter(private val messageEmitter: StepEmitter, private val printStdOut: Boolean = true) : ProcessAdapter() {
        override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
            LOG.debug {
                val prefix = if (outputType == ProcessOutputTypes.STDERR) {
                    "[STDERR]"
                } else {
                    "[STDOUT]"
                }

                "$prefix ${event.text.trim()}"
            }

            if (outputType == ProcessOutputTypes.STDOUT) {
                if (printStdOut) {
                    messageEmitter.emitMessage(event.text, isError = false)
                }
            } else {
                messageEmitter.emitMessage(event.text, outputType == ProcessOutputTypes.STDERR)
            }
        }
    }

    private fun monitorProcess(processHandler: ProcessHandler, context: Context, ignoreCancellation: Boolean) {
        while (!processHandler.waitFor(WAIT_INTERVAL_MILLIS)) {
            if (!ignoreCancellation && context.isCancelled()) {
                if (!processHandler.isProcessTerminating && !processHandler.isProcessTerminated) {
                    processHandler.putUserData(ProcessHandler.TERMINATION_REQUESTED, true)
                    processHandler.destroyProcess()
                }
            }
        }
    }

    private companion object {
        const val WAIT_INTERVAL_MILLIS = 100L
        private val LOG = getLogger<CliBasedStep>()
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.Project
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
import software.aws.toolkits.core.utils.AttributeBag
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.tryOrNull
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Cross step context that exists for the life of a step workflow execution. Keeps track of the global execution state and allows passing data between steps.
 */
class Context {
    interface Listener {
        fun onCancel() {}
        fun onComplete() {}
    }

    val workflowToken = UUID.randomUUID().toString()
    private val attributeMap = AttributeBag()
    private val isCancelled = AtomicBoolean(false)
    private val isCompleted = AtomicBoolean(false)
    private val listeners = mutableListOf<Listener>()

    fun cancel() {
        if (isCompleted()) {
            return
        }

        isCancelled.set(true)
        isCompleted.set(true)

        listeners.forEach {
            LOG.tryOrNull("Context callback failed") {
                it.onCancel()
            }
        }
    }

    fun complete() {
        if (isCompleted()) {
            return
        }

        isCompleted.set(true)

        listeners.forEach {
            LOG.tryOrNull("Context callback failed") {
                it.onComplete()
            }
        }
    }

    fun addListener(listener: Listener) {
        listeners.add(listener)
    }

    fun isCancelled() = isCancelled.get()
    fun isCompleted() = isCompleted.get()

    fun throwIfCancelled() {
        if (isCancelled()) {
            throw ProcessCanceledException()
        }
    }

    fun <T : Any> getAttribute(key: AttributeBagKey<T>): T? = attributeMap.get(key)

    /**
     * Try to get attribute for timeout milliseconds, throws a kotlinx.coroutines.TimeoutCancellationException on failure
     * @param key The key to try to get
     * @param timeout The timeout in milliseconds
     */
    suspend fun <T : Any> pollingGet(key: AttributeBagKey<T>, timeout: Long = 10000): T = withTimeout(timeout) {
        while (!isCancelled()) {
            val item = attributeMap.get(key)
            if (item != null) {
                return@withTimeout item
            }
            delay(100)
        }
        throw CancellationException("getAttributeOrWait cancelled")
    }

    fun <T : Any> getRequiredAttribute(key: AttributeBagKey<T>): T = attributeMap.getOrThrow(key)

    fun <T : Any> putAttribute(key: AttributeBagKey<T>, data: T) {
        attributeMap.putData(key, data)
    }

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

        val PROJECT_ATTRIBUTE = AttributeBagKey.create<Project>("project")
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.execution.process.ProcessHandler

interface WorkflowEmitter {
    fun createStepEmitter(): StepEmitter
    fun workflowStarted() {}
    fun workflowCompleted() {}
    fun workflowFailed(e: Throwable) {}
}

interface StepEmitter {
    fun createChildEmitter(stepName: String, hidden: Boolean): StepEmitter
    fun stepStarted() {}
    fun stepSkipped() {}
    fun stepFinishSuccessfully() {}
    fun stepFinishExceptionally(e: Throwable) {}
    fun emitMessage(message: String, isError: Boolean) {}
    fun emitMessageLine(message: String, isError: Boolean) = emitMessage("$message\n", isError)
    fun attachProcess(handler: ProcessHandler) {}
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.openapi.application.ApplicationManager
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException

/**
 * [Step] that creates multiple child steps and runs them in parallel waiting on the result.
 *
 * It can optionally hide itself from the tree. If hidden, it acts as just a logical parent.
 * If shown, it shows itself as a parent node in the tree to its children.
 */
abstract class ParallelStep : Step() {
    private inner class ChildStep(val future: CompletableFuture<*>)

    private val listOfChildTasks = mutableListOf<ChildStep>()

    override val hidden = true

    protected abstract fun buildChildSteps(context: Context): List<Step>

    final override fun execute(
        context: Context,
        messageEmitter: StepEmitter,
        ignoreCancellation: Boolean
    ) {
        buildChildSteps(context).forEach {
            val stepFuture = CompletableFuture<Unit>()
            listOfChildTasks.add(ChildStep(stepFuture))

            ApplicationManager.getApplication().executeOnPooledThread {
                try {
                    it.run(context, messageEmitter, ignoreCancellation)
                    stepFuture.complete(null)
                } catch (e: Throwable) {
                    stepFuture.completeExceptionally(e)
                }
            }
        }

        try {
            CompletableFuture.allOf(*listOfChildTasks.map { it.future }.toTypedArray()).join()
        } catch (e: CompletionException) {
            throw e.cause ?: e
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.openapi.progress.ProcessCanceledException
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info

abstract class Step {
    public abstract val stepName: String
    protected open val hidden: Boolean = false

    fun run(context: Context, parentEmitter: StepEmitter, ignoreCancellation: Boolean = false) {
        if (!ignoreCancellation) {
            context.throwIfCancelled()
        }

        // If we are not hidden, we will create a new factory so that the parent node is correct, else pass the current factory so in effect
        // this node does not exist in the hierarchy
        val stepEmitter = parentEmitter.createChildEmitter(stepName, hidden)
        stepEmitter.stepStarted()
        try {
            execute(context, stepEmitter, ignoreCancellation)

            stepEmitter.stepFinishSuccessfully()
        } catch (e: SkipStepException) {
            stepEmitter.stepSkipped()
        } catch (e: Throwable) {
            LOG.info(e) { "Step $stepName failed" }
            stepEmitter.stepFinishExceptionally(e)
            throw e
        }
    }

    protected abstract fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean)

    /**
     * Exception used to abort out of a step and mark it as skipped. This may be used in the case that a step has to make a decision before it decides it
     * wants to run in the workflow. This differs from throwing a [ProcessCanceledException] which will terminate the workflow.
     */
    protected class SkipStepException : RuntimeException()

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


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutputType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.tryOrNull
import java.io.OutputStream

/**
 * Executor for a [StepWorkflow]
 */
class StepExecutor(
    project: Project?,
    private val workflow: StepWorkflow,
    private val messageEmitter: WorkflowEmitter
) {
    var onSuccess: ((Context) -> Unit)? = null
    var onError: ((Throwable) -> Unit)? = null

    private val context = Context()
    private val processHandler = StepExecutorProcessHandler(context)

    init {
        project?.let { context.putAttribute(Context.PROJECT_ATTRIBUTE, project) }
    }

    /**
     * Returns a pseudo process handler that coincides with the life cycle of the StepExecutors workflow
     *
     * Can also be used to give the ownership of the lifecycle over to a 3rd party such as a Run Config
     */
    fun getProcessHandler(): ProcessHandler = processHandler

    /**
     * Starts the workflow if not already started
     */
    fun startExecution(): ProcessHandler {
        if (!processHandler.isStartNotified) {
            processHandler.startNotify()
        }

        return processHandler
    }

    fun<T : Any> addContext(key: AttributeBagKey<T>, value: T) = context.putAttribute(key, value)

    private fun startWorkflow() {
        ApplicationManager.getApplication().executeOnPooledThread {
            execute(context, WorkflowEmitterWrapper(messageEmitter), processHandler)
        }
    }

    private fun execute(context: Context, messageEmitter: WorkflowEmitter, processHandler: StepExecutorProcessHandler) {
        try {
            executionStarted()
            workflow.run(context, messageEmitter.createStepEmitter())

            // If the dummy process was cancelled (or any step got cancelled), we need to rethrow the cancel
            context.throwIfCancelled()

            onSuccess?.invoke(context)
            context.complete()
            executionFinishedSuccessfully(processHandler)
        } catch (e: Throwable) {
            context.cancel()
            LOG.tryOrNull("Failed to invoke error callback") {
                onError?.invoke(e)
            }
            executionFinishedExceptionally(processHandler, e)
        }
    }

    private fun executionStarted() {
        LOG.tryOrNull("Failed to invoke error workflowStarted") {
            messageEmitter.workflowStarted()
        }
    }

    private fun executionFinishedSuccessfully(processHandler: StepExecutorProcessHandler) {
        LOG.tryOrNull("Failed to invoke error workflowCompleted") {
            messageEmitter.workflowCompleted()
        }

        processHandler.notifyProcessTerminated(0)
    }

    private fun executionFinishedExceptionally(processHandler: StepExecutorProcessHandler, e: Throwable) {
        LOG.tryOrNull("Failed to invoke error workflowFailed") {
            messageEmitter.workflowFailed(e)
        }

        processHandler.notifyProcessTerminated(1)
    }

    /**
     * Wraps the [WorkflowEmitter] so we can pass messages back to the [ProcessHandler]
     */
    private inner class WorkflowEmitterWrapper(private val delegate: WorkflowEmitter) : WorkflowEmitter by delegate {
        override fun createStepEmitter(): StepEmitter = StepEmitterWrapper(delegate.createStepEmitter())
    }

    /**
     * Wraps the [StepEmitter] so we can pass messages back to the [ProcessHandler]
     */
    private inner class StepEmitterWrapper(private val delegate: StepEmitter) : StepEmitter by delegate {
        override fun createChildEmitter(stepName: String, hidden: Boolean): StepEmitter = StepEmitterWrapper(delegate.createChildEmitter(stepName, hidden))

        override fun emitMessage(message: String, isError: Boolean) {
            if (ApplicationManager.getApplication().isUnitTestMode) {
                print(message)
            }

            delegate.emitMessage(message, isError)
            processHandler.notifyTextAvailable(message, if (isError) ProcessOutputType.STDERR else ProcessOutputType.STDOUT)
        }
    }

    /**
     * Fake process handle that acts as the "wrapper" for all steps. Any cancelled step will "kill" this process and vice versa.
     * For example, in a run config this process handle can be tied to the red Stop button thus stopping it cancels the step execution flow.
     */
    private inner class StepExecutorProcessHandler(private val context: Context) : ProcessHandler() {
        override fun startNotify() {
            startWorkflow()
            super.startNotify()
        }

        override fun getProcessInput(): OutputStream? = null

        override fun detachIsDefault() = false

        override fun detachProcessImpl() {
            destroyProcessImpl()
        }

        override fun destroyProcessImpl() {
            context.cancel()
        }

        public override fun notifyProcessTerminated(exitCode: Int) {
            super.notifyProcessTerminated(exitCode)
        }
    }

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


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

package software.aws.toolkits.jetbrains.utils.execution.steps

/**
 * This is the hidden step that is the root of the tree of [Step] in the workflow. The children [topLevelSteps] are ran sequentially.
 */
open class StepWorkflow(protected val topLevelSteps: List<Step>) : Step() {
    constructor(vararg topLevelSteps: Step) : this(topLevelSteps.toList())

    override val stepName = "StepWorkflow"
    override val hidden = true

    override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        topLevelSteps.forEach {
            it.run(context, stepEmitter)
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils.ui

import com.intellij.ui.SimpleColoredRenderer
import java.awt.BorderLayout
import java.awt.Component
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTable
import javax.swing.border.CompoundBorder
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.TableCellRenderer

abstract class ResizingColumnRenderer : TableCellRenderer, SimpleColoredRenderer() {
    private val defaultRenderer = DefaultTableCellRenderer()
    abstract fun getText(value: Any?): String?

    override fun getTableCellRendererComponent(table: JTable?, value: Any?, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component {
        // This wrapper will let us force the component to be at the top instead of in the middle for linewraps
        val wrapper = JPanel(BorderLayout())

        // this is basically what ColoredTableCellRenderer is doing, but we can't override getTableCellRendererComponent on that class
        cellState.updateRenderer(defaultRenderer)

        val defaultComponent = defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
        if (table == null) {
            return defaultComponent
        }
        val component = defaultComponent as? JLabel ?: return defaultComponent

        // This will set the component text accordingly
        component.text = getText(value)

        if (component.preferredSize.width > table.columnModel.getColumn(column).preferredWidth) {
            // add 3 pixels of padding. No padding makes it go into ... mode cutting off the end
            table.columnModel.getColumn(column).preferredWidth = component.preferredSize.width + 3
            table.columnModel.getColumn(column).maxWidth = component.preferredSize.width + 3
        }
        wrapper.add(component, BorderLayout.NORTH)
        // Make sure the background matches for selection
        wrapper.background = component.background
        // if a component is selected, it puts a border on it, move the border to the wrapper instead
        if (isSelected) {
            // this border has an outside and inside border, take only the outside border
            wrapper.border = (component.border as? CompoundBorder)?.outsideBorder
        }
        component.border = null
        return wrapper
    }
}


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

package software.aws.toolkits.jetbrains.utils.ui

import java.awt.event.AdjustmentEvent
import java.awt.event.AdjustmentListener
import javax.swing.JScrollBar
import javax.swing.JScrollPane

fun JScrollPane.topReached(block: () -> Unit) {
    verticalScrollBar.addAdjustmentListener(
        object : AdjustmentListener {
            var lastAdjustment = verticalScrollBar.minimum
            override fun adjustmentValueChanged(e: AdjustmentEvent?) {
                if (e == null || e.value == lastAdjustment) {
                    return
                }
                lastAdjustment = e.value
                if (verticalScrollBar.isAtTop()) {
                    block()
                }
            }
        }
    )
}

fun JScrollPane.bottomReached(block: () -> Unit) {
    verticalScrollBar.addAdjustmentListener(
        object : AdjustmentListener {
            var lastAdjustment = verticalScrollBar.minimum
            override fun adjustmentValueChanged(e: AdjustmentEvent?) {
                if (e == null || e.value == lastAdjustment) {
                    return
                }
                lastAdjustment = e.value
                if (verticalScrollBar.isAtBottom()) {
                    block()
                }
            }
        }
    )
}

private fun JScrollBar.isAtBottom(): Boolean = value == (maximum - visibleAmount)
private fun JScrollBar.isAtTop(): Boolean = value == minimum


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

package software.aws.toolkits.jetbrains.utils.ui

import com.intellij.ui.SearchTextField
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener

fun SearchTextField.onEmpty(block: () -> Unit) {
    textEditor.addPropertyChangeListener(
        object : PropertyChangeListener {
            private var lastText = ""
            override fun propertyChange(evt: PropertyChangeEvent?) {
                val searchFieldText = text.trim()
                if (searchFieldText == lastText) {
                    return
                }
                lastText = searchFieldText
                if (text.isEmpty()) {
                    block()
                }
            }
        }
    )
}

fun SearchTextField.onEnter(block: () -> Unit) {
    textEditor.addActionListener(
        object : ActionListener {
            private var lastText = ""
            override fun actionPerformed(e: ActionEvent?) {
                val searchFieldText = text.trim()
                if (searchFieldText == lastText) {
                    return
                }
                lastText = searchFieldText
                block()
            }
        }
    )
}


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

package software.aws.toolkits.jetbrains.utils.ui

import com.intellij.icons.AllIcons
import com.intellij.ide.HelpTooltip
import com.intellij.lang.Language
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.GraphicsConfig
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.CellRendererPanel
import com.intellij.ui.ClickListener
import com.intellij.ui.EditorTextField
import com.intellij.ui.JBColor
import com.intellij.ui.JreHiDpiUtil
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBRadioButton
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.dsl.builder.MutableProperty
import com.intellij.ui.layout.Cell
import com.intellij.ui.layout.CellBuilder
import com.intellij.ui.layout.ComponentPredicate
import com.intellij.ui.layout.PropertyBinding
import com.intellij.ui.layout.Row
import com.intellij.ui.layout.applyToComponent
import com.intellij.ui.layout.toBinding
import com.intellij.ui.layout.withSelectedBinding
import com.intellij.ui.paint.LinePainter2D
import com.intellij.ui.speedSearch.SpeedSearchSupply
import com.intellij.util.text.DateFormatUtil
import com.intellij.util.text.SyncDateFormat
import com.intellij.util.ui.GraphicsUtil
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import org.jetbrains.annotations.Nls
import software.aws.toolkits.jetbrains.ui.KeyValueTextField
import software.aws.toolkits.jetbrains.utils.formatText
import java.awt.AlphaComposite
import java.awt.Color
import java.awt.Component
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Shape
import java.awt.event.MouseEvent
import java.awt.geom.RoundRectangle2D
import java.text.SimpleDateFormat
import javax.swing.AbstractButton
import javax.swing.JButton
import javax.swing.JComboBox
import javax.swing.JComponent
import javax.swing.JTable
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.ListModel
import javax.swing.table.TableCellRenderer
import javax.swing.text.Highlighter
import javax.swing.text.JTextComponent
import kotlin.reflect.KMutableProperty0
import com.intellij.ui.dsl.builder.Cell as Cell2

fun JTextField?.blankAsNull(): String? = if (this?.text?.isNotBlank() == true) {
    text
} else {
    null
}

@Suppress("UNCHECKED_CAST")
fun <T> JComboBox<T>?.selected(): T? = this?.selectedItem as? T

fun EditorTextField.formatAndSet(content: String, language: Language) {
    CommandProcessor.getInstance().runUndoTransparentAction {
        val formatted = formatText(this.project, language, content)
        runWriteAction {
            document.setText(formatted)
        }
    }
}

/**
 * Allows triggering [button] selection based on clicking on receiver component
 */
@JvmOverloads
fun JComponent.addQuickSelect(button: AbstractButton, postAction: Runnable? = null) {
    object : ClickListener() {
        override fun onClick(event: MouseEvent, clickCount: Int): Boolean {
            if (button.isSelected) {
                return false
            }
            button.isSelected = true
            postAction?.run()
            return true
        }
    }.installOn(this)
}

fun <T> ListModel<T>.find(predicate: (T) -> Boolean): T? {
    for (i in 0 until size) {
        val element = getElementAt(i)?.takeIf(predicate)
        if (element != null) {
            return element
        }
    }
    return null
}

// Error messages do not work on a disabled component. May be a JetBrains bug?
fun JComponent.validationInfo(message: String) = when {
    isEnabled -> ValidationInfo(message, this)
    else -> ValidationInfo(message)
}

val BETTER_GREEN = JBColor(Color(104, 197, 116), JBColor.GREEN.darker())

/**
 * Fork of JetBrain's intellij-community UIUtils.drawSearchMatch allowing us to highlight multiple a multi-line
 * text field (startY was added and used instead of constants). Also auto-converted to Kotlin by Intellij
 */
fun drawSearchMatch(
    g: Graphics2D,
    startX: Float,
    endX: Float,
    startY: Float,
    height: Int
) {
    val color1 = JBColor.namedColor("SearchMatch.startBackground", JBColor.namedColor("SearchMatch.startColor", 0xffeaa2))
    val color2 = JBColor.namedColor("SearchMatch.endBackground", JBColor.namedColor("SearchMatch.endColor", 0xffd042))
    drawSearchMatch(g, startX, endX, startY, height, color1, color2)
}

fun drawSearchMatch(graphics2D: Graphics2D, startXf: Float, endXf: Float, startY: Float, height: Int, gradientStart: Color, gradientEnd: Color) {
    val config = GraphicsConfig(graphics2D)
    var alpha = JBUI.getInt("SearchMatch.transparency", 70) / 100f
    alpha = if (alpha < 0 || alpha > 1) 0.7f else alpha
    graphics2D.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)
    graphics2D.paint = UIUtil.getGradientPaint(startXf, startY + 2f, gradientStart, startXf, startY - height - 5.toFloat(), gradientEnd)
    if (JreHiDpiUtil.isJreHiDPI(graphics2D)) {
        val c = GraphicsUtil.setupRoundedBorderAntialiasing(graphics2D)
        graphics2D.fill(RoundRectangle2D.Float(startXf, startY + 2, endXf - startXf, (height - 4).toFloat(), 5f, 5f))
        c.restore()
        config.restore()
        return
    }
    val startX = startXf.toInt()
    val endX = endXf.toInt()
    graphics2D.fillRect(startX, startY.toInt() + 3, endX - startX, height - 5)
    val drawRound = endXf - startXf > 4
    if (drawRound) {
        LinePainter2D.paint(graphics2D, startX - 1.toDouble(), startY + 4.0, startX - 1.toDouble(), startY + height - 4.toDouble())
        LinePainter2D.paint(graphics2D, endX.toDouble(), startY + 4.0, endX.toDouble(), startY + height - 4.toDouble())
        graphics2D.color = Color(100, 100, 100, 50)
        LinePainter2D.paint(graphics2D, startX - 1.toDouble(), startY + 4.0, startX - 1.toDouble(), startY + height - 4.toDouble())
        LinePainter2D.paint(graphics2D, endX.toDouble(), startY + 4.0, endX.toDouble(), startY + height - 4.toDouble())
        LinePainter2D.paint(graphics2D, startX.toDouble(), startY + 3.0, endX - 1.toDouble(), startY + 3.0)
        LinePainter2D.paint(graphics2D, startX.toDouble(), startY + height - 3.toDouble(), endX - 1.toDouble(), startY + height - 3.toDouble())
    }
    config.restore()
}

fun Component.setSelectionHighlighting(table: JTable, isSelected: Boolean) {
    if (isSelected) {
        foreground = table.selectionForeground
        background = table.selectionBackground
    } else {
        foreground = table.foreground
        background = table.background
    }
}

private class SpeedSearchHighlighter : Highlighter.HighlightPainter {
    override fun paint(g: Graphics?, startingPoint: Int, endingPoint: Int, bounds: Shape?, component: JTextComponent?) {
        component ?: return
        val graphics = g as? Graphics2D ?: return
        val beginningRect = component.modelToView(startingPoint)
        val endingRect = component.modelToView(endingPoint)
        drawSearchMatch(graphics, beginningRect.x.toFloat(), endingRect.x.toFloat(), beginningRect.y.toFloat(), beginningRect.height)
    }
}

private fun JTextArea.speedSearchHighlighter(speedSearchEnabledComponent: JComponent) {
    // matchingFragments does work with wrapped text but not around words if they are wrapped, so it will also need to be extended
    // in the future
    val speedSearch = SpeedSearchSupply.getSupply(speedSearchEnabledComponent) ?: return
    val fragments = speedSearch.matchingFragments(text)?.iterator() ?: return
    fragments.forEach {
        highlighter?.addHighlight(it.startOffset, it.endOffset, SpeedSearchHighlighter())
    }
}

class WrappingCellRenderer(
    private val wrapOnSelection: Boolean = false,
    private val wrapOnToggle: Boolean = false,
    private val truncateAfterChars: Int? = null
) : CellRendererPanel(), TableCellRenderer {
    var wrap: Boolean = false

    private val textArea = JBTextArea()

    init {
        textArea.font = UIUtil.getLabelFont()
        textArea.wrapStyleWord = true

        add(textArea)
    }

    override fun getTableCellRendererComponent(table: JTable?, value: Any?, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component {
        if (table == null) {
            return this
        }

        textArea.lineWrap = (wrapOnSelection && isSelected) || (wrapOnToggle && wrap)
        val text = (value as? String) ?: ""
        textArea.text = if (truncateAfterChars != null) {
            text.take(truncateAfterChars)
        } else {
            text
        }
        textArea.setSelectionHighlighting(table, isSelected)

        setSize(table.columnModel.getColumn(column).width, preferredSize.height)
        if (table.getRowHeight(row) != preferredSize.height) {
            table.setRowHeight(row, preferredSize.height)
        }

        textArea.speedSearchHighlighter(table)

        return this
    }
}

// TODO: figure out why this has weird hysteresis during rendering causing no text
class ResizingDateColumnRenderer(showSeconds: Boolean) : ResizingColumnRenderer() {
    private val formatter: SyncDateFormat = if (showSeconds) {
        SyncDateFormat(SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"))
    } else {
        DateFormatUtil.getDateTimeFormat()
    }

    override fun getText(value: Any?): String? = (value as? String)?.toLongOrNull()?.let { formatter.format(it) }
}

class ResizingTextColumnRenderer : ResizingColumnRenderer() {
    override fun getText(value: Any?): String? = (value as? String)?.trim()
}

/**
 * When a panel is made of more than one panel, apply and the validation callbacks do not work as expected for
 * the child panels. This function makes it so the validation callbacks and apply are actually called.
 * @param applies An additional function that allows control based on visibility of other components or other factors
 */
fun CellBuilder<DialogPanel>.installOnParent(applies: () -> Boolean = { true }): CellBuilder<DialogPanel> {
    withValidationOnApply {
        validate(applies, it)
    }
    return this
}

fun Cell2<DialogPanel>.installOnParent(applies: () -> Boolean = { true }): Cell2<DialogPanel> {
    validationOnApply {
        validate(applies, it)
    }
    return this
}

private inline fun validate(applies: () -> Boolean, component: DialogPanel): ValidationInfo? =
    if (!applies()) {
        null
    } else {
        val errors = component.validateCallbacks.mapNotNull { it() }
        if (errors.isEmpty()) {
            component.apply()
        }
        errors.firstOrNull()
    }

// backport since removed in 223
fun <T> CellBuilder<JBRadioButton>.bindValueToProperty(prop: PropertyBinding<T>, value: T): CellBuilder<JBRadioButton> = apply {
    component.isSelected = prop.get() == value
    onApply { if (component.isSelected) prop.set(value) }
    onReset { component.isSelected = prop.get() == value }
    onIsModified { component.isSelected != (prop.get() == value) }
}

fun Row.visibleIf(predicate: ComponentPredicate): Row {
    visible = predicate()
    predicate.addListener { visible = it }
    return this
}

/**
 * Add a contextual help icon component
 */
fun Cell.contextualHelp(description: String): CellBuilder<JBLabel> {
    val l = JBLabel(AllIcons.General.ContextHelp)
    HelpTooltip().apply {
        setDescription(description)
        installOn(l)
    }
    return component(l)
}

fun CellBuilder<KeyValueTextField>.withBinding(binding: PropertyBinding<Map<String, String>>) =
    this.withBinding(
        componentGet = { component -> component.envVars },
        componentSet = { component, value -> component.envVars = value },
        binding
    )

fun com.intellij.ui.dsl.builder.Cell<KeyValueTextField>.withBinding(binding: MutableProperty<Map<String, String>>) =
    this.bind(
        componentGet = { component -> component.envVars },
        componentSet = { component, value -> component.envVars = value },
        binding
    )

fun Row.keyValueTextField(title: String? = null, property: KMutableProperty0<Map<String, String>>): CellBuilder<KeyValueTextField> {
    val field = if (title == null) {
        KeyValueTextField()
    } else {
        KeyValueTextField(title)
    }

    return component(field)
        .withBinding(property.toBinding())
}

fun <T : JComponent> CellBuilder<T>.toolTipText(@Nls text: String): CellBuilder<T> {
    applyToComponent {
        toolTipText = text
    }

    return this
}

fun JButton.setEnabledAndVisible(state: Boolean) {
    isEnabled = state
    isVisible = state
}

fun<T> CellBuilder<JBRadioButton>.enumBinding(property: KMutableProperty0<T>, buttonValue: T) = this
    .withSelectedBinding(
        PropertyBinding(
            get = { property.get() == buttonValue },
            set = { if (it) property.set(buttonValue) }
        )
    )
    .applyToComponent {
        isSelected = property.get() == buttonValue
    }


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

package software.aws.toolkits.jetbrains.core

typealias CollaborationToolsUIUtil = com.intellij.collaboration.ui.CollaborationToolsUIUtil


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

package software.aws.toolkits.jetbrains.core.docker

import com.intellij.docker.DockerCloudConfiguration
import com.intellij.docker.DockerCloudType
import com.intellij.docker.DockerServerRuntimeInstance
import com.intellij.docker.agent.DockerAgent
import com.intellij.docker.registry.DockerRepositoryModel
import com.intellij.docker.runtimes.DockerImageRuntime
import com.intellij.openapi.project.Project
import com.intellij.remoteServer.configuration.RemoteServer
import com.intellij.remoteServer.configuration.deployment.DeploymentConfiguration
import com.intellij.remoteServer.impl.configuration.RemoteServerImpl
import com.intellij.remoteServer.runtime.ServerConnectionManager
import com.intellij.remoteServer.runtime.ServerConnector
import com.intellij.remoteServer.runtime.deployment.ServerRuntimeInstance
import kotlinx.coroutines.future.await
import kotlinx.coroutines.withContext
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.await
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import java.util.concurrent.CompletableFuture

private fun defaultDockerConnection() = ServerConnectionManager.getInstance().createTemporaryConnection(
    RemoteServerImpl("DockerConnection", DockerCloudType.getInstance(), DockerCloudConfiguration.createDefault())
)

suspend fun getDockerServerRuntimeFacade(project: Project, server: RemoteServer<DockerCloudConfiguration>? = null): DockerRuntimeFacade {
    val instancePromise = AsyncPromise<DockerServerRuntimeInstance>()
    val connection = server?.let {
        withContext(getCoroutineUiContext()) {
            ServerConnectionManager.getInstance().getOrCreateConnection(server)
        }
    } ?: defaultDockerConnection()

    connection.connectIfNeeded(object : ServerConnector.ConnectionCallback<DeploymentConfiguration> {
        override fun errorOccurred(errorMessage: String) {
            instancePromise.setError(errorMessage)
        }

        override fun connected(serverRuntimeInstance: ServerRuntimeInstance<DeploymentConfiguration>) {
            instancePromise.setResult(serverRuntimeInstance as DockerServerRuntimeInstance)
        }
    })

    val runtime = instancePromise.await()
    return object : DockerRuntimeFacade {
        override val agent: DockerAgent
            get() = runtime.agent

        override suspend fun pushImage(imageId: String, config: DockerRepositoryModel): CompletableFuture<Unit> {
            val physicalLocalRuntime = runtime.findRuntimeLater(imageId, false).await()
            (physicalLocalRuntime as? DockerImageRuntime)?.pushImage(project, config) ?: error { "couldn't map tag to appropriate docker runtime" }

            return CompletableFuture.completedFuture(Unit)
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.docker.compatability

typealias DockerRegistry = com.intellij.docker.registry.DockerRegistryConfiguration


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

package software.aws.toolkits.jetbrains.core.terminal

import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.terminal.JBTerminalWidget
import org.jetbrains.plugins.terminal.LocalTerminalDirectRunner

class AwsLocalTerminalRunner(
    project: Project,
    private val termName: String,
    private val applyConnection: (MutableMap<String, String>) -> Unit
) : LocalTerminalDirectRunner(project) {
    override fun getInitialCommand(envs: MutableMap<String, String>): MutableList<String> = super.getInitialCommand(envs.apply(applyConnection))
    override fun createTerminalWidget(parent: Disposable, currentWorkingDirectory: String?, deferSessionStartUntilUiShown: Boolean): JBTerminalWidget {
        val widget = super.createTerminalWidget(parent, currentWorkingDirectory, deferSessionStartUntilUiShown)
        return widget.apply {
            terminalTitle.change {
                tag = termName
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.caws

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.amazon.awssdk.services.codecatalyst.model.InstanceType

private val descriptions: ParameterDescriptions by lazy {
    ParameterDescriptions::class.java.getResourceAsStream("parameterDescriptions.json")?.use {
        jacksonObjectMapper()
            .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).readValue(it)
    } ?: throw IllegalStateException("Failed to locate parameterDescriptions.json")
}

fun loadParameterDescriptions(): ParameterDescriptions = descriptions

data class ParameterDescriptions(
    @JsonProperty("environment")
    val environmentParameters: EnvironmentParameters
)

data class EnvironmentParameters(
    @JsonProperty("instanceType")
    val instanceTypes: Map<InstanceType, InstanceInfo>,
    val persistentStorageSize: List<Int>
)

data class InstanceInfo(
    @JsonProperty("vcpus")
    val vCpus: Int,
    val ram: Ram,
    val arch: String
)

data class Ram(
    val value: Int,
    val unit: String
)

fun isSubscriptionFreeTier(
    client: CodeCatalystClient,
    space: String?
): Boolean {
    val subscriptionTier = if (space != null) {
        client.getSubscription {
            it.spaceName(space)
        }.subscriptionType()
    } else {
        return true
    }

    return subscriptionTier == "FREE"
}

fun InstanceType.isSupportedInFreeTier() =
    when (this) {
        InstanceType.DEV_STANDARD1_SMALL -> true
        else -> false
    }

fun Int.isSupportedInFreeTier() =
    when (this) {
        16 -> true
        else -> false
    }


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

package software.aws.toolkits.jetbrains.services.caws

import software.aws.toolkits.resources.message
import java.time.Duration

data class InactivityTimeout(val duration: Duration) : Comparable<InactivityTimeout> {
    fun displayText() = if (duration.isZero) {
        message("caws.workspace.details.no_timeout")
    } else if (duration.toMinutesPart() == 0) {
        message("date.in.n.hours", duration.toHours())
    } else {
        message("date.in.n.minutes", duration.toMinutes())
    }

    fun asMinutes() = duration.toMinutes().toInt()

    override fun compareTo(other: InactivityTimeout): Int = duration.compareTo(other.duration)

    companion object {
        val DEFAULT_TIMEOUT = InactivityTimeout(Duration.ofMinutes(15))
        val DEFAULT_VALUES = arrayOf(
            InactivityTimeout(Duration.ofMinutes(0)),
            DEFAULT_TIMEOUT,
            InactivityTimeout(Duration.ofMinutes(30)),
            InactivityTimeout(Duration.ofMinutes(45)),
            InactivityTimeout(Duration.ofHours(1)),
            InactivityTimeout(Duration.ofHours(2)),
            InactivityTimeout(Duration.ofHours(4)),
            InactivityTimeout(Duration.ofHours(8))
        )

        fun fromMinutes(minutes: Int) = InactivityTimeout(Duration.ofMinutes(minutes.toLong()))
    }
}


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

package software.aws.toolkits.jetbrains.core.docker

import com.intellij.docker.DockerCloudConfiguration
import com.intellij.docker.DockerCloudType
import com.intellij.docker.DockerServerRuntimesManager
import com.intellij.docker.agent.DockerAgent
import com.intellij.docker.registry.DockerRepositoryModel
import com.intellij.openapi.project.Project
import com.intellij.remoteServer.configuration.RemoteServer
import com.intellij.remoteServer.impl.configuration.RemoteServerImpl
import kotlinx.coroutines.future.await
import java.util.concurrent.CompletableFuture

private fun defaultDockerConnection() = RemoteServerImpl("DockerConnection", DockerCloudType.getInstance(), DockerCloudConfiguration.createDefault())

suspend fun getDockerServerRuntimeFacade(project: Project, server: RemoteServer<DockerCloudConfiguration>? = null): DockerRuntimeFacade {
    val connectionConfig = server ?: defaultDockerConnection()
    val runtime = DockerServerRuntimesManager.getInstance(project).getOrCreateConnection(connectionConfig).await()

    return object : DockerRuntimeFacade {
        override val agent: DockerAgent
            get() = runtime.agent

        override suspend fun pushImage(imageId: String, config: DockerRepositoryModel): CompletableFuture<Unit> {
            val imageRuntime = runtime.runtimesManager.images[imageId]
            return imageRuntime?.push(project, config) ?: error("couldn't map tag to appropriate docker runtime")
        }
    }
}