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

package software.aws.toolkits.jetbrains.services.cloudformation

import software.aws.toolkits.jetbrains.services.cloudformation.IndexedResource.Companion.from
import software.aws.toolkits.resources.message
import java.io.DataInput
import java.io.DataOutput

/**
 * Immutable data class for indexing [Resource]. Use [from] to create an instance so that it always
 * returns a concrete [IndexedResource] such as [IndexedFunction] if applicable.
 */
open class IndexedResource protected constructor(val type: String, val indexedProperties: Map<String, String>, val indexedMetadata: Map<String, String>) {

    protected constructor(resource: Resource, indexProperties: List<String>, indexMetadata: List<String>) :
        this(
            resource.type() ?: throw RuntimeException(message("cloudformation.template_index.missing_type")),
            indexProperties.mapScalars(resource, Resource::getScalarProperty),
            indexMetadata.mapScalars(resource, Resource::getScalarMetadata)
        )

    fun save(dataOutput: DataOutput) {
        dataOutput.writeUTF(type)
        dataOutput.writeInt(indexedProperties.size)
        indexedProperties.forEach { (key, value) ->
            dataOutput.writeUTF(key)
            dataOutput.writeUTF(value)
        }
        dataOutput.writeInt(indexedMetadata.size)
        indexedMetadata.forEach { (key, value) ->
            dataOutput.writeUTF(key)
            dataOutput.writeUTF(value)
        }
    }

    override fun equals(other: Any?): Boolean = this === other || (other as? IndexedResource)?.indexedProperties == indexedProperties

    override fun hashCode(): Int = indexedProperties.hashCode()

    companion object {
        private fun List<String>.mapScalars(resource: Resource, fn: Resource.(property: String) -> String): Map<String, String> =
            this
                .asSequence()
                .map {
                    it to try {
                        resource.fn(it)
                    } catch (e: Exception) {
                        null
                    }
                }
                .mapNotNull { (key, value) -> value?.let { key to it } }
                .toMap()

        fun read(dataInput: DataInput): IndexedResource {
            val propertyList: MutableMap<String, String> = mutableMapOf()
            val metadataList: MutableMap<String, String> = mutableMapOf()

            val type = dataInput.readUTF()
            val propertySize = dataInput.readInt()
            repeat(propertySize) {
                val key = dataInput.readUTF()
                val value = dataInput.readUTF()
                propertyList[key] = value
            }
            val metadataSize = dataInput.readInt()
            repeat(metadataSize) {
                val key = dataInput.readUTF()
                val value = dataInput.readUTF()
                metadataList[key] = value
            }
            return from(type, propertyList, metadataList)
        }

        fun from(type: String, indexedProperties: Map<String, String>, indexedMetadata: Map<String, String>) =
            INDEXED_RESOURCE_MAPPINGS[type]?.first?.invoke(type, indexedProperties, indexedMetadata)
                ?: IndexedResource(type, indexedProperties, indexedMetadata)

        fun from(resource: Resource): IndexedResource? = resource.type()?.let {
            INDEXED_RESOURCE_MAPPINGS[it]?.second?.invoke(resource) ?: IndexedResource(resource, listOf(), listOf())
        }
    }
}

class IndexedFunction : IndexedResource {

    internal constructor(type: String, indexedProperties: Map<String, String>, indexedMetadata: Map<String, String>) :
        super(type, indexedProperties, indexedMetadata)

    internal constructor(resource: Resource) : super(resource, listOf("Runtime", "Handler", "PackageType"), listOf("BuildMethod"))

    fun runtime(): String? = indexedProperties["Runtime"]

    fun handler(): String? = indexedProperties["Handler"]

    fun packageType(): String? = indexedProperties["PackageType"]

    fun buildMethod(): String? = indexedMetadata["BuildMethod"]

    override fun toString(): String = "IndexedFunction(indexedProperties=$indexedProperties,indexedMetadata=$indexedMetadata)"
}

internal val INDEXED_RESOURCE_MAPPINGS = mapOf<
    String,
    Pair<
        (String, Map<String, String>, Map<String, String>) -> IndexedResource,
        (Resource) -> IndexedResource
        >
    >(
    LAMBDA_FUNCTION_TYPE to Pair(::IndexedFunction, ::IndexedFunction),
    SERVERLESS_FUNCTION_TYPE to Pair(::IndexedFunction, ::IndexedFunction)
)


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

package software.aws.toolkits.jetbrains.services.cloudformation

import software.amazon.awssdk.services.lambda.model.PackageType
import software.aws.toolkits.jetbrains.services.cloudformation.yaml.YamlCloudFormationTemplate.Companion.getTextValues
import software.aws.toolkits.resources.message

interface Function : Resource {
    fun codeLocation(): String
    fun setCodeLocation(location: String)
    fun packageType(): PackageType {
        val key = "PackageType"
        val type = getOptionalScalarProperty(key) ?: return PackageType.ZIP
        return PackageType.values().firstOrNull { it.toString() == type } ?: throw IllegalStateException(message("cloudformation.invalid_property", key, type))
    }

    fun runtime(): String = getScalarProperty("Runtime")
    fun handler(): String = getScalarProperty("Handler")
    fun architectures(): List<String>? = getOptionalSequenceProperty("Architectures")?.getTextValues()
    fun timeout(): Int? = getOptionalScalarProperty("Timeout")?.toInt()
    fun memorySize(): Int? = getOptionalScalarProperty("MemorySize")?.toInt()
}

const val LAMBDA_FUNCTION_TYPE = "AWS::Lambda::Function"

class LambdaFunction(private val delegate: Resource) : Resource by delegate, Function {
    override fun setCodeLocation(location: String) {
        setScalarProperty("Code", location)
    }

    override fun codeLocation(): String = getScalarProperty("Code")

    override fun toString(): String = logicalName
}

const val SERVERLESS_FUNCTION_TYPE = "AWS::Serverless::Function"

class SamFunction(private val delegate: Resource) : Resource by delegate, Function {
    private val globals = cloudFormationTemplate.globals()

    override fun getScalarProperty(key: String): String = getOptionalScalarProperty(key)
        ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))

    override fun getOptionalScalarProperty(key: String): String? =
        delegate.getOptionalScalarProperty(key) ?: globals["Function"]?.getOptionalScalarProperty(key)

    override fun setCodeLocation(location: String) {
        setScalarProperty("CodeUri", location)
    }

    override fun codeLocation(): String = when (packageType()) {
        PackageType.ZIP -> getScalarProperty("CodeUri")
        PackageType.IMAGE -> getScalarMetadata("DockerContext")
        else -> throw IllegalStateException("Bad packageType somehow returned to code location: ${packageType()}")
    }

    fun dockerFile(): String? = getOptionalScalarMetadata("Dockerfile")

    override fun toString(): String = logicalName
}

internal val RESOURCE_MAPPINGS = mapOf<String, (Resource) -> Resource>(
    LAMBDA_FUNCTION_TYPE to ::LambdaFunction,
    SERVERLESS_FUNCTION_TYPE to ::SamFunction
)


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

package software.aws.toolkits.jetbrains.services.cloudformation.actions

import com.intellij.openapi.application.runInEdt
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.DeleteResourceAction
import software.aws.toolkits.jetbrains.core.explorer.refreshAwsTree
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationStackNode
import software.aws.toolkits.jetbrains.services.cloudformation.resources.CloudFormationResources
import software.aws.toolkits.jetbrains.services.cloudformation.stack.StackWindowManager
import software.aws.toolkits.jetbrains.services.cloudformation.waitForStackDeletionComplete
import software.aws.toolkits.resources.message

class DeleteStackAction : DeleteResourceAction<CloudFormationStackNode>(message("cloudformation.stack.delete.action")) {
    override fun performDelete(selected: CloudFormationStackNode) {
        val client: CloudFormationClient = selected.nodeProject.awsClient()
        client.deleteStack { it.stackName(selected.stackName) }
        runInEdt {
            StackWindowManager.getInstance(selected.nodeProject).openStack(selected.stackName, selected.stackId)
        }
        client.waitForStackDeletionComplete(selected.stackName)
        selected.nodeProject.refreshAwsTree(CloudFormationResources.ACTIVE_STACKS)
    }
}


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

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

import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import software.amazon.awssdk.services.cloudformation.model.StackSummary
import software.aws.toolkits.jetbrains.core.ClientBackedCachedResource
import software.aws.toolkits.jetbrains.core.Resource

object CloudFormationResources {
    @JvmField
    // "Active" stacks means everything that is not deleted
    val ACTIVE_STACKS: Resource.Cached<List<StackSummary>> = ClientBackedCachedResource(CloudFormationClient::class, "cloudformation.list_active_stacks") {
        listStacksPaginator {
            it.stackStatusFilters(
                // We want all values except for DELETE_COMPLETE
                StackStatus.knownValues().toList().filter { status -> status != StackStatus.DELETE_COMPLETE }
            )
        }.stackSummaries().toList().filter { it.stackName() != null }
    }
}


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

import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.table.JBTable
import java.awt.event.MouseListener
import javax.swing.JComponent
import javax.swing.SwingUtilities
import javax.swing.table.DefaultTableModel
import javax.swing.table.TableCellRenderer

class DynamicTableView<T>(private vararg val fields: Field<T>) : View {
    private val model = object : DefaultTableModel(fields.map(Field<T>::readableName).toTypedArray(), 0) {
        override fun isCellEditable(row: Int, column: Int) = false
    }

    private val table = JBTable(model).apply {
        autoCreateRowSorter = true
        autoscrolls = true
        cellSelectionEnabled = true
        setShowColumns(true)
        setPaintBusy(true)
        fields.forEach { field ->
            field.renderer?.let {
                getColumn(field.readableName).cellRenderer = it
            }
        }
    }

    override val component: JComponent = JBScrollPane(table)

    fun updateItems(items: List<T>, clearExisting: Boolean = false) {
        assert(SwingUtilities.isEventDispatchThread())
        if (clearExisting) {
            model.rowCount = 0
        }
        items.forEachIndexed { index, item ->
            model.insertRow(index, fields.map { it.getData(item) }.toTypedArray())
        }
        table.setPaintBusy(false)
    }

