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

package software.aws.toolkits.jetbrains.services.codewhisperer.inlay

import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.util.Disposer
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk

class CodeWhispererInlayManager {
    fun updateInlays(states: InvocationContext, chunks: List<RecommendationChunk>) {
        val editor = states.requestContext.editor
        clearInlays(editor)

        chunks.forEach { chunk ->
            createCodeWhispererInlays(editor, chunk.inlayOffset, chunk.text, states.popup)
        }
    }

    private fun createCodeWhispererInlays(editor: Editor, startOffset: Int, inlayText: String, popup: JBPopup) {
        if (inlayText.isEmpty()) return
        val firstNewlineIndex = inlayText.indexOf("\n")
        val firstLine: String
        val otherLines: String
        if (firstNewlineIndex != -1 && firstNewlineIndex < inlayText.length - 1) {
            firstLine = inlayText.substring(0, firstNewlineIndex)
            otherLines = inlayText.substring(firstNewlineIndex + 1)
        } else {
            firstLine = inlayText
            otherLines = ""
        }

        val firstLineRenderer = CodeWhispererInlayInlineRenderer(firstLine)
        val inlineInlay = editor.inlayModel.addInlineElement(startOffset, true, firstLineRenderer)
        inlineInlay?.let { Disposer.register(popup, it) }

        if (otherLines.isEmpty()) {
            return
        }
        val otherLinesRenderer = CodeWhispererInlayBlockRenderer(otherLines)
        val blockInlay = editor.inlayModel.addBlockElement(
            startOffset,
            true,
            false,
            0,
            otherLinesRenderer
        )
        blockInlay?.let { Disposer.register(popup, it) }
    }

    fun clearInlays(editor: Editor) {
        editor.inlayModel.getInlineElementsInRange(
            0,
            editor.document.textLength,
            CodeWhispererInlayInlineRenderer::class.java
        ).forEach { disposable ->
            Disposer.dispose(disposable)
        }
        editor.inlayModel.getBlockElementsInRange(
            0,
            editor.document.textLength,
            CodeWhispererInlayBlockRenderer::class.java
        ).forEach { disposable ->
            Disposer.dispose(disposable)
        }
    }

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


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

package software.aws.toolkits.jetbrains.services.codewhisperer.inlay

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorCustomElementRenderer
import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry
import com.intellij.openapi.editor.impl.FontInfo
import com.intellij.xdebugger.ui.DebuggerColors
import java.awt.Font
import java.awt.Graphics

abstract class CodeWhispererInlayRenderer(protected val myValue: String) : EditorCustomElementRenderer {
    fun getFontInfo(editor: Editor): FontInfo {
        val colorsScheme = editor.colorsScheme
        val fontPreferences = colorsScheme.fontPreferences
        val attributes = editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE)
        val fontStyle = attributes?.fontType ?: Font.PLAIN
        return ComplementaryFontsRegistry.getFontAbleToDisplay(
            'a'.toInt(),
            fontStyle,
            fontPreferences,
            FontInfo.getFontRenderContext(editor.contentComponent)
        )
    }

    fun applyCodeWhispererColorAndFontSettings(editor: Editor, g: Graphics) {
        val attributes = editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE) ?: return
        val fgColor = attributes.foregroundColor ?: return
        g.color = fgColor
        val fontInfo = getFontInfo(editor)
        g.font = fontInfo.font
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language

import com.intellij.openapi.components.service
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererC
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCpp
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererCsharp
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererGo
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJavaScript
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJsx
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererKotlin
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPhp
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPlainText
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererPython
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererRuby
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererRust
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererScala
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererShell
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererSql
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTsx
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererTypeScript
import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererUnknownLanguage

class CodeWhispererLanguageManager {
    // Always use this method to check for language support for CodeWhisperer features.
    // The return type here implicitly means that the corresponding language plugin has been installed to the user's IDE,
    // (e.g. 'Python' plugin for Python and 'JavaScript and TypeScript' for JS/TS). So we can leverage these language
    // plugin features when developing CodeWhisperer features.
    /**
     * resolve language by
     * 1. file type
     * 2. extension
     * 3. fallback to unknown
     */
    fun getLanguage(vFile: VirtualFile): CodeWhispererProgrammingLanguage {
        val fileTypeName = vFile.fileType.name.lowercase()

        val fileExtension = vFile.extension?.lowercase()

        // We want to support Python Console which does not have a file extension
        if (fileExtension == null && !fileTypeName.contains("python")) {
            return CodeWhispererUnknownLanguage.INSTANCE
        }

        return when {
            fileTypeName.contains("python") -> CodeWhispererPython.INSTANCE
            fileTypeName.contains("javascript") -> CodeWhispererJavaScript.INSTANCE
            fileTypeName.contains("java") -> CodeWhispererJava.INSTANCE
            fileTypeName.contains("jsx harmony") -> CodeWhispererJsx.INSTANCE
            fileTypeName.contains("c#") -> CodeWhispererCsharp.INSTANCE
            fileTypeName.contains("typescript jsx") -> CodeWhispererTsx.INSTANCE
            fileTypeName.contains("typescript") -> CodeWhispererTypeScript.INSTANCE
            fileTypeName.contains("scala") -> CodeWhispererScala.INSTANCE
            fileTypeName.contains("kotlin") -> CodeWhispererKotlin.INSTANCE
            fileTypeName.contains("ruby") -> CodeWhispererRuby.INSTANCE
            fileTypeName.contains("php") -> CodeWhispererPhp.INSTANCE
            fileTypeName.contains("sql") -> CodeWhispererSql.INSTANCE
            fileTypeName.contains("go") -> CodeWhispererGo.INSTANCE
            fileTypeName.contains("shell") -> CodeWhispererShell.INSTANCE
            fileTypeName.contains("rust") -> CodeWhispererRust.INSTANCE
            fileTypeName.contains("plain_text") -> CodeWhispererPlainText.INSTANCE
            else -> null
        }
            ?: languageExtensionsMap[fileExtension]
            ?: CodeWhispererUnknownLanguage.INSTANCE
    }

    /**
     * will call getLanguage(virtualFile) first, then fallback to string resolve in case of psi only exists in memeory
     */
    fun getLanguage(psiFile: PsiFile): CodeWhispererProgrammingLanguage = psiFile.virtualFile?.let {
        getLanguage(it)
    } ?: languageExtensionsMap.keys.find { ext -> psiFile.name.endsWith(ext) }?.let { languageExtensionsMap[it] }
        ?: CodeWhispererUnknownLanguage.INSTANCE

    companion object {
        fun getInstance(): CodeWhispererLanguageManager = service()

        /**
         * languageExtensionMap will look like
         * {
         *      "cpp" to CodeWhispererCpp,
         *      "c++" to CodeWhispererCpp,
         *      "cc" to CodeWhispererCpp,
         *      "java" to CodeWhispererJava,
         *      ...
         * }
         */
        val languageExtensionsMap = listOf(
            listOf("java") to CodeWhispererJava.INSTANCE,
            listOf("py") to CodeWhispererPython.INSTANCE,
            listOf("js") to CodeWhispererJavaScript.INSTANCE,
            listOf("jsx") to CodeWhispererJsx.INSTANCE,
            listOf("ts") to CodeWhispererTypeScript.INSTANCE,
            listOf("tsx") to CodeWhispererTsx.INSTANCE,
            listOf("cs") to CodeWhispererCsharp.INSTANCE,
            listOf("kt") to CodeWhispererKotlin.INSTANCE,
            listOf("scala") to CodeWhispererScala.INSTANCE,
            listOf("c", "h") to CodeWhispererC.INSTANCE,
            listOf("cpp", "c++", "cc") to CodeWhispererCpp.INSTANCE,
            listOf("sh") to CodeWhispererShell.INSTANCE,
            listOf("rb") to CodeWhispererRuby.INSTANCE,
            listOf("rs") to CodeWhispererRust.INSTANCE,
            listOf("go") to CodeWhispererGo.INSTANCE,
            listOf("php") to CodeWhispererPhp.INSTANCE,
            listOf("sql") to CodeWhispererSql.INSTANCE,
            listOf("txt") to CodeWhispererPlainText.INSTANCE
        ).map {
            val exts = it.first
            val lang = it.second
            exts.map { ext -> ext to lang }
        }.flatten()
            .associateBy({ it.first }, { it.second })
    }
}

fun PsiFile.programmingLanguage(): CodeWhispererProgrammingLanguage = CodeWhispererLanguageManager.getInstance().getLanguage(this)

fun VirtualFile.programmingLanguage(): CodeWhispererProgrammingLanguage = CodeWhispererLanguageManager.getInstance().getLanguage(this)


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language

import software.aws.toolkits.telemetry.CodewhispererLanguage

/**
 * Any subclass of CodeWhispererProgrammingLanguage should have private constructor
 */
abstract class CodeWhispererProgrammingLanguage {
    abstract val languageId: String

