/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.convert
import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.file
import software.aws.mcs.cql2cfn.cfn.CfnTemplateGenerator
import software.aws.mcs.cql2cfn.cql.CqlScriptParser
import software.aws.mcs.cql2cfn.util.Logger
import java.io.File

class Main : CliktCommand() {
    private val cqlScript: String by argument(
        "CQL_SCRIPT",
        help = "Path to a CQL script to be converted"
    ).file(canBeDir = false, mustBeReadable = true).convert { it.readText() }
    private val cfnTemplateFile: File? by argument(
        "CFN_TEMPLATE",
        help = "Path to the generated CloudFormation template"
    ).file(canBeDir = false).optional()
    private val debug: Boolean by option(
        "--debug", "-d",
        help = "Enable debug logging"
    ).flag()
    private val strict: Boolean by option(
        "--strict", "-s",
        help = "Exit on warnings in addition to errors"
    ).flag()

    override fun run() {
        Logger.debugEnabled = debug
        Logger.exitOnWarn = strict
        try {
            val cfnTemplate = convert(cqlScript)
            cfnTemplateFile?.writeText(cfnTemplate) ?: println(cfnTemplate)
        } catch (e: Exception) {
            Logger.error("Encountered an unexpected error", e)
        }
    }

    internal fun convert(cqlScript: String): String =
        CfnTemplateGenerator().apply {
            CqlScriptParser.parse(cqlScript).forEach(::handle)
        }.generate()
}

fun main(args: Array<String>): Unit =
    Main().main(args)


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cfn

import software.amazon.awscdk.core.App
import software.amazon.awscdk.core.CfnResource
import software.amazon.awscdk.core.Stack
import software.aws.mcs.cql2cfn.cql.Identifier
import software.aws.mcs.cql2cfn.cql.Statement
import java.io.File

class CfnTemplateGenerator {
    private val app: App = App()

    val stack: Stack = Stack(app)
    var usedKeyspace: Identifier? = null
    val createdKeyspaces: MutableMap<Identifier, CfnResource> = mutableMapOf()
    val createdTypes: MutableMap<Pair<Identifier, Identifier>, CfnResource> = mutableMapOf()
    val createdTables: MutableMap<Pair<Identifier, Identifier>, CfnResource> = mutableMapOf()

    fun handle(statement: Statement): Unit =
        when (statement) {
            is Statement.Use ->
                UseHandler.handle(this, statement)
            is Statement.CreateKeyspace ->
                CreateKeyspaceHandler.handle(this, statement)
            is Statement.CreateTable ->
                CreateTableHandler.handle(this, statement)
        }

    fun generate(): String =
        File(app.synth().stacks.first().templateFullPath).readText()
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cfn

import software.amazon.awscdk.core.Tags
import software.amazon.awscdk.services.cassandra.CfnKeyspace
import software.aws.mcs.cql2cfn.cql.Identifier
import software.aws.mcs.cql2cfn.cql.PropertyMap
import software.aws.mcs.cql2cfn.cql.Statement
import software.aws.mcs.cql2cfn.util.Logger

object CreateKeyspaceHandler : StatementHandler<Statement.CreateKeyspace>() {
    override fun CfnTemplateGenerator.doHandle(statement: Statement.CreateKeyspace) {
        Logger.info("Processing CREATE KEYSPACE statement for '${statement.ksName}'")
        if (shouldSkipKeyspace(statement.ksName, statement.ifNotExists)) {
            return
        }
        val keyspaceBuilder = CfnKeyspace.Builder
            .create(stack, "Keyspace${createdKeyspaces.size + 1}")
            .keyspaceName(statement.ksName.name)
        val keyspace = keyspaceBuilder.build()
        processRegularProperties(statement.ksProps.regularProperties, keyspace)
        processCustomProperties(statement.ksProps.customProperties, keyspace)
        createdKeyspaces[statement.ksName] = keyspace
    }

    private fun CfnTemplateGenerator.shouldSkipKeyspace(
        ksName: Identifier,
        ifNotExists: Boolean
    ): Boolean =
        if (createdKeyspaces.containsKey(ksName)) {
            if (ifNotExists) {
                Logger.warn("Attempted to create keyspace again with IF NOT EXISTS specified")
                true
            } else {
                Logger.error("Cannot create more than one keyspace with the same name " +
                        "unless IF NOT EXISTS is specified")
            }
        } else {
            false
        }

    private fun CfnTemplateGenerator.processRegularProperties(
        regularProperties: PropertyMap<Identifier, Any>,
        keyspace: CfnKeyspace
    ) {
        if (!regularProperties.containsKey(Identifier("replication"))) {
            Logger.error("Missing required property 'replication'")
        }
        regularProperties.forEach { (key, value) ->
            when (key.name) {
                "replication" -> {
                    @Suppress("UNCHECKED_CAST")
                    val replication = (value as? PropertyMap<String, String>)?.toMutableMap()
                        ?: Logger.error("Invalid value for property 'replication'; it should be a map")
                    when (val clazz = replication.remove("class")) {
                        null ->
                            Logger.error("Missing required option 'class' of property 'replication'")
                        "SingleRegionStrategy" ->
                            if (replication.isNotEmpty()) {
                                Logger.error("Unrecognized option '${replication.keys.first()}' " +
                                        "for replication class 'SingleRegionStrategy'")
                            }
                        else ->
                            Logger.warn("Replication class '${clazz}' is not applicable to Keyspaces")
                    }
                }
                "tags" -> {
                    @Suppress("UNCHECKED_CAST")
                    val tags = value as? PropertyMap<String, String>
                        ?: Logger.error("Invalid value for property 'tags'; it should be a map")
                    Tags.of(keyspace).apply {
                        tags.forEach { add(it.key, it.value) }
                    }
                    Logger.error("Property 'tags' currently cannot be set via CloudFormation")
                }
                else ->
                    Logger.warn("Property '$key' is not applicable to Keyspaces")
            }
        }
    }