    fun showBusy(busy: Boolean = true) {
        table.setPaintBusy(busy)
    }

    fun addMouseListener(listener: MouseListener) = table.addMouseListener(listener)

    fun selectedRow(): Map<Field<T>, Any?>? {
        val row = table.selectedRows?.takeIf { it.size == 1 }?.firstOrNull() ?: return null
        return (0 until model.columnCount).map { col ->
            val field = fields.find { field -> field.readableName == model.getColumnName(col) } ?: return null
            field to model.getValueAt(row, col)
        }.toMap()
    }

    data class Field<T>(
        val readableName: String,
        val renderer: TableCellRenderer? = null,
        val getData: (T) -> Any?
    )
}


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

import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.DescribeStackEventsRequest
import software.amazon.awssdk.services.cloudformation.model.StackEvent
import javax.swing.SwingUtilities

/**
 * AWS returns events in reverse chronological order. This class remembers last event id and returns only new one.
 * [lastEventIdOfCurrentPage] String? id of the last event used to prevent duplicates.
 * [id] String field used as event id
 * [previousPages] stack of tokens for all pages except first. First page does not have token
 * [nextPage] next page token  (if any)
 * [currentPage] current page token (or null if current page is first)
 */
class EventsFetcher(private val stackName: String) {
    private var lastEventIdOfCurrentPage: String? = null
    private val previousPages = mutableListOf<String>()
    private var nextPage: String? = null
    private var currentPage: String? = null

    /**
     * [pageToSwitchTo] switch to another page.
     *
     * @return new events from last call or all events if [pageToSwitchTo] is set (because all events
     * are new to another page) and set of available [Page]s
     */
    fun fetchEvents(client: CloudFormationClient, pageToSwitchTo: Page?): Pair<List<StackEvent>, Set<Page>> {
        assert(!SwingUtilities.isEventDispatchThread())

        val pageToFetch: String? = when (pageToSwitchTo) {
            Page.NEXT -> nextPage
            Page.PREVIOUS -> previousPages.lastOrNull()
            else -> currentPage
        }

        val request = DescribeStackEventsRequest.builder().stackName(stackName).nextToken(pageToFetch).build()
        val response = client.describeStackEvents(request)

        when (pageToSwitchTo) {
            Page.NEXT -> currentPage?.let { previousPages.add(it) } // Store current as prev
            Page.PREVIOUS -> if (previousPages.isNotEmpty()) previousPages.removeAt(previousPages.size - 1)
            else -> {}
        }
        nextPage = response.nextToken()
        currentPage = pageToFetch

        if (pageToSwitchTo != null) { // page changed, last event is not valid
            lastEventIdOfCurrentPage = null
        }

        val eventsUnprocessed = response.stackEvents()
        val eventsProcessed = when (lastEventIdOfCurrentPage) {
            null -> eventsUnprocessed
            else -> eventsUnprocessed.takeWhile { it.id != lastEventIdOfCurrentPage }
        }
        eventsProcessed.firstOrNull()?.let { lastEventIdOfCurrentPage = it.id }

        val availablePages = mutableSetOf<Page>()

        if (currentPage != null) { // We only can go prev. if current page is not first (not null)
            availablePages.add(Page.PREVIOUS)
        }
        if (nextPage != null) {
            availablePages.add(Page.NEXT)
        }

        return Pair(eventsProcessed, availablePages)
    }

    private val StackEvent.id: String get() = eventId()
}


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

import com.intellij.openapi.Disposable
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.SideBorder
import software.amazon.awssdk.services.cloudformation.model.StackEvent
import software.aws.toolkits.jetbrains.utils.ui.WrappingCellRenderer
import software.aws.toolkits.resources.message
import java.awt.Component
import javax.swing.JComponent
import javax.swing.JTable
import javax.swing.table.DefaultTableCellRenderer

/**
 * Events table
 */
interface EventsTable : View {
    /**
     * [events] List<StackEvent> Events in reverse chronological order.
     * All events are inserted in the top of the table.
     * Also, resets "Loading" icon (see [showBusyIcon]) and inserts [events] to the top of the table.
     * If [pageChanged] current events are removed
     */
    fun insertEvents(events: List<StackEvent>, pageChanged: Boolean)

    /**
     * Show "Loading" icon. Will be reset after [insertEvents]
     */
    fun showBusyIcon()
}

class StatusCellRenderer : DefaultTableCellRenderer() {
    override fun getTableCellRendererComponent(table: JTable, value: Any, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component =
        super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column).also {
            it.foreground = StatusType.fromStatusValue(value as String).color
        }
}

internal class EventsTableImpl : EventsTable, Disposable {

    private val logicalId = DynamicTableView.Field<StackEvent>(message("cloudformation.stack.logical_id")) { e -> e.logicalResourceId() }
    private val physicalId = DynamicTableView.Field<StackEvent>(message("cloudformation.stack.physical_id")) { e -> e.physicalResourceId() }

    private val table = DynamicTableView(
        DynamicTableView.Field(message("general.time")) { e -> e.timestamp() },
        // CFN Resource Status does not match what we expect (StackStatus enum)
        DynamicTableView.Field(message("cloudformation.stack.status"), renderer = StatusCellRenderer()) { e -> e.resourceStatusAsString() },
        logicalId,
        physicalId,
        DynamicTableView.Field(
            message("cloudformation.stack.reason"),
            WrappingCellRenderer(wrapOnSelection = true, wrapOnToggle = false)
        ) { e -> e.resourceStatusReason() ?: "" }
    ).apply { component.border = IdeBorderFactory.createBorder(SideBorder.BOTTOM) }

    override val component: JComponent = table.component

    override fun showBusyIcon() {
        table.showBusy(true)
    }

    override fun insertEvents(events: List<StackEvent>, pageChanged: Boolean) {
        table.updateItems(events, clearExisting = pageChanged)
    }

    override fun dispose() {}
}


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

import com.intellij.openapi.Disposable
import com.intellij.util.ui.JBUI
import software.amazon.awssdk.services.cloudformation.model.Output
import software.aws.toolkits.resources.message
import javax.swing.JComponent

class OutputsTableView : View, OutputsListener, Disposable {
    private val table = DynamicTableView<Output>(
        DynamicTableView.Field(message("cloudformation.stack.outputs.key")) { it.outputKey() },
        DynamicTableView.Field(message("cloudformation.stack.outputs.value")) { it.outputValue() },
        DynamicTableView.Field(message("cloudformation.stack.outputs.description")) { it.description() },
        DynamicTableView.Field(message("cloudformation.stack.outputs.export")) { it.exportName() }
    ).apply { component.border = JBUI.Borders.empty() }

    override val component: JComponent = table.component

    override fun updatedOutputs(outputs: List<Output>) = table.updateItems(outputs.sortedBy { it.outputKey() }, clearExisting = true)

    override fun dispose() {}
}

interface OutputsListener {
    fun updatedOutputs(outputs: List<Output>)
}


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

import com.intellij.openapi.Disposable
import java.awt.FlowLayout
import javax.swing.JButton
import javax.swing.JPanel
import javax.swing.SwingUtilities

/**
 * Next/Prev [Page] buttons
 * [onPageButtonClick] called when user clicks page
 */
class PageButtons(private val onPageButtonClick: (Page) -> Unit) : Disposable {
    private val buttons = Page.values().map { page ->
        page to JButton(page.icon).apply {
            addActionListener { onPageButtonClick(page) }
        }
    }.toMap()
    val component = JPanel(FlowLayout(FlowLayout.RIGHT)).apply {
        buttons.values.forEach { add(it) }
    }

    /**
     * [availablePages] only enable these pages
     */
    fun setPagesAvailable(availablePages: Set<Page>) {
        assert(SwingUtilities.isEventDispatchThread())
        buttons.forEach { it.value.isEnabled = it.key in availablePages }
    }

