xpoet_ext.kt

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.
 */

package androidx.room.ext

import androidx.room.compiler.codegen.CodeLanguage
import androidx.room.compiler.codegen.VisibilityModifier
import androidx.room.compiler.codegen.XClassName
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XFunSpec
import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.apply
import androidx.room.compiler.codegen.XMemberName.Companion.companionMember
import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.codegen.asMutableClassName
import com.squareup.kotlinpoet.javapoet.JTypeName
import java.util.concurrent.Callable

object SupportDbTypeNames {
    val DB = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteDatabase")
    val SQLITE_STMT = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteStatement")
    val SQLITE_OPEN_HELPER = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper")
    val SQLITE_OPEN_HELPER_CALLBACK =
        XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper", "Callback")
    val SQLITE_OPEN_HELPER_CONFIG =
        XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper", "Configuration")
    val QUERY = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteQuery")
}

object RoomTypeNames {
    val STRING_UTIL = XClassName.get("$ROOM_PACKAGE.util", "StringUtil")
    val ROOM_DB = XClassName.get(ROOM_PACKAGE, "RoomDatabase")
    val ROOM_DB_KT = XClassName.get(ROOM_PACKAGE, "RoomDatabaseKt")
    val ROOM_DB_CALLBACK = XClassName.get(ROOM_PACKAGE, "RoomDatabase", "Callback")
    val ROOM_DB_CONFIG = XClassName.get(ROOM_PACKAGE, "DatabaseConfiguration")
    val INSERTION_ADAPTER = XClassName.get(ROOM_PACKAGE, "EntityInsertionAdapter")
    val UPSERTION_ADAPTER = XClassName.get(ROOM_PACKAGE, "EntityUpsertionAdapter")
    val DELETE_OR_UPDATE_ADAPTER = XClassName.get(ROOM_PACKAGE, "EntityDeletionOrUpdateAdapter")
    val SHARED_SQLITE_STMT = XClassName.get(ROOM_PACKAGE, "SharedSQLiteStatement")
    val INVALIDATION_TRACKER = XClassName.get(ROOM_PACKAGE, "InvalidationTracker")
    val ROOM_SQL_QUERY = XClassName.get(ROOM_PACKAGE, "RoomSQLiteQuery")
    val OPEN_HELPER = XClassName.get(ROOM_PACKAGE, "RoomOpenHelper")
    val OPEN_HELPER_DELEGATE = XClassName.get(ROOM_PACKAGE, "RoomOpenHelper", "Delegate")
    val OPEN_HELPER_VALIDATION_RESULT =
        XClassName.get(ROOM_PACKAGE, "RoomOpenHelper", "ValidationResult")
    val TABLE_INFO = XClassName.get("$ROOM_PACKAGE.util", "TableInfo")
    val TABLE_INFO_COLUMN = XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "Column")
    val TABLE_INFO_FOREIGN_KEY = XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "ForeignKey")
    val TABLE_INFO_INDEX =
        XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "Index")
    val FTS_TABLE_INFO = XClassName.get("$ROOM_PACKAGE.util", "FtsTableInfo")
    val VIEW_INFO = XClassName.get("$ROOM_PACKAGE.util", "ViewInfo")
    val LIMIT_OFFSET_DATA_SOURCE = XClassName.get("$ROOM_PACKAGE.paging", "LimitOffsetDataSource")
    val DB_UTIL = XClassName.get("$ROOM_PACKAGE.util", "DBUtil")
    val CURSOR_UTIL = XClassName.get("$ROOM_PACKAGE.util", "CursorUtil")
    val MIGRATION = XClassName.get("$ROOM_PACKAGE.migration", "Migration")
    val AUTO_MIGRATION_SPEC = XClassName.get("$ROOM_PACKAGE.migration", "AutoMigrationSpec")
    val UUID_UTIL = XClassName.get("$ROOM_PACKAGE.util", "UUIDUtil")
    val AMBIGUOUS_COLUMN_RESOLVER = XClassName.get(ROOM_PACKAGE, "AmbiguousColumnResolver")
    val RELATION_UTIL = XClassName.get("androidx.room.util", "RelationUtil")
}