    @Suppress("UNUSED_PARAMETER")
    private fun CfnTemplateGenerator.processCustomProperties(
        customProperties: PropertyMap<String, PropertyMap<String, String>>?,
        keyspace: CfnKeyspace
    ) {
        if (customProperties != null) {
            Logger.error("Custom properties are not supported for keyspace yet")
        }
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cfn

import software.amazon.awscdk.core.Fn
import software.amazon.awscdk.core.Tags
import software.amazon.awscdk.services.cassandra.CfnTable
import software.aws.mcs.cql2cfn.cql.Identifier
import software.aws.mcs.cql2cfn.cql.PropertyMap
import software.aws.mcs.cql2cfn.cql.Statement
import software.aws.mcs.cql2cfn.cql.TableProperties
import software.aws.mcs.cql2cfn.cql.Type
import software.aws.mcs.cql2cfn.util.Logger

object CreateTableHandler : StatementHandler<Statement.CreateTable>() {
    override fun CfnTemplateGenerator.doHandle(statement: Statement.CreateTable) {
        Logger.info("Processing CREATE TABLE statement " +
                "for '${statement.ksName?.let { "$it." } ?: ""}${statement.cfName}'")
        val ksName = resolveKeyspace(statement.ksName)
        if (shouldSkipTable(ksName, statement.cfName, statement.ifNotExists)) {
            return
        }
        val tableBuilder = CfnTable.Builder
            .create(stack, "Table${createdTables.size + 1}")
            .keyspaceName(getKeyspaceNameOrRef(ksName))
            .tableName(statement.cfName.name)
        processColumns(
            ksName,
            statement.cfProps.columnDefinitions,
            statement.cfProps.staticColumnNames,
            statement.cfProps.partitionKeyColumnNames,
            statement.cfProps.clusteringKeyColumnNames,
            statement.cfProps.clusteringOrders,
            tableBuilder
        )
        val table = tableBuilder.build()
        processRegularProperties(statement.cfProps.regularProperties, table)
        processCustomProperties(statement.cfProps.customProperties, table)
        processCompactStorage(statement.cfProps.compactStorage)
        createdTables[Pair(ksName, statement.cfName)] = table
    }

    private fun CfnTemplateGenerator.resolveKeyspace(ksName: Identifier?): Identifier =
        (ksName ?: usedKeyspace) ?: Logger.error("No keyspace specified for table")

    private fun CfnTemplateGenerator.getKeyspaceNameOrRef(ksName: Identifier): String =
        createdKeyspaces[ksName]?.ref
            ?: ksName.name.also {
                Logger.warn("Attempted to create table in keyspace before creating it first")
            }

    private fun CfnTemplateGenerator.shouldSkipTable(
        ksName: Identifier,
        cfName: Identifier,
        ifNotExists: Boolean
    ): Boolean =
        if (createdTables.containsKey(Pair(ksName, cfName))) {
            if (ifNotExists) {
                Logger.warn("Attempted to create table again with IF NOT EXISTS specified")
                true
            } else {
                Logger.error("Cannot create more than one table with the same name " +
                        "unless IF NOT EXISTS is specified")
            }
        } else {
            false
        }

    private fun CfnTemplateGenerator.processColumns(
        ksName: Identifier,
        columnDefinitions: Map<Identifier, Type>,
        staticColumnNames: List<Identifier>,
        partitionKeyColumnNames: List<Identifier>,
        clusteringKeyColumnNames: List<Identifier>,
        clusteringOrders: Map<Identifier, TableProperties.ClusteringOrder>,
        tableBuilder: CfnTable.Builder
    ) {
        val allColumnDefinitions = columnDefinitions.toMutableMap()
        partitionKeyColumnNames.map { columnName ->
            val columnType = allColumnDefinitions.remove(columnName)!!
            CfnTable.ColumnProperty.builder()
                .columnName(columnName.name)
                .columnType(getTypeNameOrRef(ksName, columnType))
                .build()
        }.also { tableBuilder.partitionKeyColumns(it) }
        clusteringKeyColumnNames.map { columnName ->
            val columnType = allColumnDefinitions.remove(columnName)!!
            CfnTable.ClusteringKeyColumnProperty.builder()
                .column(CfnTable.ColumnProperty.builder()
                    .columnName(columnName.name)
                    .columnType(getTypeNameOrRef(ksName, columnType))
                    .build())
                .orderBy(clusteringOrders[columnName]?.name)
                .build()
        }.also { tableBuilder.clusteringKeyColumns(it) }
        if (staticColumnNames.isNotEmpty()) {
            Logger.error("Static columns are not supported by Keyspaces yet")
        }
        allColumnDefinitions.entries.map { (columnName, columnType) ->
            CfnTable.ColumnProperty.builder()
                .columnName(columnName.name)
                .columnType(getTypeNameOrRef(ksName, columnType))
                .build()
        }.also { tableBuilder.regularColumns(it) }
    }

    private fun CfnTemplateGenerator.getTypeNameOrRef(ksName: Identifier, type: Type): String =
        when (type) {
            is Type.Simple ->
                type.name
            is Type.Composite ->
                Fn.sub("${type.kind}<\${Types}>",
                    mapOf("Types" to Fn.join(",", type.elementTypes.map { getTypeNameOrRef(ksName, it) })))
            is Type.User -> {
                if (type.ksName != null && type.ksName != ksName) {
                    Logger.error("Cannot reference user type '${type.utName}' " +
                            "in another keyspace '${type.ksName}'")
                }
                createdTypes[Pair(ksName, type.utName)]?.let { Fn.select(1, Fn.split("|", it.ref)) }
                    ?: type.utName.name.also {
                        Logger.warn("Attempted to reference user type '${type.utName}' before creating it first")
                    }
                Logger.error("User types are not supported by Keyspaces yet")
            }
            is Type.Custom ->
                Logger.error("Custom types are not supported by Keyspaces")
        }

    private fun CfnTemplateGenerator.processRegularProperties(
        regularProperties: PropertyMap<Identifier, Any>,
        table: CfnTable
    ) {
        regularProperties.forEach { (key, value) ->
            when (key.name) {
                "comment" -> {
                    @Suppress("UNUSED_VARIABLE")
                    val comment = value as? String
                        ?: Logger.error("Invalid value for property 'comments'; it should be a string")
                    Logger.error("Property 'comment' currently cannot be set via CloudFormation")
                }
                "default_time_to_live" -> {
                    @Suppress("UNUSED_VARIABLE")
                    val defaultTimeToLive = (value as? String)?.toIntOrNull()
                        ?: Logger.error("Invalid value for property 'default_time_to_live'; " +
                                "it should be an integer")
                    Logger.error("Property 'default_time_to_live' is not supported by Keyspaces yet")
                }
                "tags" -> {
                    @Suppress("UNCHECKED_CAST")
                    val tags = value as? PropertyMap<String, String>
                        ?: Logger.error("Invalid value for property 'tags'; it should be a map")
                    Tags.of(table).apply {
                        tags.forEach { add(it.key, it.value) }
                    }
                    Logger.error("Property 'tags' currently cannot be set via CloudFormation")
                }
                else ->
                    Logger.warn("Property '$key' is not applicable to Keyspaces")
            }
        }
    }

    private fun CfnTemplateGenerator.processCustomProperties(
        customProperties: PropertyMap<String, PropertyMap<String, String>>?,
        table: CfnTable
    ) {
        customProperties?.forEach { (key, value) ->
            when (key) {
                "capacity_mode" -> {
                    val capacityMode = value.toMutableMap()
                    when (val throughputMode = capacityMode.remove("throughput_mode")) {
                        null ->
                            Logger.error("Missing required option 'throughput_mode' " +
                                    "of custom property 'capacity_mode'")
                        "PAY_PER_REQUEST" -> {
                            if (capacityMode.isNotEmpty()) {
                                Logger.error("Unrecognized option '${capacityMode.keys.first()}' " +
                                        "for capacity mode throughput mode 'PAY_PER_REQUEST'")
                            }
                            table.setBillingMode(CfnTable.BillingModeProperty.builder()
                                .mode("ON_DEMAND")
                                .build())
                        }
                        "PROVISIONED" -> {
                            val readCapacityUnits = capacityMode.remove("read_capacity_units")?.let {
                                it.toIntOrNull()
                                    ?: Logger.error("Invalid value for option 'read_capacity_units' " +
                                                "of custom property 'capacity_mode'; it should be an integer")
                            } ?: Logger.error("Missing required option 'read_capacity_units' " +
                                        "for capacity mode throughput mode 'PROVISIONED'")
                            val writeCapacityUnits = capacityMode.remove("write_capacity_units")?.let {
                                it.toIntOrNull()
                                    ?: Logger.error("Invalid value for option 'write_capacity_units' " +
                                            "of custom property 'capacity_mode'; it should be an integer")
                            } ?: Logger.error("Missing required option 'write_capacity_units' " +
                                    "for capacity mode throughput mode 'PROVISIONED'")
                            if (capacityMode.isNotEmpty()) {
                                Logger.error("Unrecognized option '${capacityMode.keys.first()}' " +
                                        "for capacity mode throughput mode 'PROVISIONED'")
                            }
                            table.setBillingMode(CfnTable.BillingModeProperty.builder()
                                .mode("PROVISIONED")
                                .provisionedThroughput(CfnTable.ProvisionedThroughputProperty.builder()
                                    .readCapacityUnits(readCapacityUnits)
                                    .writeCapacityUnits(writeCapacityUnits)
                                    .build())
                                .build())
                        }
                        else ->
                            Logger.error("Capacity mode throughput mode '$throughputMode' " +
                                    "is not recognized by Keyspaces")
                    }
                }
                "point_in_time_recovery" -> {
                    val pointInTimeRecovery = value.toMutableMap()
                    @Suppress("UNUSED_VARIABLE")
                    val enabled = when (val status = pointInTimeRecovery.remove("status")) {
                        null ->
                            Logger.error("Missing required option 'status' of custom property 'point_in_time_recovery'")
                        "enabled" ->
                            true
                        "disabled" ->
                            false
                        else ->
                            Logger.error("Point in time recovery status '$status' is not recognized by Keyspaces")
                    }
                    if (pointInTimeRecovery.isNotEmpty()) {
                        Logger.error("Unrecognized option '${pointInTimeRecovery.keys.first()}' " +
                                "of custom property 'point_in_time_recovery'")
                    }
                    Logger.error("Custom property 'point_in_time_recovery' currently cannot be set via CloudFormation")
                }
                else ->
                    Logger.error("Custom property '$key' is not recognized by Keyspaces")
            }
        }
    }

    private fun CfnTemplateGenerator.processCompactStorage(
        compactStorage: Boolean
    ) {
        if (compactStorage) {
            Logger.warn("COMPACT STORAGE is not applicable to Keyspaces")
        }
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cfn

import software.aws.mcs.cql2cfn.cql.Statement
import software.aws.mcs.cql2cfn.util.Logger

abstract class StatementHandler<T : Statement> {
    fun handle(generator: CfnTemplateGenerator, statement: T): Unit =
        generator.run {
            Logger.debug("$statement")
            doHandle(statement)
        }

    protected abstract fun CfnTemplateGenerator.doHandle(statement: T)
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cfn

import software.aws.mcs.cql2cfn.cql.Statement
import software.aws.mcs.cql2cfn.util.Logger

object UseHandler : StatementHandler<Statement.Use>() {
    override fun CfnTemplateGenerator.doHandle(statement: Statement.Use) {
        Logger.info("Processing USE statement for '${statement.ksName}'")
        if (!createdKeyspaces.containsKey(statement.ksName)) {
            Logger.warn("Attempted to use keyspace before creating it first")
        }
        usedKeyspace = statement.ksName
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cql

import org.antlr.runtime.ANTLRStringStream
import org.antlr.runtime.CommonTokenStream
import software.aws.mcs.cql2cfn.util.Logger

object CqlScriptParser {
    fun parse(cqlScript: String): List<Statement> {
        try {
            val lexer = CfnCompatibleCqlLexer(ANTLRStringStream(cqlScript))
            val parser = CfnCompatibleCqlParser(CommonTokenStream(lexer))
            return parser.cqlScript()
        } catch (e: ParsingException) {
            Logger.error("Failed to parse CQL script: ${e.message}")
        }
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cql

import java.util.Locale

data class Identifier(val name: String) {
    constructor(name: String, keepCase: Boolean) : this(name.toInternalName(keepCase))

    override fun toString(): String =
        name

    private companion object {
        fun String.toInternalName(keepCase: Boolean): String =
            if (keepCase) {
                this
            } else {
                toLowerCase(Locale.US)
            }
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cql

class ParsingException(message: String) : RuntimeException(message)


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cql

class PropertyMap<K, V> : LinkedHashMap<K, V>() {
    fun addProperty(key: K, value: V) {
        if (containsKey(key)) {
            throw ParsingException("multiple definition for property '$key'");
        }
        put(key, value)
    }
}

interface Properties {
    val regularProperties: PropertyMap<Identifier, Any>
    var customProperties: PropertyMap<String, PropertyMap<String, String>>?

    fun validate()

    fun defineCustomProperties(customProperties: PropertyMap<String, PropertyMap<String, String>>) {
        if (this.customProperties != null) {
            throw ParsingException("multiple definition for property 'custom_properties'")
        }
        this.customProperties = customProperties
    }
}

data class KeyspaceProperties @JvmOverloads constructor(
    override val regularProperties: PropertyMap<Identifier, Any> = PropertyMap(),
    override var customProperties: PropertyMap<String, PropertyMap<String, String>>? = null
) : Properties {
    override fun validate() {
    }
}

data class TableProperties @JvmOverloads constructor(
    override val regularProperties: PropertyMap<Identifier, Any> = PropertyMap(),
    override var customProperties: PropertyMap<String, PropertyMap<String, String>>? = null,
    val columnDefinitions: MutableMap<Identifier, Type> = LinkedHashMap(),
    val staticColumnNames: MutableList<Identifier> = ArrayList(),
    val partitionKeyColumnNames: MutableList<Identifier> = ArrayList(),
    val clusteringKeyColumnNames: MutableList<Identifier> = ArrayList(),
    val clusteringOrders: MutableMap<Identifier, ClusteringOrder> = LinkedHashMap(),
    var compactStorage: Boolean = false
) : Properties {
    override fun validate() {
        if (partitionKeyColumnNames.isEmpty()) {
            throw ParsingException("no PRIMARY KEY specified; exactly one required")
        }
        if (clusteringKeyColumnNames.isEmpty() && staticColumnNames.isNotEmpty()) {
            throw ParsingException("static columns not allowed if table has no clustering column")
        }
    }

    fun addColumnDefinition(columnName: Identifier, columnType: Type) {
        if (columnDefinitions.containsKey(columnName)) {
            throw ParsingException("multiple definition of column '$columnName'")
        }
        columnDefinitions[columnName] = columnType
    }

    fun addStaticColumnName(columnName: Identifier) {
        staticColumnNames.add(columnName)
    }

    fun startDefiningPartitionKeyColumnNames() {
        if (partitionKeyColumnNames.isNotEmpty()) {
            throw ParsingException("multiple definition of PRIMARY KEY; exactly one required")
        }
    }

    fun addPartitionKeyColumnName(columnName: Identifier) {
        addKeyColumnName(columnName, partitionKeyColumnNames)
    }

    fun addClusteringColumnName(columnName: Identifier) {
        addKeyColumnName(columnName, clusteringKeyColumnNames)
    }

    private fun addKeyColumnName(columnName: Identifier, keyColumnNames: MutableList<Identifier>) {
        if (!columnDefinitions.containsKey(columnName)) {
            throw ParsingException("unknown column '$columnName' referenced in PRIMARY KEY")
        }
        if (staticColumnNames.contains(columnName)) {
            throw ParsingException("static column '$columnName' referenced in PRIMARY KEY")
        }
        if (partitionKeyColumnNames.contains(columnName) || clusteringKeyColumnNames.contains(columnName)) {
            throw ParsingException("column '$columnName' referenced more than once in PRIMARY KEY")
        }
        keyColumnNames.add(columnName)
    }

    fun startDefiningClusteringOrder() {
        if (clusteringOrders.isNotEmpty()) {
            throw ParsingException("multiple definition of CLUSTERING ORDER")
        }
    }

    fun addClusteringAscendingOrder(columnName: Identifier) {
        addClusteringOrder(columnName, ClusteringOrder.ASC)
    }

    fun addClusteringDescendingOrder(columnName: Identifier) {
        addClusteringOrder(columnName, ClusteringOrder.DESC)
    }

    private fun addClusteringOrder(columnName: Identifier, clusteringOrder: ClusteringOrder) {
        val position = clusteringKeyColumnNames.indexOf(columnName)
        if (position < 0) {
            throw ParsingException("only clustering key columns can be referenced in CLUSTERING ORDER")
        }
        if (clusteringOrders.containsKey(columnName)) {
            throw ParsingException("column '$columnName' referenced more than once in CLUSTERING ORDER")
        }
        if (clusteringOrders.size != position) {
            throw ParsingException("missing CLUSTERING ORDER " +
                    "for column '${clusteringKeyColumnNames[clusteringOrders.size]}'")
        }
        clusteringOrders[columnName] = clusteringOrder
    }

    fun defineCompactStorage() {
        if (compactStorage) {
            throw ParsingException("multiple definition of COMPACT STORAGE")
        }
        compactStorage = true
    }

    enum class ClusteringOrder {
        ASC,
        DESC
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cql

sealed class Statement {
    data class Use(
        val ksName: Identifier
    ) : Statement()

    data class CreateKeyspace(
        val ksName: Identifier,
        val ifNotExists: Boolean,
        val ksProps: KeyspaceProperties
    ) : Statement() {
        init {
            ksProps.validate()
        }
    }

    data class CreateTable(
        val ksName: Identifier?,
        val cfName: Identifier,
        val ifNotExists: Boolean,
        val cfProps: TableProperties
    ) : Statement() {
        init {
            cfProps.validate()
        }
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.cql

sealed class Type {
    class Simple(name: String) : Type() {
        val name = name.toLowerCase()

        override fun toString(): String =
            name
    }

    class Composite(kind: String, vararg val elementTypes: Type) : Type() {
        val kind = kind.toLowerCase()

        override fun toString(): String =
            "$kind<${elementTypes.joinToString()}>"
    }

    class User(val ksName: Identifier?, val utName: Identifier) : Type() {
        override fun toString(): String =
            "${ksName?.let { "$it." } ?: ""}$utName"
    }

    class Custom(val className: String) : Type() {
        override fun toString(): String =
            "'$className'"
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn.util

import kotlin.system.exitProcess

object Logger {
    var debugEnabled: Boolean = false
    var exitOnWarn: Boolean = false

    fun debug(message: String) {
        if (debugEnabled) {
            System.err.println("\u001B[0;36m[DEBUG] $message\u001B[0m")
        }
    }

    fun info(message: String) {
        System.err.println("\u001B[0;32m[INFO] $message\u001B[0m")
    }

    fun warn(message: String) {
        System.err.println("\u001B[1;33m[WARN] $message\u001B[0m")
        if (exitOnWarn) {
            exitProcess(1)
        }
    }

    fun error(message: String, e: Throwable? = null): Nothing {
        System.err.println("\u001B[1;91m[ERROR] $message\u001B[0m")
        e?.printStackTrace()
        exitProcess(1)
    }
}


/*-
 * #%L
 * Amazon Keyspaces CQL Script to CFN Template Converter
 * %%
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package software.aws.mcs.cql2cfn

import kotlin.test.Test
import kotlin.test.assertEquals

class MainTests {
    @Test
    fun testConvert() {
        val cqlScript = javaClass.classLoader.getResource("cql-script.cql")!!.readText()
        val cfnTemplate = javaClass.classLoader.getResource("cfn-template.json")!!.readText()
        assertEquals(cfnTemplate, Main().convert(cqlScript))
    }
}


package software.amazon.awssdk.iotsamples

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("software.amazon.awssdk.iotsamples", appContext.packageName)
    }
}


package software.amazon.awssdk.iotsamples

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.OutputStream
import java.io.PrintStream
import java.lang.Exception
import kotlin.concurrent.thread

val SAMPLES = mapOf(
    "Publish/Subscribe Sample" to "pubsub.PubSub",
    "Jobs Client Sample" to "jobs.JobsSample",
    "Shadow Client Sample" to "shadow.ShadowSample",
    "Cognito Client Sample" to "cognitoconnect.CognitoConnect"
)

class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener {

    private class StreamTee(val source: OutputStream, val log: (message: String) -> Unit)
        : PrintStream(source, true) {
        init {
            if (source == System.out) {
                System.setOut(this)
            } else if (source == System.err) {
                System.setErr(this)
            }
        }

        override fun write(buf: ByteArray, off: Int, len: Int) {
            source.write(buf, off, len)
            log(String(buf.slice(IntRange(off, off+len-1)).toByteArray()))
        }

        override fun write(b: ByteArray) {
            source.write(b)
            log(String(b))
        }
    }

    private val stdout : StreamTee;
    private val stderr : StreamTee;

    private var console: TextView? = null;
    private var sampleSelect: Spinner? = null;

    init {
        stdout = StreamTee(System.out) { writeToConsole(it) }
        stderr = StreamTee(System.err) { writeToConsole(it) }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        console = findViewById<TextView>(R.id.console)
        console?.isEnabled = false

        sampleSelect = findViewById<Spinner>(R.id.sampleSelect);

        val samples = SAMPLES.keys.toMutableList()
        samples.add(0, "Please select a sample")
        val samplesAdapter = ArrayAdapter<String>(this, R.layout.support_simple_spinner_dropdown_item, samples)
        sampleSelect?.adapter = samplesAdapter

        sampleSelect?.onItemSelectedListener = this
    }

    private fun clearConsole() {
        runOnUiThread() {
            console?.text = ""
        }
    }

    private fun writeToConsole(message: String) {
        runOnUiThread() {
            console?.append(message)
        }
    }

    private fun onSampleComplete() {
        runOnUiThread() {
            writeToConsole("Sample Complete\n")
            sampleSelect?.isEnabled = true
        }
    }

    private fun assetContents(assetName: String) : String {
        resources.assets.open(assetName).use {res ->
            val bytes = ByteArray(res.available())
            res.read(bytes)
            return String(bytes).trim()
        }
    }

    private fun assetContentsOr(assetName: String, defaultValue: String) : String {
        return try {
            assetContents(assetName)
        } catch (fnf: FileNotFoundException) {
            defaultValue
        }
    }

    private fun runSample(name: String) {
        val classLoader = Thread.currentThread().contextClassLoader
        val sampleClass = classLoader?.loadClass(name);
        if (sampleClass == null) {
            clearConsole()
            writeToConsole("Could not find sample '${name}'")
        }
        writeToConsole("Running sample '${name}'\n")

        thread(name="sample_runner", contextClassLoader = classLoader) {

            var isResourcesFound: Boolean = true
            val args = mutableListOf<String?>()
            var resourceNames = mutableListOf<String>()
            val resourceMap = HashMap<String, String>()

            // All samples require endpoint.txt
            resourceNames.add("endpoint.txt")

            // Add required files for Samples here
            when(name) {
                "pubsub.PubSub", "jobs.JobsSample", "shadow.ShadowSample" -> {
                    resourceNames.add("certificate.pem")
                    resourceNames.add("privatekey.pem")
                }

                "cognitoconnect.CognitoConnect" -> {
                    resourceNames.add("cognitoIdentity.txt")
                    resourceNames.add("signingRegion.txt")
                }
            }

            // Copy to cache and get file locations for required files
            for (resourceName in resourceNames) {
                try {
                    resources.assets.open(resourceName).use { res ->
                        val cachedName = "${externalCacheDir}/${resourceName}"
                        FileOutputStream(cachedName).use { cachedRes ->
                            res.copyTo(cachedRes)
                        }
                        resourceMap[resourceName] = cachedName
                    }
                } catch (e: Exception) {
                    isResourcesFound = false;
                    writeToConsole(e.toString())
                    writeToConsole("\n'${resourceName}' must be in the assets folder for sample to run.\n")
                    break
                }
            }

            if(isResourcesFound) {
                args.addAll(arrayOf("--endpoint", assetContents("endpoint.txt")))

                when(name) {
                    "pubsub.PubSub" -> {
                        args.addAll(arrayOf(
                            "--cert", resourceMap["certificate.pem"],
                            "--key", resourceMap["privatekey.pem"],
                            "--port", assetContentsOr("port.txt", "8883"),
                            "--client_id", assetContentsOr("clientId.txt", "android-java-crt-test"),
                            "--topic", assetContentsOr("topic.txt", "test/topic"),
                            "--message", assetContentsOr("message.txt", "Hello World From Android")))
                    }

                    "jobs.JobsSample", "shadow.ShadowSample" -> {
                        args.addAll(arrayOf(
                            "--cert", resourceMap["certificate.pem"],
                            "--key", resourceMap["privatekey.pem"],
                            "--port", assetContentsOr("port.txt", "8883"),
                            "--client_id", assetContentsOr("clientId.txt", "android-java-crt-test"),
                            "--thing_name", assetContentsOr("thingName.txt", "aws-iot-unit-test")))
                    }

                    "cognitoconnect.CognitoConnect" -> {
                        args.addAll(arrayOf(
                            "--signing_region", assetContents("signingRegion.txt"),
                            "--cognito_identity", assetContents("cognitoIdentity.txt")))
                    }
                }

                // Check for optional root CA file
                try {
                    resources.assets.open("rootca.pem").use { res ->
                        val cachedName = "${externalCacheDir}/rootca.pem"
                        FileOutputStream(cachedName).use { cachedRes ->
                            res.copyTo(cachedRes)
                        }
                        args.addAll(arrayOf("--ca_file", cachedName))
                    }
                } catch (e: Exception) {}

                val main = sampleClass?.getMethod("main", Array<String>::class.java)

                try {
                    main?.invoke(null, args.toTypedArray())
                } catch (e: Exception) {
                    writeToConsole(e.toString())
                }
            }
            onSampleComplete();
        }
    }

    override fun onNothingSelected(p0: AdapterView<*>?) {
        clearConsole()
        writeToConsole("Please select a sample above")
    }

    override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
        clearConsole()
        val sampleName = parent?.getItemAtPosition(pos).toString()
        val sampleClassName = SAMPLES[sampleName]
        if (sampleClassName != null) {
            return runSample(sampleClassName)
        }
    }
}


package software.amazon.awssdk.iotsamples

import org.junit.Test

import org.junit.Assert.*

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}


package com.example.jetbrains

import com.intellij.lang.Language
import com.intellij.openapi.fileTypes.LanguageFileType
import javax.swing.Icon

class bspec : LanguageFileType(bspecLang.INSTANCE) {
    override fun getName(): String = "bspec"

    override fun getDescription(): String = "bspec"

    override fun getDefaultExtension(): String = "bspec"
    override fun getIcon() = null

    companion object {
        val INSTANCE = bspec()
    }
}

class bspecLang : Language("bspec") {
    companion object {
        val INSTANCE = bspecLang()
    }
}


package com.example.jetbrains

import com.intellij.openapi.application.PreloadingActivity
import com.intellij.openapi.progress.ProgressIndicator
import org.wso2.lsp4intellij.IntellijLanguageClient
import org.wso2.lsp4intellij.client.languageserver.serverdefinition.RawCommandServerDefinition

class LspPreloadingActivity : PreloadingActivity() {
    override fun preload(indicator: ProgressIndicator) {
        IntellijLanguageClient.addServerDefinition(RawCommandServerDefinition("yml,yaml", arrayOf("node", "--nolazy", "--inspect=5599",
            "<Path to language server.js>", "--", "--stdio")))
    }
}


package com.example.jetbrains

import com.intellij.lang.Language
import com.intellij.openapi.fileTypes.LanguageFileType

class stepfunctions : LanguageFileType(stepfunctionsLang.INSTANCE) {
    override fun getName(): String = "stepfunctions"

    override fun getDescription(): String = "stepfunctions"

    override fun getDefaultExtension(): String = "stepfunctions"
    override fun getIcon() = null

    companion object {
        val INSTANCE = stepfunctions()
    }
}

class stepfunctionsLang : Language("stepfunctions") {
    companion object {
        val INSTANCE = stepfunctionsLang()
    }
}


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

package software.aws.toolkits.telemetry.generator

object ResourceLoader {
    private const val schemaPath = "/telemetrySchema.json"

    val SCHEMA_FILE = this.javaClass.getResourceAsStream(schemaPath).use { it.bufferedReader().readText() }
    // TODO add a manifest or something
    val DEFINITIONS_FILES = listOf("/definitions/commonDefinitions.json").map {
        this.javaClass.getResourceAsStream(it).use {
            it.bufferedReader().readText()
        }
    }
}


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

package software.aws.toolkits.telemetry.generator

import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.STRING
import com.squareup.kotlinpoet.TypeSpec
import java.io.File
import java.time.Instant

const val PACKAGE_NAME = "software.aws.toolkits.telemetry"

const val JETBRAINS_TELEMETRY_PACKAGE_NAME = "software.aws.toolkits.jetbrains.services.telemetry"
val METRIC_METADATA = ClassName(JETBRAINS_TELEMETRY_PACKAGE_NAME, "MetricEventMetadata")
val TELEMETRY_SERVICE = ClassName(JETBRAINS_TELEMETRY_PACKAGE_NAME, "TelemetryService")
val PROJECT = ClassName("com.intellij.openapi.project", "Project").copy(nullable = true)
val CONNECTION_SETTINGS = ClassName("software.aws.toolkits.core", "ConnectionSettings").copy(nullable = true)

const val RESULT = "result"
const val SUCCESS = "success"

fun String.filterInvalidCharacters() = this.replace(".", "")
fun String.toTypeFormat() = this.filterInvalidCharacters().split("_", "-").joinToString(separator = "") { it.capitalize() }

fun String.toArgumentFormat() = this.toTypeFormat().decapitalize()

fun generateTelemetryFromFiles(
    inputFiles: List<File>,
    defaultDefinitions: List<String> = ResourceLoader.DEFINITIONS_FILES,
    outputFolder: File
) {
    val telemetry = TelemetryParser.parseFiles(defaultDefinitions, inputFiles)
    // make sure the output directory exists before writing to it
    outputFolder.mkdirs()
    FileSpec.builder(PACKAGE_NAME, "TelemetryDefinitions")
        .indent(" ".repeat(4))
        .generateHeader()
        .generateTelemetryEnumTypes(telemetry.types)
        .generateTelemetryObjects(telemetry.metrics)
        .build()
        .writeTo(outputFolder)
}

private fun FileSpec.Builder.generateHeader(): FileSpec.Builder {
    addFileComment("Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n")
    addFileComment("SPDX-License-Identifier: Apache-2.0\n")
    addFileComment("THIS FILE IS GENERATED! DO NOT EDIT BY HAND!")
    addAnnotation(AnnotationSpec.builder(Suppress::class).addMember("\"unused\", \"MemberVisibilityCanBePrivate\"").build())

    return this
}

private fun FileSpec.Builder.generateTelemetryEnumTypes(items: List<TelemetryMetricType>): FileSpec.Builder {
    items.forEach {
        // We only need to generate enums if they are actually enums, skip other types
        if (it.allowedValues == null) {
            return@forEach
        }
        generateTelemetryEnumType(it)
    }

    return this
}

private fun FileSpec.Builder.generateTelemetryEnumType(item: TelemetryMetricType): FileSpec.Builder {
    val enum = TypeSpec.enumBuilder(item.name.toTypeFormat())
        .primaryConstructor(
            FunSpec.constructorBuilder()
                .addParameter("value", String::class)
                .build()
        )
        .addProperty(PropertySpec.builder("value", String::class, KModifier.PRIVATE).initializer("value").build())
        .addFunction(FunSpec.builder("toString").addModifiers(KModifier.OVERRIDE).returns(String::class).addStatement("return value").build())
        .addKdoc(item.description)

    item.allowedValues!!.forEach { enumValue ->
        enum.addEnumConstant(
            enumValue.toString().toTypeFormat(), TypeSpec.anonymousClassBuilder()
                .addSuperclassConstructorParameter("%S", enumValue.toString())
                .build()
        )
    }

    // Add an unknown value
    val unknownType = "unknown".toTypeFormat()

    enum.addEnumConstant(
        unknownType,
        TypeSpec.anonymousClassBuilder()
            .addSuperclassConstructorParameter("%S", "unknown")
            .build()
    ).build()

    val companion = TypeSpec.companionObjectBuilder()
        .addFunction(
            FunSpec.builder("from")
                .returns(ClassName("", item.name.toTypeFormat()))
                .addParameter("type", String::class)
                .addStatement("return values().firstOrNullÂ·{Â·it.valueÂ·==Â·typeÂ·} ?:Â·$unknownType")
                .build()
        )
        .build()

    enum.addType(companion)

    addType(enum.build())

    return this
}

private fun FileSpec.Builder.generateTelemetryObjects(schema: List<MetricSchema>): FileSpec.Builder {
    schema.groupBy { it.namespace() }
        .toSortedMap()
        .forEach { (namespace, metrics) -> generateNamespaces(namespace, metrics) }

    return this
}

private fun FileSpec.Builder.generateNamespaces(namespace: String, metrics: List<MetricSchema>): FileSpec.Builder {
    val telemetryObject = TypeSpec.objectBuilder("${namespace.toTypeFormat()}Telemetry")

    metrics.sortedBy { it.name }.forEach { telemetryObject.generateRecordFunctions(it) }

    addType(telemetryObject.build())

    return this
}

private fun TypeSpec.Builder.generateRecordFunctions(metric: MetricSchema) {
    // metric.name.split("_")[1] is guaranteed to work at this point because the schema requires the metric name to have at least 1 underscore
    val functionName = metric.name.split("_")[1]

    addFunction(buildProjectFunction(functionName, metric))
    addFunction(buildConnectionSettingsFunction(functionName, metric))
    addFunction(buildMetricMetadataFunction(functionName, metric))

    // Result is special cased to generate a function that accepts true/false instead of a Result
    if (metric.metadata.none { it.type.name == RESULT }) {
        return
    }

    addFunction(buildProjectOverloadFunction(functionName, metric))
    addFunction(buildConnectionSettingsOverloadFunction(functionName, metric))
    addFunction(buildMetricMetadataOverloadFunction(functionName, metric))
}

fun buildProjectFunction(functionName: String, metric: MetricSchema): FunSpec {
    val metadataProvider = ParameterSpec.builder("project", PROJECT).build()

    return buildRecordFunction(metadataProvider, functionName, metric)
}

fun buildConnectionSettingsFunction(functionName: String, metric: MetricSchema): FunSpec {
    val metadataProvider = ParameterSpec.builder("connectionSettings", CONNECTION_SETTINGS).defaultValue("null").build()

    return buildRecordFunction(metadataProvider, functionName, metric)
}

fun buildMetricMetadataFunction(functionName: String, metric: MetricSchema): FunSpec {
    val metadataProvider = ParameterSpec.builder("metadata", METRIC_METADATA).build()

    return buildRecordFunction(metadataProvider, functionName, metric)
}

private fun buildRecordFunction(metadataProvider: ParameterSpec, functionName: String, metric: MetricSchema): FunSpec {
    val functionParameters = mutableListOf<ParameterSpec>()
    functionParameters.add(metadataProvider)
    functionParameters.addAll(buildMetricParameters(metric))

    return FunSpec.builder(functionName)
        .addKdoc(metric.description)
        .addParameters(functionParameters)
        .generateFunctionBody(metadataProvider, metric)
        .build()
}

private fun buildMetricParameters(metric: MetricSchema): List<ParameterSpec> {
    val list = mutableListOf<ParameterSpec>()

    list.addAll(metric.metadata.map { it.metadataToParameter() })
    list.add(ParameterSpec.builder("passive", BOOLEAN).defaultValue(metric.passive.toString()).build())
    list.add(ParameterSpec.builder("value", DOUBLE).defaultValue("1.0").build())
    list.add(ParameterSpec.builder("createTime", Instant::class).defaultValue("Instant.now()").build())

    return list
}

private fun MetadataSchema.metadataToParameter(): ParameterSpec {
    // Allowed values indicates an enum
    val typeName = if (type.allowedValues != null) {
        ClassName(PACKAGE_NAME, type.name.toTypeFormat())
    } else {
        type.type.kotlinType()
    }.copy(nullable = required == false)

    val parameterSpec = ParameterSpec.builder(type.name.toArgumentFormat(), typeName)
    if (required == false) {
        parameterSpec.defaultValue("null")
    }
    return parameterSpec.build()
}

private fun FunSpec.Builder.generateFunctionBody(metadataParameter: ParameterSpec, metric: MetricSchema): FunSpec.Builder {
    val metricUnit = MemberName("software.amazon.awssdk.services.toolkittelemetry.model", "Unit")
    beginControlFlow("%T.getInstance().record(${metadataParameter.name})", TELEMETRY_SERVICE)
    beginControlFlow("datum(%S)", metric.name)
    addStatement("createTime(createTime)")
    addStatement("unit(%M.${(metric.unit ?: MetricUnit.NONE).name})", metricUnit)
    addStatement("value(value)")
    addStatement("passive(passive)")
    metric.metadata.forEach {
        generateMetadataStatement(it)
    }
    endControlFlow()
    endControlFlow()

    return this
}

fun buildProjectOverloadFunction(functionName: String, metric: MetricSchema): FunSpec {
    val metadataProvider = ParameterSpec.builder("project", PROJECT).defaultValue("null").build()

    return buildResultOverloadFunction(metadataProvider, functionName, metric)
}

fun buildConnectionSettingsOverloadFunction(functionName: String, metric: MetricSchema): FunSpec {
    val metadataProvider = ParameterSpec.builder("connectionSettings", CONNECTION_SETTINGS).defaultValue("null").build()
    return buildResultOverloadFunction(metadataProvider, functionName, metric)
}

fun buildMetricMetadataOverloadFunction(functionName: String, metric: MetricSchema): FunSpec {
    val metadataProvider = ParameterSpec.builder("metadata", METRIC_METADATA).build()

    return buildResultOverloadFunction(metadataProvider, functionName, metric)
}

fun buildResultOverloadFunction(metadataProvider: ParameterSpec, functionName: String, metric: MetricSchema): FunSpec {
    val overloadedParameters = buildMetricParameters(metric).map {
        if (it.name == RESULT) {
            ParameterSpec.builder(SUCCESS, BOOLEAN).build()
        } else {
            it
        }
    }

    val functionParameters = mutableListOf<ParameterSpec>()
    functionParameters.add(metadataProvider)
    functionParameters.addAll(overloadedParameters)

    return FunSpec.builder(functionName)
        .addKdoc(metric.description)
        .addParameters(functionParameters)
        .generateResultOverloadFunctionBody(functionName, functionParameters)
        .build()
}

private fun FunSpec.Builder.generateResultOverloadFunctionBody(functionName: String, parameters: List<ParameterSpec>): FunSpec.Builder {
    addStatement("%L(%L)", functionName, parameters.joinToString {
        if (it.name != SUCCESS) {
            it.name
        } else {
            "if($SUCCESS) Result.Succeeded else Result.Failed"
        }
    })

    return this
}

private fun FunSpec.Builder.generateMetadataStatement(data: MetadataSchema): FunSpec.Builder {
    if (data.required == false) {
        beginControlFlow("if(%L != null) {", data.type.name.toArgumentFormat())
    }

    // If its type is already a string, we dont need to call toString
    val setStatement = if (data.type.type.kotlinType() != STRING || data.type.allowedValues?.isNotEmpty() == true) {
        "${data.type.name.toArgumentFormat()}.toString()"
    } else {
        data.type.name.toArgumentFormat()
    }

    addStatement("metadata(%S, %L)", data.type.name.toArgumentFormat(), setStatement)
    if (data.required == false) {
        endControlFlow()
    }

    return this
}


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

package software.aws.toolkits.telemetry.generator

import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.squareup.kotlinpoet.TypeName
import org.everit.json.schema.Schema
import org.everit.json.schema.loader.SchemaLoader
import org.json.JSONObject
import org.json.JSONTokener
import java.io.File

enum class MetricMetadataTypes(@get:JsonValue val type: String) {
    STRING("string") {
        override fun kotlinType(): TypeName = com.squareup.kotlinpoet.STRING
    },
    INT("int") {
        override fun kotlinType(): TypeName = com.squareup.kotlinpoet.INT
    },
    DOUBLE("double") {
        override fun kotlinType(): TypeName = com.squareup.kotlinpoet.DOUBLE
    },
    BOOLEAN("boolean") {
        override fun kotlinType(): TypeName = com.squareup.kotlinpoet.BOOLEAN
    };

    abstract fun kotlinType(): TypeName
}

data class TelemetryMetricType(
    val name: String,
    val description: String,
    val type: MetricMetadataTypes = MetricMetadataTypes.STRING,
    val allowedValues: List<Any>?
)

enum class MetricUnit(@get:JsonValue val type: String) {
    NONE("None"),
    MILLISECONDS("Milliseconds"),
    BYTES("Bytes"),
    PERCENT("Percent"),
    COUNT("Count")
}

private data class MetadataDefinition(
    val type: String,
    val required: Boolean?
)

private data class MetricDefinition(
    val name: String,
    val description: String,
    val unit: MetricUnit?,
    val metadata: List<MetadataDefinition> = listOf(),
    val passive: Boolean = false
)

private data class TelemetryDefinition(
    val types: List<TelemetryMetricType> = listOf(),
    val metrics: List<MetricDefinition>
)

data class TelemetrySchema(
    val types: List<TelemetryMetricType>,
    val metrics: List<MetricSchema>
)

data class MetricSchema(
    val name: String,
    val description: String,
    val unit: MetricUnit?,
    val metadata: List<MetadataSchema>,
    val passive: Boolean = false
)

fun MetricSchema.namespace() = name.split('_').first().toLowerCase()

data class MetadataSchema(
    val type: TelemetryMetricType,
    val required: Boolean?
)

object TelemetryParser {
    private val MAPPER = jacksonObjectMapper()

    /**
     * Read in default definitions and extra definitions files, and deserialize into an object
     *
     * Definitions from `paths` will override definitions from defaultResourceFiles if their names
     * are the same. This allows testing updates to existing definitions from within each IDE project.
     */
    fun parseFiles(
        defaultResourcesFiles: List<String>,
        paths: List<File> = listOf()
    ): TelemetrySchema {
        val files = paths.map { it.readText() } + defaultResourcesFiles
        val rawSchema = JSONObject(JSONTokener(ResourceLoader.SCHEMA_FILE))
        val schema: Schema = SchemaLoader.load(rawSchema)
        files.forEach { validate(it, schema) }

        val telemetryDefinition = files.map { parse(it) }.fold(TelemetryDefinition(listOf(), listOf())) { it, it2 ->
            TelemetryDefinition(
                it.types.plus(it2.types),
                it.metrics.plus(it2.metrics)
            )
        }.let {
            TelemetryDefinition(
                it.types.distinctBy{ t -> t.name},
                it.metrics.distinctBy { m -> m.name }
            )
        }

        val metadataTypes = telemetryDefinition.types.associateBy { it.name }

        val resolvedMetricTypes = telemetryDefinition.metrics.map {
            MetricSchema(
                it.name,
                it.description,
                it.unit,
                it.metadata.map { metadata -> MetadataSchema(metadataTypes.getValue(metadata.type), metadata.required) },
                it.passive
            )
        }

        return TelemetrySchema(
            telemetryDefinition.types,
            resolvedMetricTypes
        )
    }

    private fun validate(fileContents: String, schema: Schema) {
        try {
            schema.validate(JSONObject(fileContents))
        } catch (e: Exception) {
            System.err.println("Schema validation failed due to thrown exception $e\non input:\n$fileContents")
            throw e
        }
    }

    private fun parse(input: String): TelemetryDefinition =
        try {
            MAPPER.readValue(input)
        } catch (e: Exception) {
            System.err.println("Error while parsing: $e")
            throw e
        }
}


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

package software.aws.toolkits.telemetry.generator.gradle

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import software.aws.toolkits.telemetry.generator.generateTelemetryFromFiles
import java.io.File

open class GenerateTelemetry : DefaultTask() {
    @InputFiles
    lateinit var inputFiles: List<File>

    @OutputDirectory
    lateinit var outputDirectory: File

    @TaskAction
    fun generate() {
        println("Generating telemetry using packaged file and additional files:\n ${inputFiles.map { it.absolutePath }.joinToString("\n")}")
        try {
            generateTelemetryFromFiles(inputFiles = inputFiles, outputFolder = outputDirectory)
        } catch (e: Exception) {
            System.err.println("Generating telemetry threw an exception! $e\n")
            throw e
        }
    }
}


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

package software.aws.toolkits.telemetry.generator

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths

class GeneratorTest {
    @JvmField
    @Rule
    val folder = TemporaryFolder()

    @Test
    fun generateFailsWhenValidationFails() {
        assertThatThrownBy {
            generateTelemetryFromFiles(inputFiles = listOf(), defaultDefinitions = listOf("{}"), outputFolder = folder.root)
        }.hasMessageContaining("required key [metrics] not found")
    }

    @Test
    fun generatesWithNormalInput() {
        testGenerator(defaultDefinitionsFile = "/testGeneratorInput.json", expectedOutputFile = "/testGeneratorOutput")
    }

    @Test
    fun resultGeneratesTwoFunctions() {
        testGenerator(defaultDefinitionsFile = "/testResultInput.json", expectedOutputFile = "/testResultOutput")
    }

    @Test
    fun generateOverrides() {
        testGenerator(defaultDefinitionsFile = "/testResultInput.json", definitionsOverrides = listOf("/testOverrideInput.json"), expectedOutputFile = "/testOverrideOutput")
    }

    @Test
    fun longEnum() {
        testGenerator(defaultDefinitionsFile = "/testLongEnumInput.json", expectedOutputFile = "/testLongEnumOutput")
    }

    @Test
    fun generateGeneratesWithDefaultDefinitions() {
        generateTelemetryFromFiles(inputFiles = listOf(), outputFolder = folder.root)
        val outputFile = Paths.get(folder.root.absolutePath, "software", "aws", "toolkits", "telemetry", "TelemetryDefinitions.kt")
        assertThat(Files.exists(outputFile)).isTrue
    }

    // inputPath and outputPath must be in test resources
    private fun testGenerator(defaultDefinitionsFile: String, definitionsOverrides: List<String> = listOf(), expectedOutputFile: String) {
        generateTelemetryFromFiles(
            defaultDefinitions = listOf(this.javaClass.getResourceAsStream(defaultDefinitionsFile).use { it.bufferedReader().readText() }),
            inputFiles = definitionsOverrides.map { File(javaClass.getResource(it).toURI()) },
            outputFolder = folder.root
        )

        val outputFile = Paths.get(folder.root.absolutePath, "software", "aws", "toolkits", "telemetry", "TelemetryDefinitions.kt")
        assertThat(outputFile).exists()

        val expected = this.javaClass.getResourceAsStream(expectedOutputFile).use {
            it.bufferedReader().readText()
        }

        assertThat(outputFile.toFile().readText()).isEqualTo(expected)
    }
}


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

package software.aws.toolkits.telemetry.generator

import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test

class ParserTest {
    @Test
    fun typeMissingDescription() {
        assertThatThrownBy {
            TelemetryParser.parseFiles(
                listOf(
                    """
            {
                "types": [
                    {
                        "name": "result",
                        "allowedValues": ["Succeeded", "Failed", "Cancelled"],
                    }
                ],
                "metrics": []
            }
            """.trimIndent()
                )
            )
        }.hasMessageContaining("required key [description] not found")
    }

    @Test
    fun missingMetricsField() {
        assertThatThrownBy {
            TelemetryParser.parseFiles(listOf("""{"types": []}"""))
        }.hasMessageContaining("required key [metrics] not found")
    }

    @Test
    fun invalidDataTypes() {
        assertThatThrownBy {
            TelemetryParser.parseFiles(
                listOf(
                    """
            {
                "types": [
                    {
                        "name": "result",
                        "type": "type that does not exist",
                        "description": "abc"
                    }
                ],
                "metrics": []
            }
            """.trimIndent()
                )
            )
        }.hasMessageContaining("type that does not exist is not a valid enum value")

    }

    @Test
    fun successfulParse() {
        TelemetryParser.parseFiles(
            listOf(
                """
            {
                "metrics": []
            }
            """.trimIndent()
            )
        )
    }
}


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

package software.aws.toolkits.gradle

import org.eclipse.jgit.api.Git
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import software.aws.toolkits.gradle.intellij.IdeVersions
import java.io.IOException
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion as KotlinVersionEnum

/**
 * Only run the given block if this build is running within a CI system (e.g. GitHub actions, CodeBuild etc)
 */
fun Project.ciOnly(block: () -> Unit) {
    if (isCi()) {
        block()
    }
}

fun Project.isCi() : Boolean = providers.environmentVariable("CI").isPresent

fun Project.jvmTarget(): Provider<JavaVersion> = withCurrentProfileName {
    when (it) {
        "2021.3", "2022.2" -> JavaVersion.VERSION_11
        else -> JavaVersion.VERSION_17
    }
}

// https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#other-bundled-kotlin-libraries
fun Project.kotlinTarget(): Provider<String> = withCurrentProfileName {
    when (it) {
        "2021.3" -> KotlinVersionEnum.KOTLIN_1_5
        "2022.2" -> KotlinVersionEnum.KOTLIN_1_6
        "2022.3" -> KotlinVersionEnum.KOTLIN_1_7
        else -> KotlinVersionEnum.KOTLIN_1_8
    }.version
}

fun<T : Any> Project.withCurrentProfileName(consumer: (String) -> T): Provider<T> {
    val name = IdeVersions.ideProfile(providers).map { it.name }
    return name.map {
        consumer(it)
    }
}

fun Project.buildMetadata() =
    try {
        val git = Git.open(rootDir)
        val currentShortHash = git.repository.findRef("HEAD").objectId.abbreviate(7).name()
        val isDirty = git.status().call().hasUncommittedChanges()

        buildString {
            append(currentShortHash)

            if (isDirty) {
                append(".modified")
            }
        }
    } catch(e: IOException) {
        logger.warn("Could not determine current commit", e)

        "unknownCommit"
    }


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

package software.aws.toolkits.gradle

import org.gradle.api.Project
import software.aws.toolkits.gradle.intellij.Profile
import java.io.File
import java.io.FileFilter

/**
 * Determines the sub-folders under a project that should be included based on ideVersion
 *
 * [project] the project to use as a directory base
 * [type] is the type of the source folder (e.g. 'src', 'tst', 'resources')
 * [ideProfile] is the IDE [Profile] currently configured in the project
 */
fun findFolders(project: Project, type: String, ideProfile: Profile): Set<File> = project.projectDir.listFiles(
    FileFilter {
        it.isDirectory && includeFolder(type, ideProfile.shortName, it.name)
    }
)?.map { File(it.name) }?.toSet() ?: setOf()

/**
 * Determines if a folder should be included based on the ideVersion being targeted
 * [type] is the type of the source folder (e.g. 'src', 'tst', 'resources')
 * [ideVersion] is the 3 digit numerical version of the JetBrains SDK (e.g. 192, 201 etc)
 * [folderName] is the folder name to match on, relative to the project directory (e.g. 'tst-201')
 *
 * Examples:
 * Given [includeFolder] is called with a [type] of "tst" and an [ideVersion] of "201"
 *
 * Then following will match:
 *  - tst
 *  - tst-201
 *  - tst-201+
 *  - tst-192+
 *  - tst-201-202
 *
 * The following with *not* match:
 *  - tst-resources
 *  - tst-resources-201
 *  - tst-192
 *  - tst-202
 *  - tst-202+
 */
internal fun includeFolder(type: String, ideVersion: String, folderName: String): Boolean {
    val ideVersionAsInt = ideVersion.toInt()
    // Check version range first
    "$type-(\\d{3})-(\\d{3})".toRegex().matchEntire(folderName)?.destructured?.let { (minVersion, maxVersion) ->
        return ideVersionAsInt in minVersion.toInt()..maxVersion.toInt()
    }
    // Then check singular versions/min+
    val (_, version, plus) = "$type(-(\\d{3}))?(\\+)?".toRegex().matchEntire(folderName)?.destructured ?: return false
    return when {
        version.isBlank() -> true
        plus.isBlank() -> version.toInt() == ideVersionAsInt
        else -> version.toInt() <= ideVersionAsInt
    }
}


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

package software.aws.toolkits.gradle.changelog

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.File
import java.time.LocalDate

val MAPPER: ObjectMapper = jacksonObjectMapper()
    .registerModule(JavaTimeModule())
    .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)

inline fun <reified T : Any> readFile(f: File): T {
    try {
        return MAPPER.readValue(f)
    } catch (e: Exception) {
        throw RuntimeException("Exception reading ${T::class.java} from $f", e)
    }
}

@JsonSerialize(using = ChangeType.Serializer::class)
enum class ChangeType(val sectionTitle: String) {
    BREAKING("Breaking Change"),
    FEATURE("Feature"),
    BUGFIX("Bug Fix"),
    DEPRECATION("Deprecation"),
    REMOVAL("Removal");

    class Serializer : StdSerializer<ChangeType>(ChangeType::class.java) {
        override fun serialize(value: ChangeType, gen: JsonGenerator?, provider: SerializerProvider?) {
            gen?.writeString(value.name.toLowerCase())
        }
    }
}

data class Entry(val type: ChangeType, val description: String)
data class ReleaseEntry(val date: LocalDate, val version: String, val entries: List<Entry>)


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

package software.aws.toolkits.gradle.changelog

import org.gradle.api.logging.Logger
import java.nio.file.Path
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import kotlin.streams.toList

/**
 * Generates a combined change log file based in Markdown syntax
 */
class ChangeLogGenerator(private val writers: List<ChangeLogWriter>, private val logger: Logger) : AutoCloseable {
    fun addUnreleasedChanges(unreleasedFiles: List<Path>) {
        val entries = unreleasedFiles.parallelStream()
            .map { readFile<Entry>(it.toFile()) }
            .toList().filterNotNull()
        val unreleasedEntry = ReleaseEntry(LocalDate.now(), "Pending Release", entries)
        logger.info("Adding unreleased entry: $unreleasedEntry")
        generateEntry(unreleasedEntry)
    }

    fun addReleasedChanges(changelogFiles: List<Path>) {
        val versions = mutableSetOf<String>()

        logger.info("Including release change logs: $changelogFiles")
        changelogFiles.parallelStream()
            .map { readFile<ReleaseEntry>(it.toFile()) }
            .toList()
            .onEach {
                val version = it.version
                if (!versions.add(version)) {
                    throw RuntimeException("Duplicate version $version found")
                }
            }
            .sortedByDescending { it.date }
            .forEach {
                logger.info("Adding release entry: $it")
                generateEntry(it)
            }
    }

    private fun generateEntry(entry: ReleaseEntry) {
        val renderedEntry = renderEntry(entry)
        writers.forEach { writer ->
            writer.writeLine(renderedEntry)
        }
    }

    override fun close() {
        writers.forEach { it.close() }
    }

    companion object {
        fun renderEntry(releaseEntry: ReleaseEntry): String {
            val renderedEntry = StringBuilder()

            renderedEntry.append("# _${releaseEntry.version}_ (${DateTimeFormatter.ISO_DATE.format(releaseEntry.date)})")
                .append('\n')
            releaseEntry.entries.forEach { entry ->
                val lines = entry.description.lines()

                // Note: 2 spaces are on end for hard breaks in Markdown
                renderedEntry.append("- **(${entry.type.sectionTitle})** ${lines.first().trim()}\n")
                for (it in lines.takeLast(lines.size - 1)) {
                    val line = it.trim()
                    if (line.isEmpty()) {
                        continue
                    }

                    renderedEntry.append("  $line").append('\n')
                }
            }

            renderedEntry.append('\n')

            return renderedEntry.toString()
        }
    }
}


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

package software.aws.toolkits.gradle.changelog

abstract class ChangeLogWriter(issueUrl: String? = null) {
    private val issueUrl = issueUrl?.trimEnd('/')?.plus("/")

    open fun writeLine(line: String) {
        append(expandIssueLinks(line))
    }

    protected abstract fun append(line: String)
    abstract fun close()

    /**
     * Expands GitHub Issue numbers to markdown URL and returns the updated entry
     */
    private fun expandIssueLinks(entry: String): String {
        if (issueUrl == null) {
            return entry
        }

        val regex = "#(\\d+)".toRegex()
        return regex.replace(entry) {
            val issue = it.groups[1]?.value ?: return@replace it.value
            "[#$issue]($issueUrl$issue)"
        }
    }
}


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

package software.aws.toolkits.gradle.changelog

import java.nio.file.Path

class GithubWriter(private val file: Path, issueUrl: String?) : ChangeLogWriter(issueUrl) {
    private val writer = file.toFile().bufferedWriter()

    override fun append(line: String) {
        writer.write(line)
    }

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

    override fun toString(): String = "GithubWriter(file=$file)"
}


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

package software.aws.toolkits.gradle.changelog

import org.eclipse.jgit.api.Git
import java.io.File

class GitStager(private val rootDirectory: File) {
    private val git = Git.open(rootDirectory)

    fun stage(file: File) {
        if (!file.toPath().toAbsolutePath().startsWith(rootDirectory.toPath())) {
            throw RuntimeException("Can't stage files/folder ($file) outside of root $rootDirectory")
        }

        val relative = rootDirectory.toPath().toAbsolutePath().relativize(file.toPath().toAbsolutePath()).toFile().path.replace('\\', '/')
        git.add().addFilepattern(relative).setUpdate(true).call()
        git.add().addFilepattern(relative).call()
    }

    companion object {
        fun create(rootDirectory: File): GitStager? = try {
            GitStager(rootDirectory)
        } catch (_: Exception) {
            null
        }
    }
}


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

package software.aws.toolkits.gradle.changelog

import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Heading
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import java.io.File
import java.lang.Math.max
import java.lang.Math.min

class JetBrainsWriter(private val changeNotesFile: File, issueUrl: String? = null) : ChangeLogWriter(issueUrl) {
    private val sb = StringBuilder()

    override fun append(line: String) {
        sb.append(line)
    }

    override fun close() {
        val renderer = HtmlRenderer.builder()
            .softbreak("<br/>")
            .build()
        val parser = Parser.builder()
            .postProcessor {
                it.accept(
                    object : AbstractVisitor() {
                        override fun visit(heading: Heading) {
                            heading.level = max(1, min(heading.level + 2, 6))
                        }
                    }
                )

                it
            }
            .build()
        val htmlVersionError = renderer.render(parser.parse(sb.toString()))

        changeNotesFile.writeText(
            """
            <idea-plugin>
                <change-notes>
                <![CDATA[
                    $htmlVersionError
                ]]>
                </change-notes>
            </idea-plugin>
            """.trimIndent()
        )
    }

    override fun toString(): String = "JetBrainsWriter(file=$changeNotesFile)"
}


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

package software.aws.toolkits.gradle.changelog

import org.gradle.api.logging.Logger
import java.io.File
import java.time.LocalDate

class ReleaseCreator(private val unreleasedFiles: Collection<File>, private val nextReleaseFile: File, logger: Logger) {
    init {
        if (nextReleaseFile.exists()) {
            throw RuntimeException("Release file $nextReleaseFile already exists!")
        }
        if (unreleasedFiles.isEmpty()) {
            logger.warn("Release created without any unreleased change files, this will yield an empty changelog")
        }
    }

    fun create(version: String, date: LocalDate = LocalDate.now()) {
        val entriesByType = unreleasedFiles.map { readFile<Entry>(it) }.groupBy { it.type }
        val entries = ChangeType.values().flatMap { entriesByType.getOrDefault(it, emptyList()) }
        val release = ReleaseEntry(date, version, entries)

        MAPPER.writerWithDefaultPrettyPrinter().writeValue(nextReleaseFile, release)
        unreleasedFiles.forEach { it.delete() }
    }
}


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

package software.aws.toolkits.gradle.changelog.tasks

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import software.aws.toolkits.gradle.changelog.GitStager

abstract class ChangeLogTask : DefaultTask() {
    @Internal
    protected val git = GitStager.create(project.rootDir)

    @InputDirectory
    val changesDirectory: DirectoryProperty = project.objects.directoryProperty().convention(project.rootProject.layout.projectDirectory.dir(".changes"))

    @InputFiles
    val nextReleaseDirectory: DirectoryProperty = project.objects.directoryProperty().convention(changesDirectory.dir("next-release"))

    init {
        group = "changelog"
    }

    protected fun DirectoryProperty.jsonFiles(): FileTree = this.asFileTree.matching {
        include("*.json")
    }
}


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

package software.aws.toolkits.gradle.changelog.tasks

import org.gradle.api.file.ProjectLayout
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import software.aws.toolkits.gradle.changelog.ChangeLogGenerator
import software.aws.toolkits.gradle.changelog.GithubWriter
import software.aws.toolkits.gradle.changelog.ReleaseCreator
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.inject.Inject

open class CreateRelease @Inject constructor(projectLayout: ProjectLayout) : ChangeLogTask() {
    @Input
    val releaseDate: Property<String> = project.objects.property(String::class.java).convention(DateTimeFormatter.ISO_DATE.format(LocalDate.now()))

    @Input
    val releaseVersion: Property<String> = project.objects.property(String::class.java)

    @Input
    @Optional
    val issuesUrl: Provider<String?> = project.objects.property(String::class.java).convention("https://github.com/aws/aws-toolkit-jetbrains/issues")

    @OutputFile
    val releaseFile: RegularFileProperty = project.objects.fileProperty().convention(changesDirectory.file(releaseVersion.map { "$it.json" }))

    @OutputFile
    val changeLogFile: RegularFileProperty = project.objects.fileProperty().convention(projectLayout.buildDirectory.file("releaseChangeLog.md"))

    @TaskAction
    fun create() {
        val releaseDate = DateTimeFormatter.ISO_DATE.parse(releaseDate.get()).let {
            LocalDate.from(it)
        }

        val releaseEntries = nextReleaseDirectory.jsonFiles()

        val creator = ReleaseCreator(releaseEntries.files, releaseFile.get().asFile, logger)
        creator.create(releaseVersion.get(), releaseDate)
        if (git != null) {
            git.stage(releaseFile.get().asFile.absoluteFile)
            git.stage(nextReleaseDirectory.get().asFile.absoluteFile)
        }

        val generator = ChangeLogGenerator(listOf(GithubWriter(changeLogFile.get().asFile.toPath(), issuesUrl.get())), logger)
        generator.use {
            generator.addReleasedChanges(listOf(releaseFile.get().asFile.toPath()))
        }
    }
}


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

package software.aws.toolkits.gradle.changelog.tasks

import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import software.aws.toolkits.gradle.changelog.ChangeLogGenerator
import software.aws.toolkits.gradle.changelog.ChangeLogWriter
import software.aws.toolkits.gradle.changelog.GithubWriter
import software.aws.toolkits.gradle.changelog.JetBrainsWriter

abstract class GenerateChangeLog(private val shouldStage: Boolean) : ChangeLogTask() {
    @Input
    @Optional
    val issuesUrl: Provider<String?> = project.objects.property(String::class.java).convention("https://github.com/aws/aws-toolkit-jetbrains/issues")

    @Input
    val includeUnreleased: Property<Boolean> = project.objects.property(Boolean::class.java).convention(false)

    @OutputFile
    val changeLogFile: RegularFileProperty = project.objects.fileProperty()

    @TaskAction
    fun generate() {
        val writer = createWriter()
        logger.info("Generating Changelog with $writer")
        val generator = ChangeLogGenerator(listOf(writer), logger)
        if (includeUnreleased.get()) {
            val unreleasedEntries = nextReleaseDirectory.jsonFiles().files

            logger.info("Including ${unreleasedEntries.size} unreleased changes")
            if (unreleasedEntries.isNotEmpty()) {
                generator.addUnreleasedChanges(unreleasedEntries.map { it.toPath() })
            }
        } else {
            logger.info("Skipping unreleased changes")
        }

        generator.addReleasedChanges(changesDirectory.jsonFiles().map { it.toPath() })
        generator.close()

        if (shouldStage) {
            git?.stage(changeLogFile.get().asFile)
        }
    }

    protected abstract fun createWriter(): ChangeLogWriter
}

open class GeneratePluginChangeLog : GenerateChangeLog(false) {
    override fun createWriter(): ChangeLogWriter = JetBrainsWriter(changeLogFile.get().asFile, issuesUrl.get())
}

open class GenerateGithubChangeLog : GenerateChangeLog(true) {
    override fun createWriter(): ChangeLogWriter = GithubWriter(changeLogFile.get().asFile.toPath(), issuesUrl.get())
}


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

package software.aws.toolkits.gradle.changelog.tasks

import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import software.aws.toolkits.gradle.changelog.ChangeType
import software.aws.toolkits.gradle.changelog.Entry
import software.aws.toolkits.gradle.changelog.MAPPER
import java.io.File
import java.util.Scanner
import java.util.UUID

open class NewChange : ChangeLogTask() {
    @get:Internal
    internal var defaultChangeType: ChangeType? = null

    @TaskAction
    fun create() {
        val changeType = if (project.hasProperty("changeType")) {
            (project.property("changeType") as? String?)?.toUpperCase()?.let { ChangeType.valueOf(it) }
        } else defaultChangeType
        val description = if (project.hasProperty("description")) {
            project.property("description") as? String?
        } else null

        val input = Scanner(System.`in`)
        val file = when {
            changeType != null && description != null -> createChange(changeType, description)
            else -> promptForChange(input, changeType)
        }
        git?.stage(file)
    }

    private fun promptForChange(input: Scanner, existingChangeType: ChangeType?): File {
        val changeType = existingChangeType ?: promptForChangeType(input)

        logger.lifecycle("> Please enter a change description: ")
        val description = input.nextLine()

        return createChange(changeType, description)
    }

    private fun promptForChangeType(input: Scanner): ChangeType {
        val changeList = ChangeType.values()
            .mapIndexed { index, changeType -> "${index + 1}. ${changeType.sectionTitle}" }
            .joinToString("\n")
        val newFeatureIndex = ChangeType.FEATURE.ordinal + 1
        logger.lifecycle("\n$changeList\n> Please enter change type ($newFeatureIndex): ")

        return input.nextLine().let {
            if (it.isNotBlank()) {
                ChangeType.values()[it.toInt() - 1]
            } else {
                ChangeType.FEATURE
            }
        }
    }

    private fun createChange(changeType: ChangeType, description: String) = newFile(changeType).apply {
        MAPPER.writerWithDefaultPrettyPrinter().writeValue(
            this,
            Entry(changeType, description)
        )
    }

    private fun newFile(changeType: ChangeType) = nextReleaseDirectory.file("${changeType.name.toLowerCase()}-${UUID.randomUUID()}.json").get().asFile.apply {
        parentFile?.mkdirs()
        createNewFile()
    }
}