    override fun dispose() {
        buttons.values.forEach { button ->
            button.actionListeners.forEach { listener -> button.removeActionListener(listener) }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudformation.stack

import com.intellij.openapi.Disposable
import com.intellij.util.ui.JBUI
import software.amazon.awssdk.services.cloudformation.model.StackResource
import software.aws.toolkits.jetbrains.utils.ui.WrappingCellRenderer
import software.aws.toolkits.resources.message
import javax.swing.JComponent

class ResourceTableView : View, ResourceListener, Disposable {
    private val logicalId = DynamicTableView.Field<StackResource>(message("cloudformation.stack.logical_id")) { it.logicalResourceId() }
    private val physicalId = DynamicTableView.Field<StackResource>(message("cloudformation.stack.physical_id")) { it.physicalResourceId() }

    private val table = DynamicTableView(
        logicalId,
        physicalId,
        DynamicTableView.Field(message("cloudformation.stack.type")) { it.resourceType() },
        DynamicTableView.Field(message("cloudformation.stack.status"), renderer = StatusCellRenderer()) { it.resourceStatusAsString() },
        DynamicTableView.Field(
            message("cloudformation.stack.reason"),
            renderer = WrappingCellRenderer(wrapOnSelection = true, wrapOnToggle = false)
        ) { it.resourceStatusReason() }
    ).apply { component.border = JBUI.Borders.empty() }

    override val component: JComponent = table.component

    override fun updatedResources(resources: List<StackResource>) = table.updateItems(resources, clearExisting = true)

    override fun dispose() {}
}

interface ResourceListener {
    fun updatedResources(resources: List<StackResource>)
}


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

import com.intellij.icons.AllIcons
import com.intellij.notification.NotificationGroup
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.util.Disposer
import com.intellij.ui.OnePixelSplitter
import com.intellij.ui.components.JBTabbedPane
import com.intellij.ui.content.Content
import com.intellij.uiDesigner.core.GridConstraints
import com.intellij.uiDesigner.core.GridLayoutManager
import com.intellij.util.ui.JBUI
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.explorer.actions.SingleResourceNodeAction
import software.aws.toolkits.jetbrains.core.toolwindow.ToolkitToolWindow
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationStackNode
import software.aws.toolkits.jetbrains.services.cloudformation.toolwindow.CloudFormationToolWindow
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudformationTelemetry
import java.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.SwingUtilities

private val UPDATE_STACK_STATUS_INTERVAL = Duration.ofSeconds(5)
private val UPDATE_STACK_STATUS_INTERVAL_ON_FINAL_STATE = Duration.ofSeconds(60)
private const val REDRAW_ANIMATED_ICON_INTERVAL = 70
private const val TREE_TABLE_INITIAL_PROPORTION = 0.25f

class StackWindowManager(private val project: Project) {
    private val toolWindow = CloudFormationToolWindow.getInstance(project)

    fun openStack(stackName: String, stackId: String) {
        assert(SwingUtilities.isEventDispatchThread())
        if (!toolWindow.showExistingContent(stackId)) {
            StackUI(project, stackName, stackId, toolWindow).start()
        }
        CloudformationTelemetry.open(project, success = true)
    }

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

class OpenStackUiAction : SingleResourceNodeAction<CloudFormationStackNode>(message("cloudformation.stack.view")), DumbAware {
    override fun actionPerformed(selected: CloudFormationStackNode, e: AnActionEvent) {
        StackWindowManager.getInstance(e.getRequiredData(LangDataKeys.PROJECT)).openStack(selected.stackName, selected.stackId)
    }
}

private class StackUI(
    private val project: Project,
    private val stackName: String,
    stackId: String,
    private val toolWindow: ToolkitToolWindow
) : UpdateListener, Disposable {

    val toolWindowTab: Content
    private val animator: IconAnimator
    private val updater: Updater
    private val notificationGroup: NotificationGroup
    private val pageButtons: PageButtons

    private val eventsTable: EventsTableImpl
    private val outputsTable = OutputsTableView()
    private val resourcesTable = ResourceTableView()

    init {
        val tree = TreeViewImpl(project, stackName)
        animator = IconAnimator(REDRAW_ANIMATED_ICON_INTERVAL, tree)
        eventsTable = EventsTableImpl()
        pageButtons = PageButtons(this::onPageButtonClick)

        val notificationId = CloudFormationToolWindow.getInstance(project).toolWindowId
        notificationGroup = NotificationGroup.findRegisteredGroup(notificationId)
            ?: NotificationGroup.toolWindowGroup(notificationId, notificationId)

        val window = SimpleToolWindowPanel(false, true)
        val mainPanel = OnePixelSplitter(false, TREE_TABLE_INITIAL_PROPORTION).apply {
            firstComponent = tree.component
            secondComponent = JBTabbedPane().apply {
                this.add(
                    message("cloudformation.stack.tab_labels.events"),
                    JPanel(GridLayoutManager(2, 1)).apply {
                        add(
                            eventsTable.component,
                            GridConstraints(
                                0,
                                0,
                                1,
                                1,
                                0,
                                GridConstraints.FILL_BOTH,
                                GridConstraints.SIZEPOLICY_CAN_GROW or GridConstraints.SIZEPOLICY_WANT_GROW or GridConstraints.SIZEPOLICY_CAN_SHRINK,
                                GridConstraints.SIZEPOLICY_CAN_GROW or GridConstraints.SIZEPOLICY_WANT_GROW or GridConstraints.SIZEPOLICY_CAN_SHRINK,
                                null,
                                null,
                                null
                            )
                        )
                        add(
                            pageButtons.component,
                            GridConstraints(
                                1,
                                0,
                                1,
                                1,
                                0,
                                GridConstraints.FILL_HORIZONTAL,
                                GridConstraints.SIZEPOLICY_CAN_GROW or GridConstraints.SIZEPOLICY_WANT_GROW or GridConstraints.SIZEPOLICY_CAN_SHRINK,
                                GridConstraints.SIZEPOLICY_CAN_GROW or GridConstraints.SIZEPOLICY_CAN_SHRINK,
                                null,
                                null,
                                null
                            )
                        )
                        tabComponentInsets = JBUI.emptyInsets()
                        border = JBUI.Borders.empty()
                    }
                )

                this.add(
                    message("cloudformation.stack.tab_labels.resources"),
                    JPanel().apply {
                        layout = BoxLayout(this, BoxLayout.Y_AXIS)
                        add(resourcesTable.component)
                    }
                )

                this.add(
                    message("cloudformation.stack.tab_labels.outputs"),
                    JPanel().apply {
                        layout = BoxLayout(this, BoxLayout.Y_AXIS)
                        add(add(outputsTable.component))
                    }
                )
            }
        }
        updater = Updater(
            tree,
            eventsTable = eventsTable,
            outputsTable = outputsTable,
            resourceListener = resourcesTable,
            updateInterval = UPDATE_STACK_STATUS_INTERVAL,
            updateIntervalOnFinalState = UPDATE_STACK_STATUS_INTERVAL_ON_FINAL_STATE,
            listener = this,
            client = project.awsClient(),
            setPagesAvailable = pageButtons::setPagesAvailable,
            stackId = stackId
        )

        window.setContent(mainPanel)
        window.toolbar = createToolbar()

        toolWindowTab = toolWindow.addTab(stackName, window, id = stackId)
        // dispose self when toolwindowtab closes
        Disposer.register(toolWindowTab, this)
        listOf(tree, updater, animator, eventsTable, outputsTable, resourcesTable, pageButtons).forEach { Disposer.register(this, it) }
    }

    fun start() {
        toolWindow.show(toolWindowTab)
        animator.start()
        updater.start()
    }

    override fun onError(message: String) {
        notify(message, NotificationType.ERROR)
        toolWindowTab.dispose()
    }

    override fun onStackStatusChanged(stackStatus: StackStatus) {
        when (stackStatus.type) {
            StatusType.COMPLETED -> NotificationType.INFORMATION
            StatusType.FAILED -> NotificationType.ERROR
            StatusType.DELETED -> NotificationType.WARNING
            else -> null
        }?.let { type -> notify(stackStatus.name, type) }
    }

    private fun notify(message: String, notificationType: NotificationType) {
        notificationGroup.createNotification("$stackName: $message", notificationType).notify(project)
    }

    private fun createToolbar(): JComponent {
        val actionGroup = DefaultActionGroup()
        actionGroup.addAction(object : DumbAwareAction(message("general.refresh"), null, AllIcons.Actions.Refresh) {
            override fun actionPerformed(e: AnActionEvent) {
                updater.start()
            }

            override fun update(e: AnActionEvent) {
                e.presentation.isEnabled = !updater.running
            }
        })

        actionGroup.addAction(createFilterAction())

        return ActionManager.getInstance().createActionToolbar("", actionGroup, false).component
    }

    private fun createFilterAction() =
        object : ToggleAction(message("cloudformation.stack.filter.show_completed"), null, AllIcons.RunConfigurations.ShowPassed), DumbAware {
            private val state = AtomicBoolean(true)
            override fun isSelected(e: AnActionEvent): Boolean = state.get()

            override fun setSelected(e: AnActionEvent, newState: Boolean) {
                if (state.getAndSet(newState) != newState) {
                    ApplicationManager.getApplication().executeOnPooledThread {
                        updater.applyFilter {
                            newState || it.resourceStatus().type != StatusType.COMPLETED
                        }
                    }
                }
            }
        }

    fun onPageButtonClick(page: Page) {
        eventsTable.showBusyIcon()
        // To prevent double click, we disable buttons. They will be enabled by Updater when data fetched
        pageButtons.setPagesAvailable(emptySet())
        updater.switchPage(page)
    }

    override fun dispose() {
    }
}


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

import com.intellij.ide.util.treeView.NodeDescriptor
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.treeStructure.Tree
import one.util.streamex.StreamEx
import software.amazon.awssdk.services.cloudformation.model.StackResource
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import software.aws.toolkits.resources.message
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode

/**
 *
 * Stack status tree
 * [component] with tree content
 */
interface TreeView : View {
    /**
     * Update list of resources
     */
    fun fillResources(resources: Collection<StackResource>)

    /**
     * Sets whole stack status.
     */
    fun setStackStatus(status: StackStatus)
}

internal class TreeViewImpl(private val project: Project, stackName: String) : TreeView, Disposable, ViewWithIcons {

    private val tree: Tree
    override val component: JComponent

    private val rootNode: DefaultMutableTreeNode
        get() = tree.model.root as DefaultMutableTreeNode

    private val rootDescriptor: StackNodeDescriptor
        get() = rootNode.userObject as StackNodeDescriptor

    private var model: DefaultTreeModel

    /**
     * Remove outdated nodes, add new nodes, update status
     */
    private fun updateResourceList(resources: Collection<StackResource>) {
        val resourcesByName = resources.map { it.logicalResourceId() to it }.toMap()
        val existingResources = mutableSetOf<String>()
        val nodesToDelete = mutableListOf<MutableTreeNode>()

        for (rawNode in rootNode.children()) {
            val node = rawNode as DefaultMutableTreeNode
            val descriptor = (node.userObject as StackNodeDescriptor)
            val name = descriptor.element
            val resource = resourcesByName[name]
            if (resource != null) {
                // Set status
                val status = resource.resourceStatus()
                descriptor.setStatusAndType(status.type, status.name)
                descriptor.update()
                existingResources.add(name)
            } else {
                nodesToDelete.add(node)
            }
        }
        nodesToDelete.forEach { rootNode.remove(it) }
        for (nameAndResource in resourcesByName) {
            val name = nameAndResource.key
            if (name in existingResources) {
                continue
            }
            val resource = nameAndResource.value

            val status = resource.resourceStatus()
            val newDescriptor = StackNodeDescriptor(project, name, status.type, status.name, rootDescriptor, physicalId = resource.physicalResourceId())
            rootNode.add(DefaultMutableTreeNode(newDescriptor, false))
        }
    }

    init {
        val descriptor = StackNodeDescriptor(project, stackName, StatusType.UNKNOWN, message("loading_resource.loading"))
        val rootNode = DefaultMutableTreeNode(descriptor, true)
        model = DefaultTreeModel(rootNode)
        tree = Tree(model).also { it.name = "$stackName.tree" }
        tree.setPaintBusy(true)
        component = JBScrollPane(tree)
    }

    override fun getIconsAndUpdaters() =
        (StreamEx.of(rootNode.children()) + listOf(rootNode))
            .filterIsInstance<DefaultMutableTreeNode>()
            .map { resourceNode -> IconInfo(resourceNode.icon) { model.reload(resourceNode) } }

    private val DefaultMutableTreeNode.icon: Icon
        get() = (userObject as StackNodeDescriptor).icon!!

    override fun fillResources(resources: Collection<StackResource>) {
        tree.setPaintBusy(false)
        updateResourceList(resources)
        tree.expandRow(0) // Collapsed tree here makes no sense
        tree.updateUI()
    }

    override fun setStackStatus(status: StackStatus) {
        rootDescriptor.setStatusAndType(status.type, status.name)
    }

    override fun dispose() {
    }
}

private class StackNodeDescriptor(
    project: Project,
    val name: String,
    private var statusType: StatusType,
    private var status: String,
    parent: StackNodeDescriptor? = null,
    var physicalId: String? = null
) : NodeDescriptor<String>(project, parent) {

    init {
        myName = name
        update()
    }

    fun setStatusAndType(statusType: StatusType, status: String) {
        this.status = status
        this.statusType = statusType
        update()
    }

    override fun update(): Boolean {
        val iconForStatus = statusType.animatedIconIfPossible
        val result = iconForStatus == icon
        icon = iconForStatus
        return result
    }

    override fun getElement() = myName!!
    override fun toString() = "$myName [$status]"
}


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

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.util.Alarm
import com.intellij.util.AlarmFactory
import software.amazon.awssdk.core.exception.SdkException
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.DescribeStackResourcesRequest
import software.amazon.awssdk.services.cloudformation.model.Output
import software.amazon.awssdk.services.cloudformation.model.StackEvent
import software.amazon.awssdk.services.cloudformation.model.StackResource
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import java.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.SwingUtilities

/**
 * CallBacks for Updater. Called on EDT.
 */
interface UpdateListener {
    fun onError(message: String)
    fun onStackStatusChanged(stackStatus: StackStatus)
}

/**
 * Updates tree view on timer. Create on EDT.
 */
class Updater(
    private val treeView: TreeView,
    private val eventsTable: EventsTable,
    private val outputsTable: OutputsListener,
    private val resourceListener: ResourceListener,
    private val updateInterval: Duration,
    private val updateIntervalOnFinalState: Duration,
    private val listener: UpdateListener,
    private val client: CloudFormationClient,
    private val setPagesAvailable: (Set<Page>) -> Unit,
    private val stackId: String
) : Disposable {

    @Volatile
    private var stackStatus: StatusType? = null

    @Volatile
    private var predicate: (StackResource) -> Boolean = { true }
    private val updating = AtomicBoolean(false)
    val running get() = updating.get()
    private val eventsFetcher = EventsFetcher(stackId)

    private val app: Application
        get() = ApplicationManager.getApplication()

    /**
     * Alarm to me used to run [fetchData]. Do not use [Application.executeOnPooledThread] : it will lead to
     * races because of different threads
     */
    private val alarm: Alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)

    fun start() {
        updating.set(true)
        // cancel pending requests after refreshing
        alarm.cancelAllRequests()
        alarm.addRequest({ fetchDataSafely() }, 0)
    }

    /**
     * Fetch and display data of another page
     */
    fun switchPage(page: Page) {
        alarm.addRequest({ fetchDataSafely(page) }, 0)
    }

    /**
     * Apply a filter to the resources returned by the updater
     */
    fun applyFilter(predicate: (StackResource) -> Boolean) {
        this.predicate = predicate
        fetchDataSafely()
    }

    private fun fetchDataSafely(pageToSwitchTo: Page? = null) {
        try {
            fetchData(pageToSwitchTo)
        } catch (e: SdkException) {
            app.invokeLater {
                listener.onError(e.localizedMessage)
            }
        }
    }

    private fun fetchData(pageToSwitchTo: Page?) {
        assert(!SwingUtilities.isEventDispatchThread())
        val stackDetails = fetchStackDetails()
        val newStackStatus = stackDetails.status
        val newStackStatusType = newStackStatus.type
        val newStackStatusNotInProgress = newStackStatusType !in setOf(StatusType.UNKNOWN, StatusType.PROGRESS)

        // Stack changed to some "final" status just now, notify the user. Don't show a notification if the state
        // happened at creation time, which is distinguished by stackStatus being null
        val stackSwitchedToFinalStatus = stackStatus != null &&
            stackStatus != newStackStatusType &&
            newStackStatusNotInProgress

        // Stack status is final and has not been changed
        val stackStatusFinalNotChanged = newStackStatusNotInProgress && newStackStatusType == stackStatus

        stackStatus = newStackStatusType

        // Only fetch events if stack is not in final state or page switched
        val eventsAndButtonStates = if (stackStatusFinalNotChanged && pageToSwitchTo == null) {
            null
        } else {
            eventsFetcher.fetchEvents(client, pageToSwitchTo)
        }

        app.invokeLater {
            outputsTable.updatedOutputs(stackDetails.outputs)
            resourceListener.updatedResources(stackDetails.resources)

            showData(
                stackStatus = newStackStatus,
                resources = stackDetails.resources.filter(predicate),
                newEvents = eventsAndButtonStates?.first ?: emptyList(),
                pageChanged = pageToSwitchTo != null
            )

            eventsAndButtonStates?.second?.let { setPagesAvailable(it) }

            if (stackSwitchedToFinalStatus) {
                listener.onStackStatusChanged(newStackStatus)
            }

            updating.set(!newStackStatusNotInProgress)
            // Reschedule next run
            if (stackStatus == StatusType.DELETED) {
                alarm.cancelAllRequests()
            } else if (!alarm.isDisposed && alarm.isEmpty) {
                if (updating.get()) {
                    alarm.addRequest({ fetchDataSafely() }, updateInterval.toMillis())
                } else {
                    alarm.addRequest({ fetchDataSafely() }, updateIntervalOnFinalState.toMillis())
                }
            }
        }
    }

    private fun showData(
        stackStatus: StackStatus,
        resources: Collection<StackResource>,
        newEvents: List<StackEvent>,
        pageChanged: Boolean
    ) {
        assert(SwingUtilities.isEventDispatchThread())
        treeView.setStackStatus(stackStatus)
        treeView.fillResources(resources)
        eventsTable.insertEvents(newEvents, pageChanged)
    }

    private fun fetchStackDetails(): Stack {
        assert(!SwingUtilities.isEventDispatchThread())
        val resourcesRequest = DescribeStackResourcesRequest.builder().stackName(stackId).build()
        val resources = client.describeStackResources(resourcesRequest).stackResources()
        val stack = client.describeStacks { it.stackName(stackId) }.stacks().firstOrNull()
        val stackStatus = stack?.stackStatus() ?: StackStatus.UNKNOWN_TO_SDK_VERSION
        val outputs = stack?.outputs() ?: emptyList()
        return Stack(stackStatus, resources, outputs)
    }

    private data class Stack(val status: StackStatus, val resources: List<StackResource>, val outputs: List<Output>)

    override fun dispose() {
    }
}


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

import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.JBColor
import com.intellij.util.Alarm
import com.intellij.util.AlarmFactory
import software.amazon.awssdk.services.cloudformation.model.ResourceStatus
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import software.aws.toolkits.jetbrains.utils.ui.BETTER_GREEN
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.SwingUtilities

interface View {
    val component: JComponent
}

/**
 * Pages of events
 */
enum class Page(val icon: Icon) {
    PREVIOUS(AllIcons.Actions.Back),
    NEXT(AllIcons.Actions.Forward)
}

enum class StatusType(val icon: Icon, val color: JBColor, private val animatedIconStrategy: (() -> AnimatedIcon)? = null) {
    UNKNOWN(AllIcons.RunConfigurations.TestUnknown, JBColor.BLACK),
    PROGRESS(AllIcons.Process.ProgressResume, JBColor.ORANGE, { AnimatedIcon.FS() }),
    COMPLETED(AllIcons.RunConfigurations.ToolbarPassed, BETTER_GREEN),
    DELETED(AllIcons.RunConfigurations.ToolbarSkipped, JBColor.GRAY),
    FAILED(AllIcons.RunConfigurations.ToolbarFailed, JBColor.RED);

    val animatedIconIfPossible: Icon get() = animatedIconStrategy?.let { it() } ?: icon

    companion object {
        fun fromStatusValue(value: String) =
            listOf(ResourceStatus.fromValue(value).type, StackStatus.fromValue(value).type).firstOrNull { it != UNKNOWN }
                ?: UNKNOWN
    }
}

internal val StackStatus.type: StatusType
    get() = when (this) {
        StackStatus.UPDATE_COMPLETE, StackStatus.CREATE_COMPLETE -> StatusType.COMPLETED
        StackStatus.UPDATE_IN_PROGRESS,
        StackStatus.CREATE_IN_PROGRESS,
        StackStatus.ROLLBACK_IN_PROGRESS,
        StackStatus.UPDATE_ROLLBACK_IN_PROGRESS,
        StackStatus.DELETE_IN_PROGRESS,
        StackStatus.REVIEW_IN_PROGRESS,
        StackStatus.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS,
        StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS -> StatusType.PROGRESS
        StackStatus.DELETE_COMPLETE -> StatusType.DELETED
        StackStatus.ROLLBACK_COMPLETE,
        StackStatus.DELETE_FAILED,
        StackStatus.UPDATE_ROLLBACK_FAILED,
        StackStatus.ROLLBACK_FAILED,
        StackStatus.UPDATE_ROLLBACK_COMPLETE,
        StackStatus.CREATE_FAILED -> StatusType.FAILED
        else -> StatusType.UNKNOWN
    }

internal val ResourceStatus.type: StatusType
    get() = when (this) {
        ResourceStatus.CREATE_COMPLETE, ResourceStatus.UPDATE_COMPLETE -> StatusType.COMPLETED
        ResourceStatus.CREATE_FAILED, ResourceStatus.UPDATE_FAILED, ResourceStatus.DELETE_FAILED -> StatusType.FAILED
        ResourceStatus.DELETE_COMPLETE, ResourceStatus.DELETE_SKIPPED -> StatusType.DELETED
        ResourceStatus.CREATE_IN_PROGRESS, ResourceStatus.DELETE_IN_PROGRESS, ResourceStatus.UPDATE_IN_PROGRESS -> StatusType.PROGRESS
        else -> StatusType.UNKNOWN
    }

/**
 * [update] Function0<Unit> function to call to update icon
 */
internal data class IconInfo(val icon: Icon, val update: () -> Unit)

internal interface ViewWithIcons {
    fun getIconsAndUpdaters(): Collection<IconInfo>
}

/**
 * Animates icon
 * [views] Array<out ViewWithIcons> views with animated icons
 */
internal class IconAnimator(private val interval: Int, private vararg val views: ViewWithIcons) : Disposable {
    private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.SWING_THREAD, this)

    fun start() {
        assert(SwingUtilities.isEventDispatchThread())
        alarm.addRequest(this::updateIcons, interval)
    }

    private fun updateIcons() {
        // Update only animated icons
        views.flatMap { it.getIconsAndUpdaters() }.filter { it.icon is AnimatedIcon }.forEach { it.update() }

        if (!alarm.isDisposed) {
            start()
        }
    }

    override fun dispose() {
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudformation.toolwindow

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.toolwindow.ToolkitToolWindow

class CloudFormationToolWindow(override val project: Project) : ToolkitToolWindow {
    override val toolWindowId = "aws.cloudformation"

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


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

package software.aws.toolkits.jetbrains.services.cloudformation.toolwindow

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import software.aws.toolkits.resources.message

class CloudFormationToolWindowFactory : ToolWindowFactory, DumbAware {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        runInEdt {
            toolWindow.installWatcher(toolWindow.contentManager)
        }
    }

    override fun init(toolWindow: ToolWindow) {
        toolWindow.stripeTitle = message("cloudformation.toolwindow.label")
    }

    override fun shouldBeAvailable(project: Project): Boolean = false
}


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

package software.aws.toolkits.jetbrains.services.cloudformation.yaml

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.yaml.YAMLElementGenerator
import org.jetbrains.yaml.YAMLLanguage
import org.jetbrains.yaml.psi.YAMLFile
import org.jetbrains.yaml.psi.YAMLKeyValue
import org.jetbrains.yaml.psi.YAMLMapping
import org.jetbrains.yaml.psi.YAMLScalar
import org.jetbrains.yaml.psi.YAMLSequence
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationParameter
import software.aws.toolkits.jetbrains.services.cloudformation.CloudFormationTemplate
import software.aws.toolkits.jetbrains.services.cloudformation.NamedMap
import software.aws.toolkits.jetbrains.services.cloudformation.Parameter
import software.aws.toolkits.jetbrains.services.cloudformation.RESOURCE_MAPPINGS
import software.aws.toolkits.jetbrains.services.cloudformation.Resource
import software.aws.toolkits.resources.message

class YamlCloudFormationTemplate(template: YAMLFile) : CloudFormationTemplate {
    constructor(project: Project, templateFile: VirtualFile) : this(loadYamlFile(project, templateFile))

    private val templateRoot = getTemplateRoot(template)

    private fun getTemplateRoot(file: YAMLFile): YAMLMapping? {
        val documents = file.documents
        if (documents.size != 1) {
            LOG.debug { message("cloudformation.yaml.too_many_documents", file) }
            return null
        }

        val topLevelValue = documents[0].topLevelValue
        return if (topLevelValue is YAMLMapping) {
            topLevelValue
        } else {
            LOG.debug { message("cloudformation.yaml.invalid_root_type", file) }
            null
        }
    }

    override fun resources(): Sequence<Resource> {
        val resourcesBlock = templateRoot?.getKeyValueByKey("Resources") ?: return emptySequence()
        val resources = PsiTreeUtil.findChildOfAnyType(resourcesBlock, YAMLMapping::class.java) ?: return emptySequence()
        return resources.keyValues.asSequence().mapNotNull { it.asResource(this) }
    }

    override fun parameters(): Sequence<Parameter> {
        val parametersBlock = templateRoot?.getKeyValueByKey("Parameters") ?: return emptySequence()
        val parameters = PsiTreeUtil.findChildOfAnyType(parametersBlock, YAMLMapping::class.java) ?: return emptySequence()
        return parameters.keyValues.asSequence().mapNotNull { it.asProperty() }
    }

    override fun globals(): Map<String, NamedMap> {
        val globalsBlock = templateRoot?.getKeyValueByKey("Globals") ?: return emptyMap()
        val globals = PsiTreeUtil.findChildOfAnyType(globalsBlock, YAMLMapping::class.java) ?: return emptyMap()
        return globals.keyValues.asSequence().mapNotNull { yamlKeyValue -> yamlKeyValue.asGlobal()?.let { Pair(it.logicalName, it) } }.toMap()
    }

    override fun text(): String = templateRoot?.text ?: "<invalid>"

    private class YamlGlobal(override val logicalName: String, private val delegate: YAMLMapping) :
        YAMLMapping by delegate, NamedMap {
        override fun getScalarProperty(key: String): String =
            getOptionalScalarProperty(key)
                ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))

        override fun getOptionalScalarProperty(key: String): String? = delegate.getKeyValueByKey(key)?.valueText

        override fun setScalarProperty(key: String, value: String) {
            throw NotImplementedError()
        }

        override fun getSequenceProperty(key: String): YAMLSequence = getOptionalSequenceProperty(key)
            ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))

        override fun getOptionalSequenceProperty(key: String): YAMLSequence? = delegate.getKeyValueByKey(key)
            ?.children?.filterIsInstance<YAMLSequence>()?.firstOrNull()
    }