object RoomAnnotationTypeNames {
    val QUERY = XClassName.get(ROOM_PACKAGE, "Query")
    val DAO = XClassName.get(ROOM_PACKAGE, "Dao")
    val DATABASE = XClassName.get(ROOM_PACKAGE, "Database")
    val PRIMARY_KEY = XClassName.get(ROOM_PACKAGE, "PrimaryKey")
    val TYPE_CONVERTERS = XClassName.get(ROOM_PACKAGE, "TypeConverters")
    val TYPE_CONVERTER = XClassName.get(ROOM_PACKAGE, "TypeConverter")
    val ENTITY = XClassName.get(ROOM_PACKAGE, "Entity")
}

object PagingTypeNames {
    val DATA_SOURCE = XClassName.get(PAGING_PACKAGE, "DataSource")
    val POSITIONAL_DATA_SOURCE = XClassName.get(PAGING_PACKAGE, "PositionalDataSource")
    val DATA_SOURCE_FACTORY = XClassName.get(PAGING_PACKAGE, "DataSource", "Factory")
    val PAGING_SOURCE = XClassName.get(PAGING_PACKAGE, "PagingSource")
    val LISTENABLE_FUTURE_PAGING_SOURCE =
        XClassName.get(PAGING_PACKAGE, "ListenableFuturePagingSource")
    val RX2_PAGING_SOURCE =
        XClassName.get("$PAGING_PACKAGE.rxjava2", "RxPagingSource")
    val RX3_PAGING_SOURCE =
        XClassName.get("$PAGING_PACKAGE.rxjava3", "RxPagingSource")
}

object LifecyclesTypeNames {
    val LIVE_DATA = XClassName.get(LIFECYCLE_PACKAGE, "LiveData")
    val COMPUTABLE_LIVE_DATA = XClassName.get(
        LIFECYCLE_PACKAGE,
        "ComputableLiveData"
    )
}

object AndroidTypeNames {
    val CURSOR = XClassName.get("android.database", "Cursor")
    val BUILD = XClassName.get("android.os", "Build")
    val CANCELLATION_SIGNAL = XClassName.get("android.os", "CancellationSignal")
}

object CollectionTypeNames {
    val ARRAY_MAP = XClassName.get(COLLECTION_PACKAGE, "ArrayMap")
    val LONG_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "LongSparseArray")
    val INT_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
}

object KotlinCollectionMemberNames {
    val ARRAY_OF_NULLS = XClassName.get("kotlin", "LibraryKt")
        .packageMember("arrayOfNulls")
}

object CommonTypeNames {
    val VOID = Void::class.asClassName()
    val COLLECTION = Collection::class.asClassName()
    val COLLECTIONS = XClassName.get("java.util", "Collections")
    val ARRAYS = XClassName.get("java.util", "Arrays")
    val LIST = List::class.asClassName()
    val MUTABLE_LIST = List::class.asMutableClassName()
    val ARRAY_LIST = XClassName.get("java.util", "ArrayList")
    val MAP = Map::class.asClassName()
    val MUTABLE_MAP = Map::class.asMutableClassName()
    val HASH_MAP = XClassName.get("java.util", "HashMap")
    val QUEUE = XClassName.get("java.util", "Queue")
    val LINKED_HASH_MAP = LinkedHashMap::class.asClassName()
    val SET = Set::class.asClassName()
    val MUTABLE_SET = Set::class.asMutableClassName()
    val HASH_SET = XClassName.get("java.util", "HashSet")
    val STRING = String::class.asClassName()
    val STRING_BUILDER = XClassName.get("java.lang", "StringBuilder")
    val OPTIONAL = XClassName.get("java.util", "Optional")
    val UUID = XClassName.get("java.util", "UUID")
    val BYTE_BUFFER = XClassName.get("java.nio", "ByteBuffer")
    val JAVA_CLASS = XClassName.get("java.lang", "Class")
    val CALLABLE = Callable::class.asClassName()
    val DATE = XClassName.get("java.util", "Date")
}

object ExceptionTypeNames {
    val ILLEGAL_STATE_EXCEPTION = IllegalStateException::class.asClassName()
    val ILLEGAL_ARG_EXCEPTION = IllegalArgumentException::class.asClassName()
}