    abstract fun toTelemetryType(): CodewhispererLanguage

    open fun isCodeCompletionSupported(): Boolean = false

    open fun isCodeScanSupported(): Boolean = false

    open fun isImportAdderSupported(): Boolean = false

    open fun isClassifierSupported(): Boolean = false

    open fun isAllClassifier(): Boolean = false

    open fun isSupplementalContextSupported(): Boolean = false

    open fun isUTGSupported(): Boolean = false

    open fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = this

    final override fun equals(other: Any?): Boolean {
        if (other !is CodeWhispererProgrammingLanguage) return false
        return this.languageId == other.languageId
    }

    /**
     * we want to force CodeWhispererProgrammingLanguage(any language implement it) be singleton,
     * override hashCode is the backup plan if another object is being created
     */
    final override fun hashCode(): Int = this.languageId.hashCode()
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererC private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.C

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "c"

        val INSTANCE = CodeWhispererC()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererCpp private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Cpp

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "cpp"

        val INSTANCE = CodeWhispererCpp()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererCsharp private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Csharp

    override fun isCodeCompletionSupported(): Boolean = true

    override fun isClassifierSupported(): Boolean = true

    override fun isAllClassifier(): Boolean = true

    companion object {
        const val ID = "csharp"

        val INSTANCE = CodeWhispererCsharp()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererGo private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Go

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "go"

        val INSTANCE = CodeWhispererGo()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererJava private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Java

    override fun isCodeCompletionSupported(): Boolean = true

    override fun isCodeScanSupported(): Boolean = true

    override fun isImportAdderSupported(): Boolean = true

    override fun isClassifierSupported(): Boolean = true

    override fun isAllClassifier(): Boolean = true

    override fun isSupplementalContextSupported() = true

    companion object {
        const val ID = "java"

        val INSTANCE = CodeWhispererJava()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererJavaScript private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Javascript

    override fun isCodeCompletionSupported(): Boolean = true

    override fun isImportAdderSupported(): Boolean = true

    override fun isClassifierSupported(): Boolean = true

    override fun isAllClassifier(): Boolean = true

    companion object {
        const val ID = "javascript"

        val INSTANCE = CodeWhispererJavaScript()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererJsx private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Jsx

    override fun isCodeCompletionSupported(): Boolean = true

    override fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = CodeWhispererJavaScript.INSTANCE

    override fun isClassifierSupported(): Boolean = true

    override fun isAllClassifier(): Boolean = true

    companion object {
        const val ID = "jsx"

        val INSTANCE = CodeWhispererJsx()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererKotlin private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Kotlin

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "kotlin"

        val INSTANCE = CodeWhispererKotlin()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererPhp private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Php

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "php"

        val INSTANCE = CodeWhispererPhp()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererPlainText private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Plaintext

    companion object {
        const val ID = "plaintext"

        val INSTANCE = CodeWhispererPlainText()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererPython private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Python

    override fun isCodeCompletionSupported(): Boolean = true

    override fun isCodeScanSupported(): Boolean = true

    override fun isImportAdderSupported(): Boolean = true

    override fun isClassifierSupported(): Boolean = true

    override fun isAllClassifier(): Boolean = true

    companion object {
        const val ID = "python"

        val INSTANCE = CodeWhispererPython()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererRuby private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Ruby

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "ruby"

        val INSTANCE = CodeWhispererRuby()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererRust private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Rust

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "rust"

        val INSTANCE = CodeWhispererRust()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererScala private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Scala

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "scala"

        val INSTANCE = CodeWhispererScala()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererShell private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Shell

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "shell"

        val INSTANCE = CodeWhispererShell()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererSql private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Sql

    override fun isCodeCompletionSupported(): Boolean = true

    companion object {
        const val ID = "sql"

        val INSTANCE = CodeWhispererSql()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererTsx private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Tsx

    override fun isCodeCompletionSupported(): Boolean = true

    override fun toCodeWhispererRuntimeLanguage(): CodeWhispererProgrammingLanguage = CodeWhispererTypeScript.INSTANCE

    override fun isClassifierSupported(): Boolean = true

    override fun isAllClassifier(): Boolean = true

    companion object {
        const val ID = "tsx"

        val INSTANCE = CodeWhispererTsx()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererTypeScript private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Typescript

    override fun isCodeCompletionSupported(): Boolean = true

    override fun isClassifierSupported(): Boolean = true

    override fun isAllClassifier(): Boolean = true

    companion object {
        const val ID = "typescript"

        val INSTANCE = CodeWhispererTypeScript()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.language.languages

import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.telemetry.CodewhispererLanguage

class CodeWhispererUnknownLanguage private constructor() : CodeWhispererProgrammingLanguage() {
    override val languageId: String = ID

    override fun toTelemetryType(): CodewhispererLanguage = CodewhispererLanguage.Unknown

    companion object {
        const val ID = "unknown"

        val INSTANCE = CodeWhispererUnknownLanguage()
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.layout

import com.intellij.util.ui.JBInsets
import java.awt.GridBagConstraints
import java.awt.GridBagConstraints.EAST
import java.awt.GridBagConstraints.HORIZONTAL
import java.awt.GridBagConstraints.WEST
import javax.swing.JLabel
import javax.swing.JPanel

object CodeWhispererLayoutConfig {
    val horizontalPanelConstraints = GridBagConstraints().apply {
        gridx = 0
        weightx = 1.0
        fill = HORIZONTAL
    }
    val navigationButtonConstraints = GridBagConstraints().apply {
        ipady = 6
    }
    val middleButtonConstraints = GridBagConstraints().apply {
        insets = JBInsets.create(0, 6)
        ipady = 6
    }
    val inlineLabelConstraints = GridBagConstraints().apply {
        anchor = WEST
    }
    val kebabMenuConstraints = GridBagConstraints().apply {
        anchor = EAST
    }
    private val horizontalGlueConstraints = GridBagConstraints().apply {
        gridy = 0
        weightx = 1.0
        weighty = 1.0
        fill = GridBagConstraints.BOTH
    }
    private val verticalGlueConstraints = GridBagConstraints().apply {
        gridx = 0
        weightx = 1.0
        weighty = 1.0
        fill = GridBagConstraints.BOTH
    }
    fun JPanel.addHorizontalGlue() {
        this.add(JLabel(), horizontalGlueConstraints)
    }
    fun JPanel.addVerticalGlue() {
        this.add(JLabel(), verticalGlueConstraints)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.model

import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.ui.popup.JBPopup
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.sessionconfig.PayloadContext
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererAutomatedTriggerType
import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestContext
import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.telemetry.CodewhispererTriggerType
import software.aws.toolkits.telemetry.Result
import java.util.concurrent.TimeUnit

data class Chunk(
    val content: String,
    val path: String,
    val nextChunk: String = "",
    val score: Double = 0.0
)

data class CaretContext(val leftFileContext: String, val rightFileContext: String, val leftContextOnCurrentLine: String = "")

data class FileContextInfo(
    val caretContext: CaretContext,
    val filename: String,
    val programmingLanguage: CodeWhispererProgrammingLanguage
)

data class SupplementalContextInfo(
    val isUtg: Boolean,
    val contents: List<Chunk>,
    val latency: Long,
    val targetFileName: String
) {
    val contentLength: Int
        get() = contents.fold(0) { acc, chunk ->
            acc + chunk.content.length
        }

    val isProcessTimeout: Boolean
        get() = latency > CodeWhispererConstants.SUPPLEMENTAL_CONTEXT_TIMEOUT
}

data class RecommendationContext(
    val details: List<DetailContext>,
    val userInputOriginal: String,
    val userInputSinceInvocation: String,
    val position: VisualPosition
)

data class DetailContext(
    val requestId: String,
    val recommendation: Completion,
    val reformatted: Completion,
    val isDiscarded: Boolean,
    val isTruncatedOnRight: Boolean,
    val rightOverlap: String = ""
)

data class SessionContext(
    val typeahead: String = "",
    val typeaheadOriginal: String = "",
    val selectedIndex: Int = 0,
    val seen: MutableSet<Int> = mutableSetOf(),
    val isFirstTimeShowingPopup: Boolean = true,
    var toBeRemovedHighlighter: RangeHighlighter? = null,
    var insertEndOffset: Int = -1
)

data class RecommendationChunk(
    val text: String,
    val offset: Int,
    val inlayOffset: Int
)

data class CaretPosition(val offset: Int, val line: Int)

data class TriggerTypeInfo(
    val triggerType: CodewhispererTriggerType,
    val automatedTriggerType: CodeWhispererAutomatedTriggerType,
)

data class InvocationContext(
    val requestContext: RequestContext,
    val responseContext: ResponseContext,
    val recommendationContext: RecommendationContext,
    val popup: JBPopup
) : Disposable {
    override fun dispose() {}
}

data class WorkerContext(
    val requestContext: RequestContext,
    val responseContext: ResponseContext,
    val response: GenerateCompletionsResponse,
    val popup: JBPopup
)

data class CodeScanTelemetryEvent(
    val codeScanResponseContext: CodeScanResponseContext,
    val duration: Double,
    val result: Result,
    val totalProjectSizeInBytes: Double?,
    val connection: ToolkitConnection?
)

data class CodeScanServiceInvocationContext(
    val artifactsUploadDuration: Long,
    val serviceInvocationDuration: Long
)

data class CodeScanResponseContext(
    val payloadContext: PayloadContext,
    val serviceInvocationContext: CodeScanServiceInvocationContext,
    val codeScanJobId: String? = null,
    val codeScanTotalIssues: Int = 0,
    val reason: String? = null
)

data class LatencyContext(
    var credentialFetchingStart: Long = 0L,
    var credentialFetchingEnd: Long = 0L,

    var codewhispererPreprocessingStart: Long = 0L,
    var codewhispererPreprocessingEnd: Long = 0L,

    var paginationFirstCompletionTime: Double = 0.0,

    var codewhispererPostprocessingStart: Long = 0L,
    var codewhispererPostprocessingEnd: Long = 0L,

    var codewhispererEndToEndStart: Long = 0L,
    var codewhispererEndToEndEnd: Long = 0L,

    var paginationAllCompletionsStart: Long = 0L,
    var paginationAllCompletionsEnd: Long = 0L,

    var firstRequestId: String = ""
) {
    fun getCodeWhispererEndToEndLatency() = TimeUnit.NANOSECONDS.toMillis(
        codewhispererEndToEndEnd - codewhispererEndToEndStart
    ).toDouble()

    fun getCodeWhispererAllCompletionsLatency() = TimeUnit.NANOSECONDS.toMillis(
        paginationAllCompletionsEnd - paginationAllCompletionsStart
    ).toDouble()

    fun getCodeWhispererPostprocessingLatency() = TimeUnit.NANOSECONDS.toMillis(
        codewhispererPostprocessingEnd - codewhispererPostprocessingStart
    ).toDouble()

    fun getCodeWhispererCredentialFetchingLatency() = TimeUnit.NANOSECONDS.toMillis(
        credentialFetchingEnd - credentialFetchingStart
    ).toDouble()

    fun getCodeWhispererPreprocessingLatency() = TimeUnit.NANOSECONDS.toMillis(
        codewhispererPreprocessingEnd - codewhispererPreprocessingStart
    ).toDouble()
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup

import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.ActionToolbar
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.ActionLink
import com.intellij.util.ui.UIUtil
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererLearnMoreAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererProvideFeedbackAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererShowSettingsAction
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.horizontalPanelConstraints
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.inlineLabelConstraints
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.kebabMenuConstraints
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.middleButtonConstraints
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.navigationButtonConstraints
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererLicenseInfoManager
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_BUTTON_BORDER
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_HOVER
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_PANEL_SEPARATOR
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_REF_INFO
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_REF_NOTICE_HEX
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_BUTTON_TEXT_SIZE
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE
import software.aws.toolkits.resources.message
import java.awt.GridBagLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.BorderFactory
import javax.swing.JButton
import javax.swing.JLabel
import javax.swing.JPanel

class CodeWhispererPopupComponents {
    val prevButton = createNavigationButton(
        message("codewhisperer.popup.button.prev", POPUP_DIM_HEX)
    )
    val nextButton = createNavigationButton(
        message("codewhisperer.popup.button.next", POPUP_DIM_HEX)
    ).apply {
        preferredSize = prevButton.preferredSize
    }
    val acceptButton = createNavigationButton(
        message("codewhisperer.popup.button.accept", POPUP_DIM_HEX)
    )
    val buttonsPanel = CodeWhispererPopupInfoPanel {
        border = BorderFactory.createCompoundBorder(
            border,
            BorderFactory.createEmptyBorder(3, 0, 3, 0)
        )
        add(acceptButton, navigationButtonConstraints)
        add(prevButton, middleButtonConstraints)
        add(nextButton, navigationButtonConstraints)
    }
    val recommendationInfoLabel = JLabel().apply {
        font = font.deriveFont(POPUP_INFO_TEXT_SIZE)
    }
    private val kebabMenuAction = DefaultActionGroup().apply {
        isPopup = true
        add(CodeWhispererProvideFeedbackAction())
        add(CodeWhispererLearnMoreAction())
        add(CodeWhispererShowSettingsAction())
    }
    private val kebabMenuPresentation = Presentation().apply {
        icon = AllIcons.Actions.More
        putClientProperty(ActionButton.HIDE_DROPDOWN_ICON, true)
    }
    private val kebabMenu = ActionButton(
        kebabMenuAction,
        kebabMenuPresentation,
        ActionPlaces.EDITOR_POPUP,
        ActionToolbar.NAVBAR_MINIMUM_BUTTON_SIZE
    )
    private val recommendationInfoPanel = CodeWhispererPopupInfoPanel {
        add(recommendationInfoLabel, inlineLabelConstraints)
        addHorizontalGlue()
        add(kebabMenu, kebabMenuConstraints)
    }
    val importLabel = JLabel().apply {
        font = font.deriveFont(POPUP_INFO_TEXT_SIZE)
    }

    val importPanel = CodeWhispererPopupInfoPanel {
        add(importLabel, inlineLabelConstraints)
        addHorizontalGlue()
    }

    val licenseCodeLabelPrefixText = JLabel().apply {
        text = message("codewhisperer.popup.reference.license_info.prefix", POPUP_REF_NOTICE_HEX)
        foreground = POPUP_REF_INFO
    }

    val codeReferencePanelLink = ActionLink(message("codewhisperer.popup.reference.panel_link"))
    val licenseCodePanel = JPanel(GridBagLayout()).apply {
        border = BorderFactory.createEmptyBorder(0, 0, 3, 0)
        add(licenseCodeLabelPrefixText, inlineLabelConstraints)
        add(ActionLink(), inlineLabelConstraints)
        add(codeReferencePanelLink, inlineLabelConstraints)
        addHorizontalGlue()
    }

    fun licenseLink(license: String) = ActionLink(license) {
        BrowserUtil.browse(CodeWhispererLicenseInfoManager.getInstance().getLicenseLink(license))
    }

    val codeReferencePanel = CodeWhispererPopupInfoPanel {
        add(licenseCodePanel, horizontalPanelConstraints)
    }
    val panel = JPanel(GridBagLayout()).apply {
        add(buttonsPanel, horizontalPanelConstraints)
        add(recommendationInfoPanel, horizontalPanelConstraints)
        add(importPanel, horizontalPanelConstraints)
        add(codeReferencePanel, horizontalPanelConstraints)
    }

    private fun createNavigationButton(buttonText: String) = JButton(buttonText).apply {
        font = font.deriveFont(POPUP_BUTTON_TEXT_SIZE)
        border = IdeBorderFactory.createRoundedBorder().apply {
            setColor(POPUP_BUTTON_BORDER)
        }
        isContentAreaFilled = false

        addMouseListener(object : MouseAdapter() {
            override fun mouseEntered(e: MouseEvent?) {
                foreground = POPUP_HOVER
            }

            override fun mouseClicked(e: MouseEvent?) {
                foreground = POPUP_HOVER
            }

            override fun mouseExited(e: MouseEvent?) {
                foreground = UIUtil.getLabelForeground()
            }
        })
    }

    class CodeWhispererPopupInfoPanel(function: CodeWhispererPopupInfoPanel.() -> Unit) : JPanel(GridBagLayout()) {
        init {
            border = BorderFactory.createCompoundBorder(
                BorderFactory.createMatteBorder(1, 0, 0, 0, POPUP_PANEL_SEPARATOR),
                BorderFactory.createEmptyBorder(2, 5, 2, 5)
            )
            function()
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup

import com.intellij.openapi.ui.popup.JBPopupListener
import com.intellij.openapi.ui.popup.LightweightWindowEvent
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
import java.time.Duration
import java.time.Instant

class CodeWhispererPopupListener(private val states: InvocationContext) : JBPopupListener {
    override fun beforeShown(event: LightweightWindowEvent) {
        super.beforeShown(event)
        CodeWhispererInvocationStatus.getInstance().setPopupStartTimestamp()
    }
    override fun onClosed(event: LightweightWindowEvent) {
        super.onClosed(event)
        val (requestContext, responseContext, recommendationContext) = states

        CodeWhispererTelemetryService.getInstance().sendUserDecisionEventForAll(
            requestContext,
            responseContext,
            recommendationContext,
            CodeWhispererPopupManager.getInstance().sessionContext,
            event.isOk,
            CodeWhispererInvocationStatus.getInstance().popupStartTimestamp?.let { Duration.between(it, Instant.now()) }
        )

        CodeWhispererInvocationStatus.getInstance().setPopupActive(false)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup

import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.hint.ParameterInfoController
import com.intellij.codeInsight.lookup.LookupManager
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_BACKSPACE
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_LEFT
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_TAB
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.actionSystem.EditorActionManager
import com.intellij.openapi.editor.actionSystem.TypedAction
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.EditorColorsListener
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.editor.event.SelectionEvent
import com.intellij.openapi.editor.event.SelectionListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.wm.WindowManager
import com.intellij.ui.ComponentUtil
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.popup.AbstractPopup
import com.intellij.util.messages.Topic
import com.intellij.util.ui.UIUtil
import software.amazon.awssdk.services.codewhispererruntime.model.Import
import software.amazon.awssdk.services.codewhispererruntime.model.Reference
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.addHorizontalGlue
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.horizontalPanelConstraints
import software.aws.toolkits.jetbrains.services.codewhisperer.layout.CodeWhispererLayoutConfig.inlineLabelConstraints
import software.aws.toolkits.jetbrains.services.codewhisperer.model.DetailContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererEditorActionHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupBackspaceHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupEnterHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupLeftArrowHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupRightArrowHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupTabHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers.CodeWhispererPopupTypedHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererAcceptButtonActionListener
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererActionListener
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererNextButtonActionListener
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererPrevButtonActionListener
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners.CodeWhispererScrollListener
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceManager
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.POPUP_DIM_HEX
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.POPUP_INFO_TEXT_SIZE
import software.aws.toolkits.resources.message
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ComponentListener
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JLabel

class CodeWhispererPopupManager {
    val popupComponents = CodeWhispererPopupComponents()

    var shouldListenerCancelPopup: Boolean = true
        private set
    var sessionContext = SessionContext()
        private set

    init {
        // Listen for global scheme changes
        ApplicationManager.getApplication().messageBus.connect().subscribe(
            EditorColorsManager.TOPIC,
            EditorColorsListener { scheme ->
                if (scheme == null) return@EditorColorsListener
                popupComponents.apply {
                    panel.background = scheme.defaultBackground
                    panel.components.forEach {
                        it.background = scheme.getColor(EditorColors.DOCUMENTATION_COLOR)
                        it.foreground = scheme.defaultForeground
                    }
                    buttonsPanel.components.forEach {
                        it.foreground = UIUtil.getLabelForeground()
                    }
                    recommendationInfoLabel.foreground = UIUtil.getLabelForeground()
                    codeReferencePanel.components.forEach {
                        it.background = scheme.getColor(EditorColors.DOCUMENTATION_COLOR)
                        it.foreground = UIUtil.getLabelForeground()
                    }
                }
            }
        )
    }

    fun changeStates(
        states: InvocationContext,
        indexChange: Int,
        typeaheadChange: String,
        typeaheadAdded: Boolean,
        recommendationAdded: Boolean = false
    ) {
        val (_, _, recommendationContext, popup) = states
        val (details) = recommendationContext
        if (recommendationAdded) {
            LOG.debug {
                "Add recommendations to the existing CodeWhisperer session, current number of recommendations: ${details.size}"
            }
            ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED)
                .recommendationAdded(states, sessionContext)
            return
        }
        val typeaheadOriginal =
            if (typeaheadAdded) {
                sessionContext.typeaheadOriginal + typeaheadChange
            } else {
                if (typeaheadChange.length > sessionContext.typeaheadOriginal.length) {
                    cancelPopup(popup)
                    return
                }
                sessionContext.typeaheadOriginal.substring(
                    0,
                    sessionContext.typeaheadOriginal.length - typeaheadChange.length
                )
            }
        val isReverse = indexChange < 0
        val userInput = states.recommendationContext.userInputSinceInvocation
        val validCount = getValidCount(details, userInput, typeaheadOriginal)
        val validSelectedIndex = getValidSelectedIndex(details, userInput, sessionContext.selectedIndex, typeaheadOriginal)
        if ((validSelectedIndex == validCount - 1 && indexChange == 1) ||
            (validSelectedIndex == 0 && indexChange == -1)
        ) {
            return
        }
        val selectedIndex = findNewSelectedIndex(
            isReverse,
            details,
            userInput,
            sessionContext.selectedIndex + indexChange,
            typeaheadOriginal
        )
        if (selectedIndex == -1 || !isValidRecommendation(details[selectedIndex], userInput, typeaheadOriginal)) {
            LOG.debug { "None of the recommendation is valid at this point, cancelling the popup" }
            cancelPopup(popup)
            return
        }
        val typeahead = resolveTypeahead(states, selectedIndex, typeaheadOriginal)
        val isFirstTimeShowingPopup = indexChange == 0 && typeaheadChange.isEmpty()
        sessionContext = SessionContext(
            typeahead,
            typeaheadOriginal,
            selectedIndex,
            sessionContext.seen,
            isFirstTimeShowingPopup,
            sessionContext.toBeRemovedHighlighter
        )

        ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_POPUP_STATE_CHANGED).stateChanged(
            states,
            sessionContext
        )
    }

    private fun resolveTypeahead(states: InvocationContext, selectedIndex: Int, typeahead: String): String {
        val recommendation = states.recommendationContext.details[selectedIndex].reformatted.content()
        val userInput = states.recommendationContext.userInputSinceInvocation
        var indexOfFirstNonWhiteSpace = typeahead.indexOfFirst { !it.isWhitespace() }
        if (indexOfFirstNonWhiteSpace == -1) {
            indexOfFirstNonWhiteSpace = typeahead.length
        }

        for (i in 0..indexOfFirstNonWhiteSpace) {
            val subTypeahead = typeahead.substring(i)
            if (recommendation.startsWith(userInput + subTypeahead)) return subTypeahead
        }
        return typeahead
    }

    fun updatePopupPanel(states: InvocationContext, sessionContext: SessionContext) {
        val userInput = states.recommendationContext.userInputSinceInvocation
        val details = states.recommendationContext.details
        val selectedIndex = sessionContext.selectedIndex
        val typeaheadOriginal = sessionContext.typeaheadOriginal
        val validCount = getValidCount(details, userInput, typeaheadOriginal)
        val validSelectedIndex = getValidSelectedIndex(details, userInput, selectedIndex, typeaheadOriginal)
        updateSelectedRecommendationLabelText(validSelectedIndex, validCount)
        updateNavigationPanel(validSelectedIndex, validCount)
        updateImportPanel(details[selectedIndex].recommendation.mostRelevantMissingImports())
        updateCodeReferencePanel(states.requestContext.project, details[selectedIndex].recommendation.references())
    }

    fun render(
        states: InvocationContext,
        sessionContext: SessionContext,
        overlappingLinesCount: Int,
        isRecommendationAdded: Boolean,
        isScrolling: Boolean
    ) {
        updatePopupPanel(states, sessionContext)

        val caretPoint = states.requestContext.editor.offsetToXY(states.requestContext.caretPosition.offset)
        sessionContext.seen.add(sessionContext.selectedIndex)

        // There are four cases that render() is called:
        // 1. Popup showing for the first time, both booleans are false, we should show the popup and update the latency
        // end time, and emit the event if it's at the pagination end.
        // 2. New recommendations being added to the existing ones, we should not update the latency end time, and emit
        // the event if it's at the pagination end.
        // 3. User scrolling (so popup is changing positions), we should not update the latency end time and should not
        // emit any events.
        // 4. User nagivating through the completions or typing as the completion shows. We should not update the latency
        // end time and should not emit any events in this case.
        if (!isRecommendationAdded) {
            showPopup(states, sessionContext, states.popup, caretPoint, overlappingLinesCount)
            if (!isScrolling) {
                states.requestContext.latencyContext.codewhispererPostprocessingEnd = System.nanoTime()
                states.requestContext.latencyContext.codewhispererEndToEndEnd = System.nanoTime()
            }
        }
        if (isScrolling ||
            CodeWhispererInvocationStatus.getInstance().hasExistingInvocation() ||
            !sessionContext.isFirstTimeShowingPopup
        ) {
            return
        }
        CodeWhispererTelemetryService.getInstance().sendClientComponentLatencyEvent(states)
    }

    fun dontClosePopupAndRun(runnable: () -> Unit) {
        try {
            shouldListenerCancelPopup = false
            runnable()
        } finally {
            shouldListenerCancelPopup = true
        }
    }

    fun reset() {
        sessionContext = SessionContext()
    }

    fun cancelPopup(popup: JBPopup) {
        popup.cancel()
    }

    fun closePopup(popup: JBPopup) {
        popup.closeOk(null)
    }

    fun showPopup(
        states: InvocationContext,
        sessionContext: SessionContext,
        popup: JBPopup,
        p: Point,
        overlappingLinesCount: Int
    ) {
        val editor = states.requestContext.editor
        val detailContexts = states.recommendationContext.details
        val userInputOriginal = states.recommendationContext.userInputOriginal
        val userInput = states.recommendationContext.userInputSinceInvocation
        val selectedIndex = sessionContext.selectedIndex
        val typeaheadOriginal = sessionContext.typeaheadOriginal
        val typeahead = sessionContext.typeahead
        val userInputLines = userInputOriginal.split("\n").size - 1
        val lineCount = getReformattedRecommendation(detailContexts[selectedIndex], userInput).split("\n").size
        val additionalLines = typeaheadOriginal.split("\n").size - typeahead.split("\n").size
        val popupSize = (popup as AbstractPopup).preferredContentSize
        val yBelowLastLine = p.y + (lineCount + additionalLines + userInputLines - overlappingLinesCount) * editor.lineHeight
        val yAboveFirstLine = p.y - popupSize.height + (additionalLines + userInputLines) * editor.lineHeight
        val editorRect = editor.scrollingModel.visibleArea
        var popupRect = Rectangle(p.x, yBelowLastLine, popupSize.width, popupSize.height)
        var shouldHidePopup = false

        CodeWhispererInvocationStatus.getInstance().setPopupActive(true)

        // Check if the current editor still has focus. If not, don't show the popup.
        if (!editor.contentComponent.isFocusOwner) {
            cancelPopup(popup)
            return
        }

        val popupLocation =
            if (!editorRect.contains(popupRect)) {
                popupRect = Rectangle(p.x, yAboveFirstLine, popupSize.width, popupSize.height)
                if (!editorRect.contains(popupRect)) {
                    // both popup location (below last line and above first line) don't work, so don't show the popup
                    shouldHidePopup = true
                }
                LOG.debug {
                    "Show popup above the first line of recommendation. " +
                        "Editor position: $editorRect, popup position: $popupRect"
                }
                Point(p.x, yAboveFirstLine)
            } else {
                LOG.debug {
                    "Show popup below the last line of recommendation. " +
                        "Editor position: $editorRect, popup position: $popupRect"
                }
                Point(p.x, yBelowLastLine)
            }

        val relativePopupLocationToEditor = RelativePoint(editor.contentComponent, popupLocation)
        if (popup.isVisible) {
            if (!shouldHidePopup) {
                popup.setLocation(relativePopupLocationToEditor.screenPoint)
                popup.size = popup.preferredContentSize
            }
        } else {
            val originalAutoPopupCompletionLookup = CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP
            CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP = false
            Disposer.register(popup) {
                CodeInsightSettings.getInstance().AUTO_POPUP_COMPLETION_LOOKUP = originalAutoPopupCompletionLookup
            }
            popup.show(relativePopupLocationToEditor)
            val perceivedLatency = CodeWhispererInvocationStatus.getInstance().getTimeSinceDocumentChanged()
            CodeWhispererTelemetryService.getInstance().sendPerceivedLatencyEvent(
                detailContexts[selectedIndex].requestId,
                states.requestContext,
                states.responseContext,
                perceivedLatency
            )
        }
        if (shouldHidePopup) {
            WindowManager.getInstance().setAlphaModeRatio(popup.popupWindow, 1f)
        } else {
            WindowManager.getInstance().setAlphaModeRatio(popup.popupWindow, 0.1f)
        }
    }

    fun initPopup(): JBPopup = JBPopupFactory.getInstance()
        .createComponentPopupBuilder(popupComponents.panel, null)
        .setAlpha(0.1F)
        .setCancelOnClickOutside(true)
        .setCancelOnOtherWindowOpen(true)
        .setCancelKeyEnabled(true)
        .setCancelOnWindowDeactivation(true)
        .createPopup()

    fun getReformattedRecommendation(detailContext: DetailContext, userInput: String) =
        detailContext.reformatted.content().substring(userInput.length)

    fun initPopupListener(states: InvocationContext) {
        addPopupListener(states)
        states.requestContext.editor.scrollingModel.addVisibleAreaListener(CodeWhispererScrollListener(states), states)
        addButtonActionListeners(states)
        addMessageSubscribers(states)
        setPopupActionHandlers(states)
        addComponentListeners(states)
    }

    private fun addPopupListener(states: InvocationContext) {
        val listener = CodeWhispererPopupListener(states)
        states.popup.addListener(listener)
        Disposer.register(states) { states.popup.removeListener(listener) }
    }

    private fun addMessageSubscribers(states: InvocationContext) {
        val connect = ApplicationManager.getApplication().messageBus.connect(states)
        connect.subscribe(
            CODEWHISPERER_USER_ACTION_PERFORMED,
            object : CodeWhispererUserActionListener {
                override fun navigateNext(states: InvocationContext) {
                    changeStates(states, 1, "", true)
                }

                override fun navigatePrevious(states: InvocationContext) {
                    changeStates(states, -1, "", true)
                }

                override fun backspace(states: InvocationContext, diff: String) {
                    changeStates(states, 0, diff, false)
                }

                override fun enter(states: InvocationContext, diff: String) {
                    changeStates(states, 0, diff, true)
                }

                override fun type(states: InvocationContext, diff: String) {
                    // remove the character at primaryCaret if it's the same as the typed character
                    val caretOffset = states.requestContext.editor.caretModel.primaryCaret.offset
                    val document = states.requestContext.editor.document
                    val text = document.charsSequence
                    if (caretOffset < text.length && diff == text[caretOffset].toString()) {
                        WriteCommandAction.runWriteCommandAction(states.requestContext.project) {
                            document.deleteString(caretOffset, caretOffset + 1)
                        }
                    }
                    changeStates(states, 0, diff, true)
                }

                override fun beforeAccept(states: InvocationContext, sessionContext: SessionContext) {
                    dontClosePopupAndRun {
                        CodeWhispererEditorManager.getInstance().updateEditorWithRecommendation(states, sessionContext)
                    }
                    closePopup(states.popup)
                }
            }
        )
    }

    private fun addButtonActionListeners(states: InvocationContext) {
        popupComponents.prevButton.addButtonActionListener(CodeWhispererPrevButtonActionListener(states))
        popupComponents.nextButton.addButtonActionListener(CodeWhispererNextButtonActionListener(states))
        popupComponents.acceptButton.addButtonActionListener(CodeWhispererAcceptButtonActionListener(states))
    }

    private fun JButton.addButtonActionListener(listener: CodeWhispererActionListener) {
        this.addActionListener(listener)
        Disposer.register(listener.states) { this.removeActionListener(listener) }
    }

    private fun setPopupActionHandlers(states: InvocationContext) {
        val actionManager = EditorActionManager.getInstance()
        setPopupTypedHandler(CodeWhispererPopupTypedHandler(TypedAction.getInstance().rawHandler, states))
        setPopupActionHandler(ACTION_EDITOR_TAB, CodeWhispererPopupTabHandler(states))
        setPopupActionHandler(ACTION_EDITOR_MOVE_CARET_LEFT, CodeWhispererPopupLeftArrowHandler(states))
        setPopupActionHandler(ACTION_EDITOR_MOVE_CARET_RIGHT, CodeWhispererPopupRightArrowHandler(states))
        setPopupActionHandler(
            ACTION_EDITOR_ENTER,
            CodeWhispererPopupEnterHandler(actionManager.getActionHandler(ACTION_EDITOR_ENTER), states)
        )
        setPopupActionHandler(
            ACTION_EDITOR_BACKSPACE,
            CodeWhispererPopupBackspaceHandler(actionManager.getActionHandler(ACTION_EDITOR_BACKSPACE), states)
        )
    }

    private fun setPopupTypedHandler(newHandler: CodeWhispererPopupTypedHandler) {
        val oldTypedHandler = TypedAction.getInstance().setupRawHandler(newHandler)
        Disposer.register(newHandler.states) { TypedAction.getInstance().setupRawHandler(oldTypedHandler) }
    }

    private fun setPopupActionHandler(id: String, newHandler: CodeWhispererEditorActionHandler) {
        val oldHandler = EditorActionManager.getInstance().setActionHandler(id, newHandler)
        Disposer.register(newHandler.states) { EditorActionManager.getInstance().setActionHandler(id, oldHandler) }
    }

    private fun addComponentListeners(states: InvocationContext) {
        val editor = states.requestContext.editor
        val codewhispererSelectionListener: SelectionListener = object : SelectionListener {
            override fun selectionChanged(event: SelectionEvent) {
                if (shouldListenerCancelPopup) {
                    cancelPopup(states.popup)
                }
                super.selectionChanged(event)
            }
        }
        editor.selectionModel.addSelectionListener(codewhispererSelectionListener)
        Disposer.register(states) { editor.selectionModel.removeSelectionListener(codewhispererSelectionListener) }

        val codewhispererDocumentListener: DocumentListener = object : DocumentListener {
            override fun documentChanged(event: DocumentEvent) {
                if (shouldListenerCancelPopup) {
                    cancelPopup(states.popup)
                }
                super.documentChanged(event)
            }
        }
        editor.document.addDocumentListener(codewhispererDocumentListener, states)

        val codewhispererCaretListener: CaretListener = object : CaretListener {
            override fun caretPositionChanged(event: CaretEvent) {
                if (shouldListenerCancelPopup) {
                    cancelPopup(states.popup)
                }
                super.caretPositionChanged(event)
            }
        }
        editor.caretModel.addCaretListener(codewhispererCaretListener)
        Disposer.register(states) { editor.caretModel.removeCaretListener(codewhispererCaretListener) }

        val editorComponent = editor.contentComponent
        if (editorComponent.isShowing) {
            val window = ComponentUtil.getWindow(editorComponent)
            val windowListener: ComponentListener = object : ComponentAdapter() {
                override fun componentMoved(event: ComponentEvent) {
                    cancelPopup(states.popup)
                }

                override fun componentShown(e: ComponentEvent?) {
                    cancelPopup(states.popup)
                    super.componentShown(e)
                }
            }
            window?.addComponentListener(windowListener)
            Disposer.register(states) { window?.removeComponentListener(windowListener) }
        }
    }

    private fun updateSelectedRecommendationLabelText(validSelectedIndex: Int, validCount: Int) {
        if (CodeWhispererInvocationStatus.getInstance().hasExistingInvocation()) {
            popupComponents.recommendationInfoLabel.text = message("codewhisperer.popup.pagination_info")
            LOG.debug { "Pagination in progress. Current total: $validCount" }
        } else {
            popupComponents.recommendationInfoLabel.text =
                message(
                    "codewhisperer.popup.recommendation_info",
                    validSelectedIndex + 1,
                    validCount,
                    POPUP_DIM_HEX
                )
            LOG.debug { "Updated popup recommendation label text. Index: $validSelectedIndex, total: $validCount" }
        }
    }

    private fun updateNavigationPanel(validSelectedIndex: Int, validCount: Int) {
        val multipleRecommendation = validCount > 1
        popupComponents.prevButton.isEnabled = multipleRecommendation && validSelectedIndex != 0
        popupComponents.nextButton.isEnabled = multipleRecommendation && validSelectedIndex != validCount - 1
    }

    private fun updateImportPanel(imports: List<Import>) {
        popupComponents.panel.apply {
            if (components.contains(popupComponents.importPanel)) {
                remove(popupComponents.importPanel)
            }
        }
        if (imports.isEmpty()) return

        val firstImport = imports.first()
        val choice = if (imports.size > 2) 2 else imports.size - 1
        val message = message("codewhisperer.popup.import_info", firstImport.statement(), imports.size - 1, choice)
        popupComponents.panel.add(popupComponents.importPanel, horizontalPanelConstraints)
        popupComponents.importLabel.text = message
    }

    private fun updateCodeReferencePanel(project: Project, references: List<Reference>) {
        popupComponents.panel.apply {
            if (components.contains(popupComponents.codeReferencePanel)) {
                remove(popupComponents.codeReferencePanel)
            }
        }
        if (references.isEmpty()) return

        popupComponents.panel.add(popupComponents.codeReferencePanel, horizontalPanelConstraints)
        val licenses = references.map { it.licenseName() }.toSet()
        popupComponents.codeReferencePanelLink.apply {
            actionListeners.toList().forEach {
                removeActionListener(it)
            }
            addActionListener {
                CodeWhispererCodeReferenceManager.getInstance(project).showCodeReferencePanel()
            }
        }
        popupComponents.licenseCodePanel.apply {
            removeAll()
            add(popupComponents.licenseCodeLabelPrefixText, inlineLabelConstraints)
            licenses.forEachIndexed { i, license ->
                add(popupComponents.licenseLink(license), inlineLabelConstraints)
                if (i == licenses.size - 1) return@forEachIndexed
                add(JLabel(", "), inlineLabelConstraints)
            }

            add(JLabel(".  "), inlineLabelConstraints)
            add(popupComponents.codeReferencePanelLink, inlineLabelConstraints)
            addHorizontalGlue()
        }
        popupComponents.licenseCodePanel.components.forEach {
            if (it !is JComponent) return@forEach
            it.font = it.font.deriveFont(POPUP_INFO_TEXT_SIZE)
        }
    }

    fun hasConflictingPopups(editor: Editor): Boolean =
        ParameterInfoController.existsWithVisibleHintForEditor(editor, true) ||
            LookupManager.getActiveLookup(editor) != null

    private fun findNewSelectedIndex(
        isReverse: Boolean,
        detailContexts: List<DetailContext>,
        userInput: String,
        start: Int,
        typeahead: String
    ): Int {
        val count = detailContexts.size
        val unit = if (isReverse) -1 else 1
        var currIndex: Int
        for (i in 0 until count) {
            currIndex = (start + i * unit) % count
            if (currIndex < 0) {
                currIndex += count
            }
            if (isValidRecommendation(detailContexts[currIndex], userInput, typeahead)) {
                return currIndex
            }
        }
        return -1
    }

    private fun getValidCount(detailContexts: List<DetailContext>, userInput: String, typeahead: String): Int =
        detailContexts.filter { isValidRecommendation(it, userInput, typeahead) }.size

    private fun getValidSelectedIndex(
        detailContexts: List<DetailContext>,
        userInput: String,
        selectedIndex: Int,
        typeahead: String
    ): Int {
        var currIndexIgnoreInvalid = 0
        detailContexts.forEachIndexed { index, value ->
            if (index == selectedIndex) {
                return currIndexIgnoreInvalid
            }
            if (isValidRecommendation(value, userInput, typeahead)) {
                currIndexIgnoreInvalid++
            }
        }
        return -1
    }

    private fun isValidRecommendation(detailContext: DetailContext, userInput: String, typeahead: String): Boolean {
        if (detailContext.isDiscarded) return false
        if (detailContext.recommendation.content().isEmpty()) return false
        val indexOfFirstNonWhiteSpace = typeahead.indexOfFirst { !it.isWhitespace() }
        if (indexOfFirstNonWhiteSpace == -1) return true

        for (i in 0..indexOfFirstNonWhiteSpace) {
            val subTypeahead = typeahead.substring(i)
            if (detailContext.reformatted.content().startsWith(userInput + subTypeahead)) return true
        }
        return false
    }

    companion object {
        private val LOG = getLogger<CodeWhispererPopupManager>()
        fun getInstance(): CodeWhispererPopupManager = service()
        val CODEWHISPERER_POPUP_STATE_CHANGED: Topic<CodeWhispererPopupStateChangeListener> = Topic.create(
            "CodeWhisperer popup state changed",
            CodeWhispererPopupStateChangeListener::class.java
        )
        val CODEWHISPERER_USER_ACTION_PERFORMED: Topic<CodeWhispererUserActionListener> = Topic.create(
            "CodeWhisperer user action performed",
            CodeWhispererUserActionListener::class.java
        )
    }
}

interface CodeWhispererPopupStateChangeListener {
    fun stateChanged(states: InvocationContext, sessionContext: SessionContext) {}
    fun scrolled(states: InvocationContext, sessionContext: SessionContext) {}
    fun recommendationAdded(states: InvocationContext, sessionContext: SessionContext) {}
}

interface CodeWhispererUserActionListener {
    fun backspace(states: InvocationContext, diff: String) {}
    fun enter(states: InvocationContext, diff: String) {}
    fun type(states: InvocationContext, diff: String) {}
    fun navigatePrevious(states: InvocationContext) {}
    fun navigateNext(states: InvocationContext) {}
    fun beforeAccept(states: InvocationContext, sessionContext: SessionContext) {}
    fun afterAccept(states: InvocationContext, sessionContext: SessionContext, rangeMarker: RangeMarker) {}
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup

import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.Disposer
import com.intellij.xdebugger.ui.DebuggerColors
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager
import software.aws.toolkits.jetbrains.services.codewhisperer.inlay.CodeWhispererInlayManager
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.RecommendationChunk
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContext
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererRecommendationManager

class CodeWhispererUIChangeListener : CodeWhispererPopupStateChangeListener {
    override fun stateChanged(states: InvocationContext, sessionContext: SessionContext) {
        val editor = states.requestContext.editor
        val editorManager = CodeWhispererEditorManager.getInstance()
        val selectedIndex = sessionContext.selectedIndex
        val typeahead = sessionContext.typeahead
        val detail = states.recommendationContext.details[selectedIndex]
        val caretOffset = editor.caretModel.primaryCaret.offset
        val document = editor.document
        val lineEndOffset = document.getLineEndOffset(document.getLineNumber(caretOffset))

        // get matching brackets from recommendations to the brackets after caret position
        val remaining = CodeWhispererPopupManager.getInstance().getReformattedRecommendation(
            detail,
            states.recommendationContext.userInputSinceInvocation
        ).substring(typeahead.length)

        val remainingLines = remaining.split("\n")
        val firstLineOfRemaining = remainingLines.first()
        val otherLinesOfRemaining = remainingLines.drop(1)

        // process first line inlays, where we do subsequence matching as much as possible
        val matchingSymbols = editorManager.getMatchingSymbolsFromRecommendation(
            editor,
            firstLineOfRemaining,
            detail.isTruncatedOnRight,
            sessionContext
        )

        sessionContext.toBeRemovedHighlighter?.let {
            editor.markupModel.removeHighlighter(it)
        }

        // Add the strike-though hint for the remaining non-matching first-line right context for multi-line completions
        if (!detail.isTruncatedOnRight && otherLinesOfRemaining.isNotEmpty()) {
            val rangeHighlighter = editor.markupModel.addRangeHighlighter(
                matchingSymbols[matchingSymbols.size - 2].second,
                lineEndOffset,
                HighlighterLayer.LAST + 1,
                TextAttributes().apply {
                    effectType = EffectType.STRIKEOUT
                    effectColor = editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE).foregroundColor
                },
                HighlighterTargetArea.EXACT_RANGE
            )
            Disposer.register(states.popup) {
                editor.markupModel.removeHighlighter(rangeHighlighter)
            }
            sessionContext.toBeRemovedHighlighter = rangeHighlighter
        }

        val chunks = CodeWhispererRecommendationManager.getInstance().buildRecommendationChunks(
            firstLineOfRemaining,
            matchingSymbols
        )

        // process other lines inlays, where we do tail-head matching as much as possible
        val overlappingLinesCount = editorManager.findOverLappingLines(
            editor,
            otherLinesOfRemaining,
            detail.isTruncatedOnRight,
            sessionContext
        )

        var otherLinesInlayText = ""
        otherLinesOfRemaining.subList(0, otherLinesOfRemaining.size - overlappingLinesCount).forEach {
            otherLinesInlayText += "\n" + it
        }

        // inlay chunks are chunks from first line(chunks) and an additional chunk from other lines
        val inlayChunks = chunks + listOf(RecommendationChunk(otherLinesInlayText, 0, chunks.last().inlayOffset))
        CodeWhispererInlayManager.getInstance().updateInlays(states, inlayChunks)
        CodeWhispererPopupManager.getInstance().render(
            states,
            sessionContext,
            overlappingLinesCount,
            isRecommendationAdded = false,
            isScrolling = false
        )
    }

    override fun scrolled(states: InvocationContext, sessionContext: SessionContext) {
        if (states.popup.isDisposed) return
        val editor = states.requestContext.editor
        val editorManager = CodeWhispererEditorManager.getInstance()
        val selectedIndex = sessionContext.selectedIndex
        val typeahead = sessionContext.typeahead
        val detail = states.recommendationContext.details[selectedIndex]

        // get matching brackets from recommendations to the brackets after caret position
        val remaining = CodeWhispererPopupManager.getInstance().getReformattedRecommendation(
            detail,
            states.recommendationContext.userInputSinceInvocation
        ).substring(typeahead.length)

        val remainingLines = remaining.split("\n")
        val otherLinesOfRemaining = remainingLines.drop(1)

        // process other lines inlays, where we do tail-head matching as much as possible
        val overlappingLinesCount = editorManager.findOverLappingLines(
            editor,
            otherLinesOfRemaining,
            detail.isTruncatedOnRight,
            sessionContext
        )

        CodeWhispererPopupManager.getInstance().render(
            states,
            sessionContext,
            overlappingLinesCount,
            isRecommendationAdded = false,
            isScrolling = true
        )
    }

    override fun recommendationAdded(states: InvocationContext, sessionContext: SessionContext) {
        CodeWhispererPopupManager.getInstance().render(
            states,
            sessionContext,
            0,
            isRecommendationAdded = true,
            isScrolling = false
        )
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers

import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext

abstract class CodeWhispererEditorActionHandler(val states: InvocationContext) : EditorActionHandler()


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager

class CodeWhispererPopupBackspaceHandler(
    private val defaultHandler: EditorActionHandler,
    states: InvocationContext
) : CodeWhispererEditorActionHandler(states) {
    override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
        val popupManager = CodeWhispererPopupManager.getInstance()
        popupManager.dontClosePopupAndRun {
            val oldOffset = editor.caretModel.offset
            defaultHandler.execute(editor, caret, dataContext)
            val newOffset = editor.caretModel.offset
            val newText = "a".repeat(oldOffset - newOffset)
            ApplicationManager.getApplication().messageBus.syncPublisher(
                CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
            ).backspace(states, newText)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.util.TextRange
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager

class CodeWhispererPopupEnterHandler(
    private val defaultHandler: EditorActionHandler,
    states: InvocationContext
) : CodeWhispererEditorActionHandler(states) {
    override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
        val popupManager = CodeWhispererPopupManager.getInstance()
        popupManager.dontClosePopupAndRun {
            val oldOffset = editor.caretModel.offset
            defaultHandler.execute(editor, caret, dataContext)
            val newOffset = editor.caretModel.offset
            val newText = editor.document.getText(TextRange.create(oldOffset, newOffset))
            ApplicationManager.getApplication().messageBus.syncPublisher(
                CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
            ).enter(states, newText)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager

class CodeWhispererPopupLeftArrowHandler(states: InvocationContext) : CodeWhispererEditorActionHandler(states) {
    override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
        ApplicationManager.getApplication().messageBus.syncPublisher(
            CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
        ).navigatePrevious(states)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager

class CodeWhispererPopupRightArrowHandler(states: InvocationContext) : CodeWhispererEditorActionHandler(states) {
    override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
        ApplicationManager.getApplication().messageBus.syncPublisher(
            CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
        ).navigateNext(states)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager

class CodeWhispererPopupTabHandler(states: InvocationContext) : CodeWhispererEditorActionHandler(states) {
    override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
        ApplicationManager.getApplication().messageBus.syncPublisher(
            CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
        ).beforeAccept(states, CodeWhispererPopupManager.getInstance().sessionContext)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.handlers

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager

class CodeWhispererPopupTypedHandler(
    private val defaultHandler: TypedActionHandler,
    val states: InvocationContext,
) : TypedActionHandler {
    override fun execute(editor: Editor, charTyped: Char, dataContext: DataContext) {
        CodeWhispererPopupManager.getInstance().dontClosePopupAndRun {
            defaultHandler.execute(editor, charTyped, dataContext)
            ApplicationManager.getApplication().messageBus.syncPublisher(
                CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
            ).type(states, charTyped.toString())
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners

import com.intellij.openapi.application.ApplicationManager
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import java.awt.event.ActionEvent

class CodeWhispererAcceptButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) {
    override fun actionPerformed(e: ActionEvent?) {
        ApplicationManager.getApplication().messageBus.syncPublisher(
            CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
        ).beforeAccept(states, CodeWhispererPopupManager.getInstance().sessionContext)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners

import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import java.awt.event.ActionListener

abstract class CodeWhispererActionListener(val states: InvocationContext) : ActionListener


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners

import com.intellij.openapi.application.ApplicationManager
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import java.awt.event.ActionEvent

class CodeWhispererNextButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) {
    override fun actionPerformed(e: ActionEvent?) {
        ApplicationManager.getApplication().messageBus.syncPublisher(
            CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
        ).navigateNext(states)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners

import com.intellij.openapi.application.ApplicationManager
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import java.awt.event.ActionEvent

class CodeWhispererPrevButtonActionListener(states: InvocationContext) : CodeWhispererActionListener(states) {
    override fun actionPerformed(e: ActionEvent?) {
        ApplicationManager.getApplication().messageBus.syncPublisher(
            CodeWhispererPopupManager.CODEWHISPERER_USER_ACTION_PERFORMED
        ).navigatePrevious(states)
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.popup.listeners

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.event.VisibleAreaEvent
import com.intellij.openapi.editor.event.VisibleAreaListener
import software.aws.toolkits.jetbrains.services.codewhisperer.model.InvocationContext
import software.aws.toolkits.jetbrains.services.codewhisperer.popup.CodeWhispererPopupManager
import software.aws.toolkits.jetbrains.services.codewhisperer.service.CodeWhispererInvocationStatus

class CodeWhispererScrollListener(private val states: InvocationContext) : VisibleAreaListener {
    override fun visibleAreaChanged(e: VisibleAreaEvent) {
        val oldRect = e.oldRectangle
        val newRect = e.newRectangle
        if (CodeWhispererInvocationStatus.getInstance().isPopupActive() &&
            (oldRect.x != newRect.x || oldRect.y != newRect.y)
        ) {
            ApplicationManager.getApplication().messageBus.syncPublisher(
                CodeWhispererPopupManager.CODEWHISPERER_POPUP_STATE_CHANGED
            ).scrolled(states, CodeWhispererPopupManager.getInstance().sessionContext)
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.service

import software.aws.toolkits.telemetry.CodewhispererAutomatedTriggerType

sealed class CodeWhispererAutomatedTriggerType(
    val telemetryType: CodewhispererAutomatedTriggerType,
    var calculationResult: Double? = null
) {
    class Classifier : CodeWhispererAutomatedTriggerType(CodewhispererAutomatedTriggerType.Classifier)
    class SpecialChar(val specialChar: Char) :
        CodeWhispererAutomatedTriggerType(CodewhispererAutomatedTriggerType.SpecialCharacters)

    class Enter : CodeWhispererAutomatedTriggerType(CodewhispererAutomatedTriggerType.Enter)

    class IntelliSense :
        CodeWhispererAutomatedTriggerType(CodewhispererAutomatedTriggerType.IntelliSenseAcceptance)

    class IdleTime : CodeWhispererAutomatedTriggerType(CodewhispererAutomatedTriggerType.IdleTime)

    class Unknown : CodeWhispererAutomatedTriggerType(CodewhispererAutomatedTriggerType.Unknown)
}


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

package software.aws.toolkits.jetbrains.services.codewhisperer.service

import com.intellij.openapi.editor.Editor
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
import software.aws.toolkits.telemetry.CodewhispererTriggerType

interface CodeWhispererAutoTriggerHandler {
    fun performAutomatedTriggerAction(
        editor: Editor,
        automatedTriggerType: CodeWhispererAutomatedTriggerType,
        latencyContext: LatencyContext,
    ) {
        val triggerTypeInfo = TriggerTypeInfo(CodewhispererTriggerType.AutoTrigger, automatedTriggerType)

        LOG.debug { "autotriggering CodeWhisperer with type ${automatedTriggerType.telemetryType}" }
        CodeWhispererService.getInstance().showRecommendationsInPopup(editor, triggerTypeInfo, latencyContext)
    }

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