    private class YamlResource(
        override val logicalName: String,
        override val cloudFormationTemplate: CloudFormationTemplate,
        private val delegate: YAMLMapping
    ) : YAMLMapping by delegate, Resource {
        override fun isType(requestedType: String): Boolean = try {
            type() == requestedType
        } catch (_: Exception) {
            false
        }

        override fun type(): String? = delegate.getKeyValueByKey("Type")?.valueText

        override fun getScalarProperty(key: String): String = getOptionalScalarProperty(key)
            ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))

        override fun getOptionalScalarProperty(key: String): String? = properties().getKeyValueByKey(key)?.valueText

        override fun setScalarProperty(key: String, value: String) {
            val newKeyValue = YAMLElementGenerator.getInstance(project).createYamlKeyValue(key, value)
            properties().putKeyValue(newKeyValue)
        }

        override fun getSequenceProperty(key: String): YAMLSequence = getOptionalSequenceProperty(key)
            ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))

        override fun getOptionalSequenceProperty(key: String): YAMLSequence? =
            properties().getKeyValueByKey(key)?.children?.filterIsInstance<YAMLSequence>()?.firstOrNull()

        override fun getScalarMetadata(key: String): String = getOptionalScalarMetadata(key)
            ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))

        override fun getOptionalScalarMetadata(key: String): String? = metadata().getKeyValueByKey(key)?.valueText

        private fun properties(): YAMLMapping = delegate.getKeyValueByKey("Properties")?.value as? YAMLMapping
            ?: throw RuntimeException(message("cloudformation.key_not_found", "Properties", logicalName))

        private fun metadata(): YAMLMapping = delegate.getKeyValueByKey("Metadata")?.value as? YAMLMapping
            ?: throw RuntimeException(message("cloudformation.key_not_found", "Metadata", logicalName))
    }

    private class YamlCloudFormationParameter(override val logicalName: String, private val delegate: YAMLMapping) :
        YAMLMapping by delegate, NamedMap {
        override fun getScalarProperty(key: String): String = getOptionalScalarProperty(key)
            ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))

        override fun getOptionalScalarProperty(key: String): String? = delegate.getKeyValueByKey(key)?.valueText

        override fun setScalarProperty(key: String, value: String) {
            throw NotImplementedError()
        }

        override fun getSequenceProperty(key: String): YAMLSequence = getOptionalSequenceProperty(key)
            ?: throw IllegalStateException(message("cloudformation.missing_property", key, logicalName))
        override fun getOptionalSequenceProperty(key: String): YAMLSequence? = delegate.getKeyValueByKey(key)
            ?.children?.filterIsInstance<YAMLSequence>()?.firstOrNull()
    }

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

        private fun loadYamlFile(project: Project, templateFile: VirtualFile): YAMLFile =
            PsiFileFactory.getInstance(project).createFileFromText(
                "template_temp.yaml",
                YAMLLanguage.INSTANCE,
                StringUtil.convertLineSeparators(VfsUtil.loadText(templateFile)),
                false,
                false,
                true
            ) as YAMLFile

        fun convertPsiToResource(psiElement: PsiElement): Resource? {
            val yamlKeyValue = psiElement as? YAMLKeyValue ?: return null
            return yamlKeyValue.asResource(YamlCloudFormationTemplate(yamlKeyValue.containingFile as YAMLFile))
        }

        fun YAMLSequence.getTextValues(): List<String> = this.items.map { it.value }.filterIsInstance<YAMLScalar>().map { it.textValue }

        private fun YAMLKeyValue.asResource(cloudFormationTemplate: CloudFormationTemplate): Resource? {
            if (PsiTreeUtil.getParentOfType(this, YAMLKeyValue::class.java)?.keyText == "Resources") {
                val yamlValue = this.value as? YAMLMapping ?: return null
                val lowLevelResource = YamlResource(this.keyText, cloudFormationTemplate, yamlValue)
                return RESOURCE_MAPPINGS[lowLevelResource.type()]?.invoke(lowLevelResource) ?: lowLevelResource
            }
            return null
        }

        private fun YAMLKeyValue.asProperty(): Parameter? =
            if (PsiTreeUtil.getParentOfType(this, YAMLKeyValue::class.java)?.keyText == "Parameters") {
                val lowLevelParameter = YamlCloudFormationParameter(this.keyText, this.value as YAMLMapping)
                CloudFormationParameter(lowLevelParameter)
            } else {
                null
            }

        private fun YAMLKeyValue.asGlobal(): NamedMap? =
            if (PsiTreeUtil.getParentOfType(this, YAMLKeyValue::class.java)?.keyText == "Globals") {
                YamlGlobal(this.keyText, this.value as YAMLMapping)
            } else {
                null
            }
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs

import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException

fun CloudWatchLogsClient.checkIfLogGroupExists(logGroup: String): Boolean {
    val existingGroups = describeLogGroups { it.logGroupNamePrefix(logGroup) }
    return existingGroups.logGroups().any { it.logGroupName() == logGroup }
}

fun CloudWatchLogsClient.checkIfLogStreamExists(logGroup: String, logStream: String) = try {
    val existingStreams = describeLogStreams { it.logGroupName(logGroup).logStreamNamePrefix(logStream) }
    existingStreams.logStreams().any { it.logStreamName() == logStream }
    // Thrown if the log group does not exist
} catch (e: ResourceNotFoundException) {
    false
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.ui.TableUtil
import com.intellij.ui.table.TableView
import com.intellij.util.ExceptionUtil
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogStreamsRequest
import software.amazon.awssdk.services.cloudwatchlogs.model.FilterLogEventsRequest
import software.amazon.awssdk.services.cloudwatchlogs.model.GetLogEventsRequest
import software.amazon.awssdk.services.cloudwatchlogs.model.GetQueryResultsResponse
import software.amazon.awssdk.services.cloudwatchlogs.model.LogStream
import software.amazon.awssdk.services.cloudwatchlogs.model.OrderBy
import software.amazon.awssdk.services.cloudwatchlogs.model.QueryStatus
import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.LogResult
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.identifier
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.toLogResult
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
import java.time.Duration

sealed class CloudWatchActor<T, Message>(
    protected val project: Project,
    protected val client: CloudWatchLogsClient,
    protected val table: TableView<T>
) : Disposable {
    @Suppress("LeakingThis")
    protected val coroutineScope = disposableCoroutineScope(this)
    val channel = Channel<Message>()

    protected abstract val emptyText: String
    protected abstract val tableErrorMessage: String
    protected abstract val notFoundText: String

    protected val edtContext = getCoroutineUiContext()
    private val exceptionHandler = CoroutineExceptionHandler { _, e ->
        LOG.error(e) { "Exception thrown in the LogStreamActor not handled:" }
        notifyError(title = message("general.unknown_error"), project = project)
        table.setPaintBusy(false)
        channel.close()
    }

    init {
        coroutineScope.launch(exceptionHandler) {
            handleMessages()
        }
    }

    abstract suspend fun handleMessages()

    protected suspend fun loadAndPopulate(loadBlock: suspend () -> List<T>) {
        try {
            tableLoading()
            val items = loadBlock()
            table.listTableModel.items = items
            table.emptyText.text = emptyText
        } catch (e: ResourceNotFoundException) {
            withContext(edtContext) {
                table.emptyText.text = notFoundText
            }
        } catch (e: Exception) {
            LOG.error(e) { tableErrorMessage }
            withContext(edtContext) {
                table.emptyText.text = tableErrorMessage
                notifyError(title = tableErrorMessage, project = project)
            }
        } finally {
            tableDoneLoading()
        }
    }

    protected suspend fun tableLoading() = withContext(edtContext) {
        table.setPaintBusy(true)
        table.emptyText.text = message("loading_resource.loading")
    }

    protected suspend fun tableDoneLoading() = withContext(edtContext) {
        table.tableViewModel.fireTableDataChanged()
        table.setPaintBusy(false)
    }

    override fun dispose() {
        channel.close()
    }

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

abstract class CloudWatchLogsActor<T>(
    project: Project,
    client: CloudWatchLogsClient,
    table: TableView<T>
) : CloudWatchActor<T, CloudWatchLogsActor.Message>(project, client, table) {
    sealed class Message {
        object LoadInitial : Message()
        class LoadInitialRange(val previousEvent: LogStreamEntry, val duration: Duration) : Message()
        class LoadInitialFilter(val queryString: String) : Message()
        object LoadForward : Message()
        object LoadBackward : Message()
    }
    protected var nextBackwardToken: String? = null
    protected var nextForwardToken: String? = null

    protected open suspend fun loadInitial() {
        throw IllegalStateException("Table does not support loadInitial")
    }

    protected open suspend fun loadInitialRange(startTime: Long, duration: Duration) {
        throw IllegalStateException("Table does not support loadInitialRange")
    }

    protected open suspend fun loadInitialFilter(queryString: String) {
        throw IllegalStateException("Table does not support loadInitialFilter")
    }

    protected abstract suspend fun loadMore(nextToken: String?, saveForwardToken: Boolean = false, saveBackwardToken: Boolean = false): List<T>

    override suspend fun handleMessages() {
        for (message in channel) {
            when (message) {
                is Message.LoadForward -> if (!nextForwardToken.isNullOrEmpty()) {
                    withContext(edtContext) { table.setPaintBusy(true) }
                    val items = try {
                        loadMore(nextForwardToken, saveForwardToken = true)
                    } catch (e: Exception) {
                        LOG.warn(e) { "Exception thrown while trying to load forwards" }
                        notifyError(
                            project = project,
                            title = message("cloudwatch.logs.exception"),
                            content = message("cloudwatch.logs.failed_to_load_more")
                        )
                        listOf()
                    }
                    withContext(edtContext) {
                        if (items.isNotEmpty()) {
                            table.listTableModel.addRows(items)
                        }
                        table.setPaintBusy(false)
                    }
                }
                is Message.LoadBackward -> if (!nextBackwardToken.isNullOrEmpty()) {
                    withContext(edtContext) { table.setPaintBusy(true) }
                    val items = try {
                        loadMore(nextBackwardToken, saveBackwardToken = true)
                    } catch (e: Exception) {
                        LOG.warn(e) { "Exception thrown while trying to load backwards" }
                        notifyError(
                            project = project,
                            title = message("cloudwatch.logs.exception"),
                            content = message("cloudwatch.logs.failed_to_load_more")
                        )
                        listOf<T>()
                    }
                    if (items.isNotEmpty()) {
                        // Selected rows can be non contiguous so we have to save every row selected
                        val newSelection = table.selectedRows.map { it + items.size }
                        val viewRect = table.visibleRect
                        table.listTableModel.items = items + table.listTableModel.items
                        withContext(edtContext) {
                            table.tableViewModel.fireTableDataChanged()
                            val offset = table.getCellRect(items.size, 0, true)
                            // Move the view box down y - cell height amount to stay in the same place
                            viewRect.y = (viewRect.y + offset.y - offset.height)
                            table.scrollRectToVisible(viewRect)
                            // Re-add the selection
                            newSelection.forEach {
                                table.addRowSelectionInterval(it, it)
                            }
                        }
                    }
                    withContext(edtContext) { table.setPaintBusy(false) }
                }
                is Message.LoadInitial -> {
                    loadInitial()
                    // make sure the scroll pane is at the top after loading. Needed for Refresh!
                    val rect = table.getCellRect(0, 0, true)
                    withContext(edtContext) {
                        table.scrollRectToVisible(rect)
                    }
                }
                is Message.LoadInitialRange -> {
                    loadInitialRange(message.previousEvent.timestamp, message.duration)
                    val item = table.listTableModel.items.firstOrNull { it == message.previousEvent }
                    val index = table.listTableModel.indexOf(item).takeIf { it > 0 } ?: return
                    withContext(edtContext) {
                        table.setRowSelectionInterval(index, index)
                        TableUtil.scrollSelectionToVisible(table)
                    }
                }
                is Message.LoadInitialFilter -> {
                    loadInitialFilter(message.queryString)
                }
            }
        }
    }

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

class LogStreamFilterActor(
    project: Project,
    client: CloudWatchLogsClient,
    table: TableView<LogStreamEntry>,
    private val logGroup: String,
    private val logStream: String
) : CloudWatchLogsActor<LogStreamEntry>(project, client, table) {
    override val emptyText = message("cloudwatch.logs.no_events_query", logStream)
    override val tableErrorMessage = message("cloudwatch.logs.failed_to_load_stream", logStream)
    override val notFoundText = message("cloudwatch.logs.log_stream_does_not_exist", logStream)

    override suspend fun loadInitialFilter(queryString: String) {
        val request = FilterLogEventsRequest
            .builder()
            .logGroupName(logGroup)
            .logStreamNames(logStream)
            .filterPattern(queryString)
            .build()
        loadAndPopulate { getSearchLogEvents(request) }
    }

    override suspend fun loadMore(nextToken: String?, saveForwardToken: Boolean, saveBackwardToken: Boolean): List<LogStreamEntry> {
        // loading backwards doesn't make sense in this context, so just skip the event
        if (saveBackwardToken) {
            return listOf()
        }

        val request = FilterLogEventsRequest
            .builder()
            .logGroupName(logGroup)
            .logStreamNames(logStream)
            .nextToken(nextToken)
            .build()

        return getSearchLogEvents(request)
    }

    private fun getSearchLogEvents(request: FilterLogEventsRequest): List<LogStreamEntry> = try {
        val response = client.filterLogEvents(request)
        val events = response.events().filterNotNull().map { it.toLogStreamEntry() }
        nextForwardToken = response.nextToken()

        events
    } catch (e: Exception) {
        runInEdt {
            Messages.showErrorDialog(project, e.localizedMessage, message("cloudwatch.logs.failed_to_load_stream", logStream))
        }
        listOf()
    }
}

class LogStreamListActor(
    project: Project,
    client: CloudWatchLogsClient,
    table: TableView<LogStreamEntry>,
    private val logGroup: String,
    private val logStream: String
) :
    CloudWatchLogsActor<LogStreamEntry>(project, client, table) {
    override val emptyText = message("cloudwatch.logs.no_events")
    override val tableErrorMessage = message("cloudwatch.logs.failed_to_load_stream", logStream)
    override val notFoundText = message("cloudwatch.logs.log_stream_does_not_exist", logStream)

    override suspend fun loadInitial() {
        val request = GetLogEventsRequest
            .builder()
            .logGroupName(logGroup)
            .logStreamName(logStream)
            .startFromHead(true)
            .build()
        loadAndPopulate { getLogEvents(request, saveForwardToken = true, saveBackwardToken = true) }
    }

    override suspend fun loadInitialRange(startTime: Long, duration: Duration) = loadAndPopulate {
        val events = mutableListOf<LogStreamEntry>()
        client.getLogEventsPaginator {
            it.logGroupName(logGroup)
                .logStreamName(logStream)
                .startFromHead(true)
                .startTime(startTime - duration.toMillis())
                .endTime(startTime + duration.toMillis())
        }.stream().forEach { response ->
            if (nextBackwardToken == null) {
                nextBackwardToken = response.nextBackwardToken()
            }
            nextForwardToken = response.nextForwardToken()
            events.addAll(response.events().mapNotNull { it.toLogStreamEntry() })
        }
        return@loadAndPopulate events
    }

    override suspend fun loadMore(nextToken: String?, saveForwardToken: Boolean, saveBackwardToken: Boolean): List<LogStreamEntry> {
        val request = GetLogEventsRequest
            .builder()
            .logGroupName(logGroup)
            .logStreamName(logStream)
            .startFromHead(true)
            .nextToken(nextToken)
            .build()

        return getLogEvents(request, saveForwardToken = saveForwardToken, saveBackwardToken = saveBackwardToken)
    }

    private fun getLogEvents(
        request: GetLogEventsRequest,
        saveForwardToken: Boolean = false,
        saveBackwardToken: Boolean = false
    ): List<LogStreamEntry> {
        val response = client.getLogEvents(request)
        val events = response.events().filterNotNull().map { it.toLogStreamEntry() }
        if (saveForwardToken) {
            nextForwardToken = response.nextForwardToken()
        }
        if (saveBackwardToken) {
            nextBackwardToken = response.nextBackwardToken()
        }

        return events
    }
}

class LogGroupActor(
    project: Project,
    client: CloudWatchLogsClient,
    table: TableView<LogStream>,
    private val logGroup: String
) : CloudWatchLogsActor<LogStream>(project, client, table) {
    override val emptyText = message("cloudwatch.logs.no_log_streams")
    override val tableErrorMessage = message("cloudwatch.logs.failed_to_load_streams", logGroup)
    override val notFoundText = message("cloudwatch.logs.log_group_does_not_exist", logGroup)

    override suspend fun loadInitial() {
        // With this order by we can't filter (API limitation), but it wouldn't make sense to order by name
        val request = DescribeLogStreamsRequest.builder()
            .logGroupName(logGroup)
            .descending(true)
            .orderBy(OrderBy.LAST_EVENT_TIME)
            .build()
        loadAndPopulate { getLogStreams(request) }
    }

    override suspend fun loadMore(nextToken: String?, saveForwardToken: Boolean, saveBackwardToken: Boolean): List<LogStream> {
        // loading backwards doesn't make sense in this context, so just skip the event
        if (saveBackwardToken) {
            return listOf()
        }

        val request = DescribeLogStreamsRequest.builder()
            .logGroupName(logGroup)
            .nextToken(nextToken)
            .descending(true)
            .orderBy(OrderBy.LAST_EVENT_TIME)
            .build()

        return getLogStreams(request)
    }

    private fun getLogStreams(request: DescribeLogStreamsRequest): List<LogStream> {
        val response = client.describeLogStreams(request)
        val events = response.logStreams().filterNotNull()
        nextForwardToken = response.nextToken()

        return events
    }
}

class LogGroupSearchActor(
    project: Project,
    client: CloudWatchLogsClient,
    table: TableView<LogStream>,
    private val logGroup: String
) : CloudWatchLogsActor<LogStream>(project, client, table) {
    override val emptyText = message("cloudwatch.logs.no_log_streams")
    override val tableErrorMessage = message("cloudwatch.logs.failed_to_load_streams", logGroup)
    override val notFoundText = message("cloudwatch.logs.log_group_does_not_exist", logGroup)

    override suspend fun loadInitialFilter(queryString: String) {
        val request = DescribeLogStreamsRequest.builder()
            .logGroupName(logGroup)
            .descending(true)
            .orderBy(OrderBy.LOG_STREAM_NAME)
            .logStreamNamePrefix(queryString)
            .build()
        loadAndPopulate { getLogStreams(request) }
    }

    override suspend fun loadMore(nextToken: String?, saveForwardToken: Boolean, saveBackwardToken: Boolean): List<LogStream> {
        // loading backwards doesn't make sense in this context, so just skip the event
        if (saveBackwardToken) {
            return listOf()
        }

        val request = DescribeLogStreamsRequest.builder()
            .logGroupName(logGroup)
            .nextToken(nextToken)
            .descending(true)
            .orderBy(OrderBy.LOG_STREAM_NAME)
            .build()

        return getLogStreams(request)
    }

    private fun getLogStreams(request: DescribeLogStreamsRequest): List<LogStream> {
        val response = client.describeLogStreams(request)
        val events = response.logStreams().filterNotNull()
        nextForwardToken = response.nextToken()

        return events
    }
}

class InsightsQueryResultsActor(
    project: Project,
    client: CloudWatchLogsClient,
    table: TableView<LogResult>,
    private val queryId: String
) : CloudWatchActor<LogResult, InsightsQueryResultsActor.Message>(project, client, table) {
    sealed class Message {
        object StartLoadingAll : Message()
        object StopLoading : Message()
    }

    override val emptyText = message("cloudwatch.logs.no_results_found")
    override val tableErrorMessage = message("cloudwatch.logs.query_results_table_error")
    override val notFoundText = message("cloudwatch.logs.no_results_found")

    private var loadJob: Job? = null

    override suspend fun handleMessages() {
        for (message in channel) {
            when (message) {
                is Message.StartLoadingAll -> {
                    try {
                        startLoading()
                    } catch (e: Exception) {
                        notifyError(
                            project = project,
                            title = message("cloudwatch.logs.exception"),
                            content = ExceptionUtil.getThrowableText(e)
                        )
                    }
                }
                is Message.StopLoading -> {
                    try {
                        stopLoading()
                    } catch (e: Exception) {
                        notifyError(
                            project = project,
                            title = message("cloudwatch.logs.exception"),
                            content = ExceptionUtil.getThrowableText(e)
                        )
                    }
                }
            }
        }
    }

    // run on a separate context so we don't lock up the message listener
    private fun startLoading() = coroutineScope.launch(getCoroutineBgContext()) {
        tableLoading()
        val loadedQueryResults = mutableSetOf<String>()
        var response: GetQueryResultsResponse

        do {
            try {
                response = client.getQueryResults {
                    it.queryId(queryId)
                }
            } catch (e: Exception) {
                notifyError(
                    project = project,
                    title = message("cloudwatch.logs.exception"),
                    content = ExceptionUtil.getThrowableText(e)
                )
                table.emptyText.text = tableErrorMessage

                channel.close()
                return@launch
            }

            // consider a buffered flow instead, but can't use until min-coroutine version in the IDE is 1.3.6
            // chunk the response because a) transforming the response into the table model may be expensive
            // and b) don't want to force the table model to redraw after every single row
            val dedupedResults = sequence {
                response.results().forEach { result ->
                    val logResult = result.toLogResult()
                    val ptr = logResult.identifier()
                    if (ptr !in loadedQueryResults) {
                        loadedQueryResults.add(ptr)
                        yield(logResult)
                    }
                }
            }.chunked(1000)

            dedupedResults.forEach { chunk ->
                LOG.info { "loading block of ${chunk.size}" }
                table.listTableModel.addRows(chunk)
            }
        } while (isQueryRunning(response.status()))

        LOG.info { "done, ${loadedQueryResults.size} entries in set" }
        tableDoneLoading()
        table.emptyText.text = emptyText
        LOG.info { "total items in table: ${table.listTableModel.items.size}" }
        channel.close()
    }.also {
        loadJob = it
    }

    private fun isQueryRunning(status: QueryStatus) = status !in terminalQueryStates

    private fun stopLoading() {
        channel.close()

        loadJob?.let { job ->
            if (job.isActive) {
                job.cancel()
            }
        }

        coroutineScope.launch {
            // network call; run off EDT
            try {
                client.stopQuery {
                    it.queryId(queryId)
                }
            } catch (e: Exception) {
                // best effort; this will fail if the query raced to completion or user does not have permission
                LOG.warn(e) { "Failed to stop query" }
            }
        }
    }

    override fun dispose() {
        stopLoading()
        super.dispose()
    }

    companion object {
        private val LOG = getLogger<InsightsQueryResultsActor>()
        private val terminalQueryStates = setOf(QueryStatus.COMPLETE, QueryStatus.CANCELLED, QueryStatus.FAILED)
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs

import com.intellij.openapi.project.Project
import icons.AwsIcons
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerResourceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerServiceNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.CacheBackedAwsExplorerServiceRootNode
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.resources.CloudWatchResources
import software.aws.toolkits.resources.message

class CloudWatchLogsServiceNode(project: Project, service: AwsExplorerServiceNode) : CacheBackedAwsExplorerServiceRootNode<LogGroup>(
    project,
    service,
    CloudWatchResources.LIST_LOG_GROUPS
) {
    override fun displayName(): String = message("explorer.node.cloudwatch")
    override fun toNode(child: LogGroup): AwsExplorerNode<*> = CloudWatchLogsNode(nodeProject, child.arn(), child.logGroupName())
}

class CloudWatchLogsNode(
    project: Project,
    val arn: String,
    val logGroupName: String
) : AwsExplorerResourceNode<String>(
    project,
    CloudWatchLogsClient.SERVICE_NAME,
    logGroupName,
    AwsIcons.Resources.CloudWatch.LOG_GROUP
) {
    override fun resourceType() = "group"

    override fun resourceArn() = arn

    override fun displayName() = logGroupName

    override fun onDoubleClick() {
        CloudWatchLogWindow.getInstance(nodeProject).showLogGroup(logGroupName)
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.util.text.DateFormatUtil
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.toolwindow.ToolkitToolWindow
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor.CloudWatchLogGroup
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.editor.CloudWatchLogStream
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.DetailedLogRecord
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.QueryDetails
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.insights.QueryResultPanel
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.toolwindow.CloudWatchLogsToolWindowFactory
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudWatchResourceType
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry
import software.aws.toolkits.telemetry.Result
import java.time.Duration

class CloudWatchLogWindow(override val project: Project) : ToolkitToolWindow {
    override val toolWindowId = CloudWatchLogsToolWindowFactory.TOOLWINDOW_ID

    fun showLogGroup(logGroup: String) {
        var result = Result.Succeeded
        runInEdt {
            try {
                if (!showExistingContent(logGroup)) {
                    val group = CloudWatchLogGroup(project, logGroup)
                    val title = message("cloudwatch.logs.log_group_title", logGroup.split("/").last())
                    addTab(title, group.content, activate = true, id = logGroup, additionalDisposable = group)
                }
            } catch (e: Exception) {
                LOG.error(e) { "Exception thrown while trying to show log group '$logGroup'" }
                result = Result.Failed
                throw e
            } finally {
                CloudwatchlogsTelemetry.open(project, result, CloudWatchResourceType.LogGroup, source = "logGroup")
            }
        }
    }

    fun showLogStream(
        logGroup: String,
        logStream: String,
        previousEvent: LogStreamEntry? = null,
        duration: Duration? = null,
        streamLogs: Boolean = false
    ) {
        var result = Result.Succeeded
        try {
            val id = "$logGroup/$logStream/${previousEvent?.timestamp}/${previousEvent?.message}/$duration"
            if (showExistingContent(id)) {
                return
            }
            val title = if (previousEvent != null && duration != null) {
                message(
                    "cloudwatch.logs.filtered_log_stream_title",
                    logStream,
                    DateFormatUtil.getDateTimeFormat().format(previousEvent.timestamp - duration.toMillis()),
                    DateFormatUtil.getDateTimeFormat().format(previousEvent.timestamp + duration.toMillis())
                )
            } else {
                message("cloudwatch.logs.log_stream_title", logStream)
            }
            val stream = CloudWatchLogStream(project, logGroup, logStream, previousEvent, duration, streamLogs)
            runInEdt {
                addTab(title, stream.content, activate = true, id = id, additionalDisposable = stream)
            }
        } catch (e: Exception) {
            LOG.error(e) { "Exception thrown while trying to show log group '$logGroup' stream '$logStream'" }
            result = Result.Failed
            throw e
        } finally {
            CloudwatchlogsTelemetry.open(project, result, CloudWatchResourceType.LogStream, source = "logStream")
        }
    }

    fun showQueryResults(queryDetails: QueryDetails, queryId: String, fields: List<String>) {
        if (showExistingContent(queryId)) {
            return
        }

        val panel = QueryResultPanel(project, fields, queryId, queryDetails)
        val title = message("cloudwatch.logs.query_tab_title", queryId)
        runInEdt {
            addTab(title, panel, activate = true, id = queryId)
        }
    }

    fun showDetailedEvent(client: CloudWatchLogsClient, identifier: String) {
        if (showExistingContent(identifier)) {
            return
        }

        val detailedLogEvent = DetailedLogRecord(project, client, identifier)
        runInEdt {
            addTab(detailedLogEvent.title, detailedLogEvent.getComponent(), activate = true, id = identifier, additionalDisposable = detailedLogEvent)
        }
    }

    fun closeLogGroup(logGroup: String) {
        findPrefix(logGroup).forEach {
            removeContent(it)
        }
    }

    companion object {
        private val LOG = getLogger<CloudWatchLogWindow>()
        fun getInstance(project: Project) = project.service<CloudWatchLogWindow>()
    }
}


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

package software.aws.toolkits.jetbrains.services.cloudwatch.logs

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.fileChooser.FileSaverDescriptor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileWrapper
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.amazon.awssdk.services.cloudwatchlogs.model.GetLogEventsRequest
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions.OpenStreamInEditor
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.actions.buildStringFromLogsOutput
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CloudWatchResourceType
import software.aws.toolkits.telemetry.CloudwatchlogsTelemetry
import software.aws.toolkits.telemetry.Result
import java.io.File
import java.nio.file.Files
import java.time.Instant
import kotlin.coroutines.CoroutineContext
import kotlin.streams.asSequence

class LogStreamDownloadTask(
    project: Project,
    private val edt: CoroutineContext,
    val client: CloudWatchLogsClient,
    val logGroup: String,
    val logStream: String
) : Task.Backgroundable(project, message("cloudwatch.logs.opening_in_editor", logStream), true) {

    override fun run(indicator: ProgressIndicator) = runBlocking {
        // Default content load limit is 20MB, default per page is 1MB/10000 log entries. so we load MaxLength/1MB
        // until we give up and prompt the user to save to file
        val maxPages = FileUtilRt.getUserContentLoadLimit() / (1 * FileUtilRt.MEGABYTE)
        val startTime = Instant.now()
        val buffer = StringBuilder()
        val request = GetLogEventsRequest
            .builder()
            .startFromHead(true)
            .logGroupName(logGroup)
            .logStreamName(logStream)
            .endTime(startTime.toEpochMilli())
        val getRequest = client.getLogEventsPaginator(request.build())
        getRequest.stream().asSequence().forEachIndexed { index, value ->
            indicator.checkCanceled()
            buffer.append(value.events().buildStringFromLogsOutput())
            // This might look off by 1 because for example if we are at index 20, it's the
            // 21st iteration, but at this point we won't try to open in a file so we bail from
            // streaming at the correct time
            if (index >= maxPages) {
                runBlocking {
                    request.nextToken(value.nextForwardToken())
                    if (promptWriteToFile() == Messages.OK) {
                        ProgressManager.getInstance().run(
                            LogStreamDownloadToFileTask(
                                project,
                                client,
                                logGroup,
                                logStream,
                                buffer.toString(),
                                request.build()
                            )
                        )
                    }
                    // Cancel this Task no matter what. If the user has agreed to download to a file,
                    // the download to a file task will handle everything from here
                    indicator.cancel()
                }
            }
        }

        val success = withContext(edt) {
            OpenStreamInEditor.open(project, logStream, buffer.toString())
        }
        CloudwatchlogsTelemetry.openStreamInEditor(project, success)
    }

    override fun onThrowable(e: Throwable) {
        LOG.error(e) { "LogStreamDownloadTask exception thrown" }
        val result = if (e is ProcessCanceledException) {
            Result.Cancelled
        } else {
            Result.Failed
        }
        CloudwatchlogsTelemetry.openStreamInEditor(project, result)
    }

    private suspend fun promptWriteToFile(): Int = withContext(edt) {
        return@withContext Messages.showOkCancelDialog(
            project,
            message("cloudwatch.logs.stream_too_big_message", logStream),
            message("cloudwatch.logs.stream_too_big"),
            message("cloudwatch.logs.stream_save_to_file", logStream),
            Messages.getCancelButton(),
            AllIcons.General.QuestionDialog
        )
    }

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

class LogStreamDownloadToFileTask(
    project: Project,
    private val client: CloudWatchLogsClient,
    private val logGroup: String,
    private val logStream: String,
    private val buffer: String,
    private val request: GetLogEventsRequest? = null
) : Task.Backgroundable(project, message("cloudwatch.logs.saving_to_disk", logStream), true) {
    private val edt = getCoroutineUiContext()

    override fun run(indicator: ProgressIndicator) = runBlocking {
        val startTime = Instant.now()
        val finalRequest = request ?: GetLogEventsRequest
            .builder()
            .startFromHead(true)
            .logGroupName(logGroup)
            .logStreamName(logStream)
            .endTime(startTime.toEpochMilli())
            .build()
        promptToDownload(indicator, finalRequest, buffer)
    }

    private suspend fun promptToDownload(indicator: ProgressIndicator, request: GetLogEventsRequest, buffer: String) {
        val descriptor = FileSaverDescriptor(message("cloudwatch.logs.download"), message("cloudwatch.logs.download.description"))
        val saveLocation = withContext(edt) {
            val destination = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, project)
            destination.save(null as VirtualFile?, logStream)
        }
        if (saveLocation != null) {
            streamLogStreamToFile(indicator, request, saveLocation.file, buffer)
        }
    }

    private fun streamLogStreamToFile(indicator: ProgressIndicator, request: GetLogEventsRequest, file: File, buffer: String) {
        try {
            // Delete the existing file if one exists so we don't append to it
            Files.deleteIfExists(file.toPath())
            file.appendText(buffer)
            val getRequest = client.getLogEventsPaginator(request)
            getRequest.stream().forEach {
                indicator.checkCanceled()
                val str = it.events().buildStringFromLogsOutput()
                file.appendText(str)
            }
            notifyInfo(
                project = project,
                title = message("aws.notification.title"),
                content = message("cloudwatch.logs.saving_to_disk_succeeded", logStream, file.path),
                notificationActions = listOf(
                    object : AnAction(message("cloudwatch.logs.open_in_editor"), null, AllIcons.Actions.MenuOpen) {
                        override fun actionPerformed(e: AnActionEvent) {
                            val virtualFile = VirtualFileWrapper(file).virtualFile
                                ?: throw IllegalStateException("Log Stream was downloaded but does not exist on disk!")
                            FileEditorManager.getInstance(project).openFile(virtualFile, true, true)
                        }
                    }
                )
            )
            CloudwatchlogsTelemetry.download(project, success = true, CloudWatchResourceType.LogStream)
        } catch (e: Exception) {
            LOG.error(e) { "Exception thrown while downloading large log stream" }
            e.notifyError(project = project, title = message("cloudwatch.logs.saving_to_disk_failed", logStream))
            CloudwatchlogsTelemetry.download(project, success = false, CloudWatchResourceType.LogStream)
        }
    }

    override fun onThrowable(e: Throwable) {
        LOG.error(e) { "LogStreamDownloadToFileTask exception thrown" }
        val result = if (e is ProcessCanceledException) {
            Result.Cancelled
        } else {
            Result.Failed
        }
        CloudwatchlogsTelemetry.download(project, result, CloudWatchResourceType.LogStream)
    }

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