object GuavaTypeNames {
    val OPTIONAL = XClassName.get("com.google.common.base", "Optional")
    val IMMUTABLE_MULTIMAP_BUILDER = XClassName.get(
        "com.google.common.collect",
        "ImmutableMultimap",
        "Builder"
    )
    val IMMUTABLE_SET_MULTIMAP = XClassName.get(
        "com.google.common.collect",
        "ImmutableSetMultimap"
    )
    val IMMUTABLE_SET_MULTIMAP_BUILDER = XClassName.get(
        "com.google.common.collect",
        "ImmutableSetMultimap",
        "Builder"
    )
    val IMMUTABLE_LIST_MULTIMAP = XClassName.get(
        "com.google.common.collect",
        "ImmutableListMultimap"
    )
    val IMMUTABLE_LIST_MULTIMAP_BUILDER = XClassName.get(
        "com.google.common.collect",
        "ImmutableListMultimap",
        "Builder"
    )
    val IMMUTABLE_MAP = XClassName.get("com.google.common.collect", "ImmutableMap")
    val IMMUTABLE_LIST = XClassName.get("com.google.common.collect", "ImmutableList")
    val IMMUTABLE_LIST_BUILDER = XClassName.get(
        "com.google.common.collect",
        "ImmutableList",
        "Builder"
    )
}

object GuavaUtilConcurrentTypeNames {
    val LISTENABLE_FUTURE = XClassName.get("com.google.common.util.concurrent", "ListenableFuture")
}

object RxJava2TypeNames {
    val FLOWABLE = XClassName.get("io.reactivex", "Flowable")
    val OBSERVABLE = XClassName.get("io.reactivex", "Observable")
    val MAYBE = XClassName.get("io.reactivex", "Maybe")
    val SINGLE = XClassName.get("io.reactivex", "Single")
    val COMPLETABLE = XClassName.get("io.reactivex", "Completable")
}

object RxJava3TypeNames {
    val FLOWABLE = XClassName.get("io.reactivex.rxjava3.core", "Flowable")
    val OBSERVABLE = XClassName.get("io.reactivex.rxjava3.core", "Observable")
    val MAYBE = XClassName.get("io.reactivex.rxjava3.core", "Maybe")
    val SINGLE = XClassName.get("io.reactivex.rxjava3.core", "Single")
    val COMPLETABLE = XClassName.get("io.reactivex.rxjava3.core", "Completable")
}

object ReactiveStreamsTypeNames {
    val PUBLISHER = XClassName.get("org.reactivestreams", "Publisher")
}

object RoomGuavaTypeNames {
    val GUAVA_ROOM = XClassName.get("$ROOM_PACKAGE.guava", "GuavaRoom")
}

object RoomRxJava2TypeNames {
    val RX_ROOM = XClassName.get(ROOM_PACKAGE, "RxRoom")
    val RX_ROOM_CREATE_FLOWABLE = "createFlowable"
    val RX_ROOM_CREATE_OBSERVABLE = "createObservable"
    val RX_EMPTY_RESULT_SET_EXCEPTION = XClassName.get(ROOM_PACKAGE, "EmptyResultSetException")
}

object RoomRxJava3TypeNames {
    val RX_ROOM = XClassName.get("$ROOM_PACKAGE.rxjava3", "RxRoom")
    val RX_ROOM_CREATE_FLOWABLE = "createFlowable"
    val RX_ROOM_CREATE_OBSERVABLE = "createObservable"
    val RX_EMPTY_RESULT_SET_EXCEPTION =
        XClassName.get("$ROOM_PACKAGE.rxjava3", "EmptyResultSetException")
}

object RoomPagingTypeNames {
    val LIMIT_OFFSET_PAGING_SOURCE = XClassName.get(
        "$ROOM_PACKAGE.paging", "LimitOffsetPagingSource"
    )
}

object RoomPagingGuavaTypeNames {
    val LIMIT_OFFSET_LISTENABLE_FUTURE_PAGING_SOURCE = XClassName.get(
            "$ROOM_PACKAGE.paging.guava",
            "LimitOffsetListenableFuturePagingSource"
        )
}

object RoomPagingRx2TypeNames {
    val LIMIT_OFFSET_RX_PAGING_SOURCE = XClassName.get(
            "$ROOM_PACKAGE.paging.rxjava2",
            "LimitOffsetRxPagingSource"
        )
}

object RoomPagingRx3TypeNames {
    val LIMIT_OFFSET_RX_PAGING_SOURCE = XClassName.get(
            "$ROOM_PACKAGE.paging.rxjava3",
            "LimitOffsetRxPagingSource"
        )
}

object RoomCoroutinesTypeNames {
    val COROUTINES_ROOM = XClassName.get(ROOM_PACKAGE, "CoroutinesRoom")
}

object KotlinTypeNames {
    val ANY = Any::class.asClassName()
    val UNIT = XClassName.get("kotlin", "Unit")
    val CONTINUATION = XClassName.get("kotlin.coroutines", "Continuation")
    val CHANNEL = XClassName.get("kotlinx.coroutines.channels", "Channel")
    val RECEIVE_CHANNEL = XClassName.get("kotlinx.coroutines.channels", "ReceiveChannel")
    val SEND_CHANNEL = XClassName.get("kotlinx.coroutines.channels", "SendChannel")
    val FLOW = XClassName.get("kotlinx.coroutines.flow", "Flow")
    val LAZY = XClassName.get("kotlin", "Lazy")
}

object RoomMemberNames {
    val DB_UTIL_QUERY = RoomTypeNames.DB_UTIL.packageMember("query")
    val DB_UTIL_DROP_FTS_SYNC_TRIGGERS = RoomTypeNames.DB_UTIL.packageMember("dropFtsSyncTriggers")
    val CURSOR_UTIL_GET_COLUMN_INDEX =
        RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndex")
    val CURSOR_UTIL_GET_COLUMN_INDEX_OR_THROW =
        RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndexOrThrow")
    val CURSOR_UTIL_WRAP_MAPPED_COLUMNS =
        RoomTypeNames.CURSOR_UTIL.packageMember("wrapMappedColumns")
    val ROOM_SQL_QUERY_ACQUIRE =
        RoomTypeNames.ROOM_SQL_QUERY.companionMember("acquire", isJvmStatic = true)
    val ROOM_DATABASE_WITH_TRANSACTION =
        RoomTypeNames.ROOM_DB_KT.packageMember("withTransaction")
    val TABLE_INFO_READ =
        RoomTypeNames.TABLE_INFO.companionMember("read", isJvmStatic = true)
    val FTS_TABLE_INFO_READ =
        RoomTypeNames.FTS_TABLE_INFO.companionMember("read", isJvmStatic = true)
    val VIEW_INFO_READ =
        RoomTypeNames.VIEW_INFO.companionMember("read", isJvmStatic = true)
}

val DEFERRED_TYPES = listOf(
    LifecyclesTypeNames.LIVE_DATA,
    LifecyclesTypeNames.COMPUTABLE_LIVE_DATA,
    RxJava2TypeNames.FLOWABLE,
    RxJava2TypeNames.OBSERVABLE,
    RxJava2TypeNames.MAYBE,
    RxJava2TypeNames.SINGLE,
    RxJava2TypeNames.COMPLETABLE,
    RxJava3TypeNames.FLOWABLE,
    RxJava3TypeNames.OBSERVABLE,
    RxJava3TypeNames.MAYBE,
    RxJava3TypeNames.SINGLE,
    RxJava3TypeNames.COMPLETABLE,
    GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE,
    KotlinTypeNames.FLOW,
    ReactiveStreamsTypeNames.PUBLISHER
)

fun XTypeName.defaultValue(): String {
    return if (!isPrimitive) {
        "null"
    } else if (this == XTypeName.PRIMITIVE_BOOLEAN) {
        "false"
    } else {
        "0"
    }
}

fun CallableTypeSpecBuilder(
    language: CodeLanguage,
    parameterTypeName: XTypeName,
    callBody: XFunSpec.Builder.() -> Unit
) = XTypeSpec.anonymousClassBuilder(language, "").apply {
    addSuperinterface(CommonTypeNames.CALLABLE.parametrizedBy(parameterTypeName))
    addFunction(
        XFunSpec.builder(
            language = language,
            name = "call",
            visibility = VisibilityModifier.PUBLIC,
            isOverride = true
        ).apply {
            returns(parameterTypeName)
            callBody()
        }.apply(
            javaMethodBuilder = {
                addException(JTypeName.get(Exception::class.java))
            },
            kotlinFunctionBuilder = { }
        ).build()
    )
}

fun Function1TypeSpec(
    language: CodeLanguage,
    parameterTypeName: XTypeName,
    parameterName: String,
    returnTypeName: XTypeName,
    callBody: XFunSpec.Builder.() -> Unit
) = XTypeSpec.anonymousClassBuilder(language, "").apply {
    superclass(
        Function1::class.asClassName().parametrizedBy(parameterTypeName, returnTypeName)
    )
    addFunction(
        XFunSpec.builder(
            language = language,
            name = "invoke",
            visibility = VisibilityModifier.PUBLIC,
            isOverride = true
        ).apply {
            addParameter(parameterTypeName, parameterName)
            returns(returnTypeName)
            callBody()
        }.build()
    )
}.build()

/**
 * Generates an array literal with the given [values]
 *
 * Example: `ArrayLiteral(XTypeName.PRIMITIVE_INT, 1, 2, 3)`
 *
 * For Java will produce: `new int[] {1, 2, 3}`
 *
 * For Kotlin will produce: `intArrayOf(1, 2, 3)`,
 */
fun ArrayLiteral(
    language: CodeLanguage,
    type: XTypeName,
    vararg values: Any
): XCodeBlock {
    val space = when (language) {
        CodeLanguage.JAVA -> "%W"
        CodeLanguage.KOTLIN -> " "
    }
    val initExpr = when (language) {
        CodeLanguage.JAVA -> XCodeBlock.of(language, "new %T[] ", type)
        CodeLanguage.KOTLIN -> XCodeBlock.of(language, getArrayOfFunction(type))
    }
    val openingChar = when (language) {
        CodeLanguage.JAVA -> "{"
        CodeLanguage.KOTLIN -> "("
    }
    val closingChar = when (language) {
        CodeLanguage.JAVA -> "}"
        CodeLanguage.KOTLIN -> ")"
    }
    return XCodeBlock.of(
        language,
        "%L$openingChar%L$closingChar",
        initExpr,
        XCodeBlock.builder(language).apply {
            val joining = Array(values.size) { i ->
                XCodeBlock.of(
                    language,
                    if (type == CommonTypeNames.STRING) "%S" else "%L",
                    values[i]
                )
            }
            val placeholders = joining.joinToString(separator = ",$space") { "%L" }
            add(placeholders, *joining)
        }.build()
    )
}

/**
 * Generates a 2D array literal where the value at `i`,`j` will be produced by `valueProducer.
 * For example:
 * ```
 * DoubleArrayLiteral(XTypeName.PRIMITIVE_INT, 2, { _ -> 3 }, { i, j -> i + j })
 * ```
 * For Java will produce:
 * ```
 * new int[][] {
 *   {0, 1, 2},
 *   {1, 2, 3}
 * }
 * ```
 * For Kotlin will produce:
 * ```
 * arrayOf(
 *   intArrayOf(0, 1, 2),
 *   intArrayOf(1, 2, 3)
 * )
 * ```
 */
fun DoubleArrayLiteral(
    language: CodeLanguage,
    type: XTypeName,
    rowSize: Int,
    columnSizeProducer: (Int) -> Int,
    valueProducer: (Int, Int) -> Any
): XCodeBlock {
    val space = when (language) {
        CodeLanguage.JAVA -> "%W"
        CodeLanguage.KOTLIN -> " "
    }
    val outerInit = when (language) {
        CodeLanguage.JAVA -> XCodeBlock.of(language, "new %T[][] ", type)
        CodeLanguage.KOTLIN -> XCodeBlock.of(language, "arrayOf")
    }
    val innerInit = when (language) {
        CodeLanguage.JAVA -> XCodeBlock.of(language, "", type)
        CodeLanguage.KOTLIN -> XCodeBlock.of(language, getArrayOfFunction(type))
    }
    val openingChar = when (language) {
        CodeLanguage.JAVA -> "{"
        CodeLanguage.KOTLIN -> "("
    }
    val closingChar = when (language) {
        CodeLanguage.JAVA -> "}"
        CodeLanguage.KOTLIN -> ")"
    }
    return XCodeBlock.of(
        language,
        "%L$openingChar%L$closingChar",
        outerInit,
        XCodeBlock.builder(language).apply {
            val joining = Array(rowSize) { i ->
                XCodeBlock.of(
                    language,
                    "%L$openingChar%L$closingChar",
                    innerInit,
                    XCodeBlock.builder(language).apply {
                        val joining = Array(columnSizeProducer(i)) { j ->
                            XCodeBlock.of(
                                language,
                                if (type == CommonTypeNames.STRING) "%S" else "%L",
                                valueProducer(i, j)
                            )
                        }
                        val placeholders = joining.joinToString(separator = ",$space") { "%L" }
                        add(placeholders, *joining)
                    }.build()
                )
            }
            val placeholders = joining.joinToString(separator = ",$space") { "%L" }
            add(placeholders, *joining)
        }.build()
    )
}

private fun getArrayOfFunction(type: XTypeName) = when (type) {
    XTypeName.PRIMITIVE_BOOLEAN -> "booleanArrayOf"
    XTypeName.PRIMITIVE_BYTE -> "byteArrayOf"
    XTypeName.PRIMITIVE_SHORT -> "shortArrayOf"
    XTypeName.PRIMITIVE_INT -> "intArrayOf"
    XTypeName.PRIMITIVE_LONG -> "longArrayOf"
    XTypeName.PRIMITIVE_CHAR -> "charArrayOf"
    XTypeName.PRIMITIVE_FLOAT -> "floatArrayOf"
    XTypeName.PRIMITIVE_DOUBLE -> "doubleArrayOf"
    else -> "arrayOf"
}

fun getToArrayFunction(type: XTypeName) = when (type) {
    XTypeName.PRIMITIVE_BOOLEAN -> "toBooleanArray()"
    XTypeName.PRIMITIVE_BYTE -> "toByteArray()"
    XTypeName.PRIMITIVE_SHORT -> "toShortArray()"
    XTypeName.PRIMITIVE_INT -> "toIntArray()"
    XTypeName.PRIMITIVE_LONG -> "toLongArray()"
    XTypeName.PRIMITIVE_CHAR -> "toCharArray()"
    XTypeName.PRIMITIVE_FLOAT -> "toFloatArray()"
    XTypeName.PRIMITIVE_DOUBLE -> "toDoubleArray()"
    else -> error("Provided type expected to be primitive. Found: $type")
}

/**
 * Code of expression for [Collection.size] in Kotlin, and [java.util.Collection.size] for Java.
 */
fun CollectionsSizeExprCode(language: CodeLanguage, varName: String) = XCodeBlock.of(
    language,
    when (language) {
        CodeLanguage.JAVA -> "%L.size()" // java.util.Collections.size()
        CodeLanguage.KOTLIN -> "%L.size" // kotlin.collections.Collection.size
    },
    varName
)

/**
 * Code of expression for [Array.size] in Kotlin, and `arr.length` for Java.
 */
fun ArraySizeExprCode(language: CodeLanguage, varName: String) = XCodeBlock.of(
    language,
    when (language) {
        CodeLanguage.JAVA -> "%L.length" // Just `arr.length`
        CodeLanguage.KOTLIN -> "%L.size" // kotlin.Array.size and primitives (e.g. IntArray)
    },
    varName
)

/**
 * Code of expression for [Map.keys] in Kotlin, and [java.util.Map.keySet] for Java.
 */
fun MapKeySetExprCode(language: CodeLanguage, varName: String) = XCodeBlock.of(
    language,
    when (language) {
        CodeLanguage.JAVA -> "%L.keySet()" // java.util.Map.keySet()
        CodeLanguage.KOTLIN -> "%L.keys" // kotlin.collections.Map.keys
    },
    varName
)