/*
 * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.ir.util

import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.DescriptorVisibility
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin.Companion.OBJECT_LITERAL
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.CustomKotlinLikeDumpStrategy.Modifiers
import org.jetbrains.kotlin.ir.visitors.IrVisitor
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.utils.Printer

/**
 * Conventions:
 * * For unsupported cases (node, type) it prints a block comment which starts with "/* ERROR:"               (*/<- hack for parser)
 * * Conventions for some operators:
 *     * IMPLICIT_CAST -- expr /*as Type */
 *     * IMPLICIT_NOTNULL -- expr /*!! Type */
 *     * IMPLICIT_COERCION_TO_UNIT -- expr /*~> Unit */
 *     * IMPLICIT_INTEGER_COERCION -- expr /*~> IntType */
 *     * SAM_CONVERSION -- expr /*-> SamType */
 *     * IMPLICIT_DYNAMIC_CAST -- expr /*~> dynamic */
 *     * REINTERPRET_CAST -- expr /*=> Type */
 */
fun IrElement.dumpKotlinLike(options: KotlinLikeDumpOptions = KotlinLikeDumpOptions()): String =
    dumpKotlinLike(this, KotlinLikeDumper::printElement, options)

fun IrType.dumpKotlinLike(): String =
    dumpKotlinLike(this, KotlinLikeDumper::printType)

fun IrTypeArgument.dumpKotlinLike(): String =
    dumpKotlinLike(this, KotlinLikeDumper::printTypeArgument)

private inline fun <T> dumpKotlinLike(
    target: T,
    print: KotlinLikeDumper.(T) -> Unit,
    options: KotlinLikeDumpOptions = KotlinLikeDumpOptions()
): String {
    val sb = StringBuilder()
    KotlinLikeDumper(Printer(sb, 1, "  "), options).print(target)
    return sb.toString()
}

data class KotlinLikeDumpOptions(
    val customDumpStrategy: CustomKotlinLikeDumpStrategy = CustomKotlinLikeDumpStrategy.Default,
    val printRegionsPerFile: Boolean = false,
    val printFileName: Boolean = true,
    val printFilePath: Boolean = true,
    // TODO support
    val useNamedArguments: Boolean = false,
    // TODO support
    val labelPrintingStrategy: LabelPrintingStrategy = LabelPrintingStrategy.NEVER,
    val printFakeOverridesStrategy: FakeOverridesStrategy = FakeOverridesStrategy.ALL,
    val bodyPrintingStrategy: BodyPrintingStrategy = BodyPrintingStrategy.PRINT_BODIES,
    val inferElseBranches: Boolean = false,
    val printUnitReturnType: Boolean = false,
    val stableOrder: Boolean = false,
    val normalizeNames: Boolean = false,
    val printExpectDeclarations: Boolean = true,
    val collapseObjectLiteralBlock: Boolean = false,

    /**
     * Whether to print member declarations (default: true).
     * - For [IrDeclarationContainer]s such as [IrFile] and [IrClass] these are the direct member declarations.
     * - For [IrProperty] these are the backing [IrField] and accessors.
     */
    val printMemberDeclarations: Boolean = true,

    /** When exactly the declaration visibility should be printed? */
    val visibilityPrintingStrategy: VisibilityPrintingStrategy = VisibilityPrintingStrategy.PRINT_IF_NON_PUBLIC,

    /*
    TODO add more options:
     always print modality
     print special names as is, and other strategies?
     print body for default accessors
     print get/set for default accessors
     print type of expressions
     ability to omit all or part of types for local variables
     omit IrSyntheticBody? or even whole declaration
     */
)

enum class LabelPrintingStrategy {
    NEVER,
    ALWAYS,
    SMART // WHEN_REQUIRED
}

enum class FakeOverridesStrategy {
    ALL,
    ALL_EXCEPT_ANY,
    NONE
}

enum class BodyPrintingStrategy {
    NO_BODIES,
    PRINT_ONLY_LOCAL_CLASSES_AND_FUNCTIONS,
    PRINT_BODIES,
}

enum class VisibilityPrintingStrategy {
    ALWAYS,
    PRINT_IF_NON_PUBLIC,
    // TODO: omit local visibility?
}

/**
 * An interface for customizing the Kotlin-like dump.
 * It allows to e.g. skip certain declarations or annotations from the dump, or print arbitrary text before and after each IR element.
 */
interface CustomKotlinLikeDumpStrategy {

    fun shouldPrintAnnotation(annotation: IrConstructorCall, container: IrAnnotationContainer): Boolean = true

    fun willPrintElement(element: IrElement, container: IrDeclaration?, printer: Printer, options: KotlinLikeDumpOptions): Boolean = true

    fun didPrintElement(element: IrElement, container: IrDeclaration?, printer: Printer) {}

    fun transformModifiersForDeclaration(declaration: IrDeclaration, modifiers: Modifiers): Modifiers = modifiers

    data class Modifiers(
        val visibility: DescriptorVisibility = DescriptorVisibilities.DEFAULT_VISIBILITY,
        val isExpect: Boolean = false,
        val modality: Modality? = null,
        val isExternal: Boolean = false,
        val isOverride: Boolean = false,
        val isFakeOverride: Boolean = false,
        val isLateinit: Boolean = false,
        val isTailrec: Boolean = false,
        val isSuspend: Boolean = false,
        val isInner: Boolean = false,
        val isInline: Boolean = false,
        val isValue: Boolean = false,
        val isData: Boolean = false,
        val isCompanion: Boolean = false,
        val isFunInterface: Boolean = false,
        val classKind: ClassKind? = null,
        val isInfix: Boolean = false,
        val isOperator: Boolean = false,
        val isVararg: Boolean = false,
        val isCrossinline: Boolean = false,
        val isNoinline: Boolean = false,
        val isHidden: Boolean = false,
        val isAssignable: Boolean = false,
    )

    object Default : CustomKotlinLikeDumpStrategy
}

// TODO_ conventions:
// TODO support -- for unsupported nodes
// TODO no test -- for the cases with no test(s)
// it's not valid kotlin -- for the cases when used some syntax which is invalid in Kotlin, maybe they are worth to reconsider

/* TODO:
    * origin : class, function, property, ...
        * option?
    * don't print members of kotlin.Any in interfaces? // or just print something like  /* Any members */
        * option?
    * use lambda syntax for FunctionN, including extension lambdas (@ExtensionFunctionType)
        * option?
    * use FQNs?
        * option?
    * don't print coercion to Unit on top level? and inside blocks?
        * option?
    * special render for call site of accessors?
        * option?
    * FlexibleNullability is it about `T!`?
        * option?
    * unique ids for symbols, or SignatureID?
        * option?
    * wrap/escape invalid identifiers with "`", like "$$delegate"
 */

private class KotlinLikeDumper(val p: Printer, val options: KotlinLikeDumpOptions) : IrVisitor<Unit, IrDeclaration?>() {
    private val variableNameData = VariableNameData(options.normalizeNames)
    private var currentWhenStmt: IrWhen? = null

    private val IrSymbol.safeName
        get() = if (!isBound) {
            "/* ERROR: unbound symbol $signature */"
        } else {
            when (val owner = owner) {
                is IrVariable -> owner.normalizedName(variableNameData)
                is IrDeclarationWithName -> owner.name.toString()
                else -> "/* ERROR: unnamed symbol $signature */"
            }
        }

    private val IrFunctionSymbol.safeParameters
        get() = if (!isBound) {
            null
        } else {
            owner.parameters
        }

    private val IrSymbol.safeParentClassName
        get() = if (!isBound) {
            "/* ERROR: unbound symbol $signature */"
        } else {
            (owner as? IrDeclaration)?.parentClassOrNull?.name?.toString() ?: "/* ERROR: unexpected parent for $safeName */"
        }

    private val IrSymbol.safeParentClassOrNull
        get() = if (!isBound) {
            null
        } else {
            (owner as? IrDeclaration)?.parentClassOrNull
        }


    fun printElement(element: IrElement) {
        element.accept(this, null)
    }

    fun printType(type: IrType) {
        type.printTypeWithNoIndent()
    }

    fun printTypeArgument(typeArg: IrTypeArgument) {
        typeArg.printTypeArgumentWithNoIndent()
    }

    @JvmName("orderedDeclarations") // Prevent JVM signature clash
    private fun List<IrDeclaration>.ordered() = if (options.stableOrder) stableOrdered() else this

    @JvmName("orderedTypes") // Prevent JVM signature clash
    private fun List<IrType>.ordered(): List<IrType> {
        if (!options.stableOrder) return this

        fun isNonInterfaceType(type: IrType) = type.classifierOrNull?.let {
            it !is IrClassSymbol || !it.owner.isInterface
        } ?: true

        val (classTypes, interfaceTypes) = partition(::isNonInterfaceType)

        return classTypes.sortedBy(IrType::render) + interfaceTypes.sortedBy(IrType::render)
    }

    private inline fun wrap(element: IrElement, container: IrDeclaration?, block: () -> Unit) {
        if (!options.customDumpStrategy.willPrintElement(element, container, p, options)) return
        try {
            block()
        } finally {
            options.customDumpStrategy.didPrintElement(element, container, p)
        }
    }

    override fun visitElement(element: IrElement, data: IrDeclaration?) {
        val e = "/* ERROR: unsupported element type: " + element.javaClass.simpleName + " */"
        if (element is IrExpression) {
            // TODO move text to the message?
            // TODO better process expressions and statements
            p.printlnWithNoIndent("error(\"\") $e")
        } else {
            p.println(e)
        }
    }

    override fun visitModuleFragment(declaration: IrModuleFragment, data: IrDeclaration?) = wrap(declaration, data) {
        p.println("// MODULE: ${declaration.name.asString()}")
        declaration.acceptChildren(this, null)
    }

    @OptIn(ObsoleteDescriptorBasedAPI::class)
    override fun visitFile(declaration: IrFile, data: IrDeclaration?) = wrap(declaration, data) {
        if (options.printRegionsPerFile) p.println("//region block: ${declaration.name}")

        if (options.printFileName) p.println("// FILE: ${declaration.name}")
        if (options.printFilePath) p.println("// path: ${declaration.path}")
        declaration.printlnAnnotations("file")
        val packageFqName = declaration.packageFqName
        if (!packageFqName.isRoot) {
            p.println("package ${packageFqName.asString()}")
        }
        if (!p.isEmpty) p.printlnWithNoIndent()

        if (options.printMemberDeclarations) {
            declaration.declarations.ordered().forEach { it.accept(this, null) }
        }

        if (options.printRegionsPerFile) p.println("//endregion")
    }

    override fun visitClass(declaration: IrClass, data: IrDeclaration?) = wrap(declaration, data) {
        // TODO omit super class for enums, annotations?
        // TODO omit Companion name for companion objects?
        // TODO do we need to print info about `thisReceiver`?
        // TODO special support for objects?
        if (declaration.isExpect && !options.printExpectDeclarations) return

        declaration.printlnAnnotations()
        p.printIndent()

        declaration.run {
            printModifiersWithNoIndent(
                this,
                Modifiers(
                    visibility = visibility,
                    isExpect = isExpect,
                    modality = modality,
                    isExternal = isExternal,
                    isInner = isInner,
                    isValue = isValue,
                    isData = isData,
                    isCompanion = isCompanion,
                    isFunInterface = isFun,
                    classKind = kind,
                ),
            )
        }

        p.printWithNoIndent(declaration.name.asString())

        declaration.printTypeParametersWithNoIndent()
        // TODO no test
        if (declaration.superTypes.isNotEmpty()) {
            var first = true
            for (type in declaration.superTypes.ordered()) {
                if (type.isAny()) continue

                if (!first) {
                    p.printWithNoIndent(", ")
                } else {
                    p.printWithNoIndent(" : ")
                    first = false
                }

                type.printTypeWithNoIndent()
            }

        }

        declaration.printWhereClauseIfNeededWithNoIndent()

        p.printlnWithNoIndent(" {")

        if (options.printMemberDeclarations) {
            p.pushIndent()
            declaration.declarations.ordered().forEach { it.accept(this, declaration) }
            p.popIndent()
        }

        p.println("}")
        p.printlnWithNoIndent()
    }

    /*
    https://kotlinlang.org/docs/reference/coding-conventions.html#modifiers
    Modifiers order:
    public / protected / private / internal
    expect / actual
    final / open / abstract / sealed / const
    external
    override
    lateinit
    tailrec
    vararg
    suspend
    inner
    enum / annotation / fun // as a modifier in `fun interface`
    companion
    inline
    value
    infix
    operator
    data
     */
    private fun printModifiersWithNoIndent(declaration: IrDeclaration, modifiers: Modifiers) =
        options.customDumpStrategy.transformModifiersForDeclaration(declaration, modifiers).run {
            val isInterfaceMember = declaration is IrOverridableMember && (declaration.parent as? IrClass)?.isInterface == true
            printVisibility(visibility)
            p(isExpect, "expect") // TODO actual?
            val defaultModality = when {
                isInterfaceMember || (isOverride || isFakeOverride) && modality == Modality.OPEN ->
                    Modality.OPEN
                classKind == ClassKind.INTERFACE ->
                    Modality.ABSTRACT
                else ->
                    Modality.FINAL
            }
            p(modality, defaultModality) { name.lowercase() }
            p(isExternal, "external")
            p(isFakeOverride, customModifier("fake"))
            p(isOverride, "override")
            p(isLateinit, "lateinit")
            p(isTailrec, "tailrec")
            printParameterModifiersWithNoIndent(
                isVararg,
                isCrossinline,
                isNoinline,
                isHidden,
                isAssignable,
            )
            p(isSuspend, "suspend")
            p(isInner, "inner")
            p(isInline, "inline")
            p(isValue, "value")
            p(isData, "data")
            p(isCompanion, "companion")
            p(isFunInterface, "fun")
            p(classKind) { name.lowercase().replace('_', ' ') + if (this == ClassKind.ENUM_ENTRY) " class" else "" }
            p(isInfix, "infix")
            p(isOperator, "operator")
        }

    private fun printVisibility(visibility: DescriptorVisibility) {
        // TODO don't print visibility if it's not changed in override?
        val shouldBePrinted = visibility != DescriptorVisibilities.DEFAULT_VISIBILITY ||
                options.visibilityPrintingStrategy == VisibilityPrintingStrategy.ALWAYS

        p(condition = shouldBePrinted, visibility.name)
    }

    private fun printParameterModifiersWithNoIndent(
        isVararg: Boolean,
        isCrossinline: Boolean,
        isNoinline: Boolean,
        isHidden: Boolean,
        isAssignable: Boolean,
    ) {
        p(isVararg, "vararg")
        p(isCrossinline, "crossinline")
        p(isNoinline, "noinline")
        p(isHidden, customModifier("hidden"))
        p(isAssignable, customModifier("var"))
    }

    private fun IrTypeParametersContainer.printTypeParametersWithNoIndent(postfix: String = "") {
        if (typeParameters.isEmpty()) return

        p.printWithNoIndent("<")
        typeParameters.forEachIndexed { i, typeParam ->
            p(i > 0, ",")

            typeParam.printATypeParameterWithNoIndent()
        }
        p.printWithNoIndent(">")
        p.printWithNoIndent(postfix)
    }

    private fun IrTypeParameter.printATypeParameterWithNoIndent() {
        variance.printVarianceWithNoIndent()
        p(isReified, "reified")

        printAnnotationsWithNoIndent()

        p.printWithNoIndent(name.asString())

        if (superTypes.size == 1) {
            p.printWithNoIndent(" : ")
            superTypes.single().printTypeWithNoIndent()
        }
    }

    private fun Variance.printVarianceWithNoIndent() {
        p(this, Variance.INVARIANT) { label }
    }

    private fun filterAnnotations(annotations: List<IrConstructorCall>, container: IrAnnotationContainer): List<IrConstructorCall> =
        annotations.filter { options.customDumpStrategy.shouldPrintAnnotation(it, container) }

    private fun IrAnnotationContainer.printAnnotationsWithNoIndent() {
        filterAnnotations(annotations, this).forEach {
            it.printAnAnnotationWithNoIndent()
            p.printWithNoIndent(" ")
        }
    }

    private fun IrAnnotationContainer.printlnAnnotations(prefix: String = "") {
        filterAnnotations(annotations, this).forEach {
            p.printIndent()
            it.printAnAnnotationWithNoIndent(prefix)
            p.printlnWithNoIndent()
        }
    }

    private fun IrConstructorCall.printAnAnnotationWithNoIndent(prefix: String = "") {
        p.printWithNoIndent("@" + (if (prefix.isEmpty()) "" else "$prefix:"))
        visitConstructorCall(this, null)
    }

    private fun IrTypeParametersContainer.printWhereClauseIfNeededWithNoIndent() {
        if (typeParameters.none { it.superTypes.size > 1 }) return

        p.printWithNoIndent(" where ")

        var first = true
        typeParameters.forEach {
            if (it.superTypes.size > 1) {
                // TODO no test with more than one generic parameter with more supertypes
                first = it.printWhereClauseTypesWithNoIndent(first)
            }
        }
    }

    private fun IrTypeParameter.printWhereClauseTypesWithNoIndent(first: Boolean): Boolean {
        var myFirst = first
        superTypes.ordered().forEach { type ->
            if (!myFirst) {
                p.printWithNoIndent(", ")
            } else {
                myFirst = false
            }

            p.printWithNoIndent(name.asString())
            p.printWithNoIndent(" : ")
            type.printTypeWithNoIndent()
        }

        return myFirst
    }

    private fun IrType.printTypeWithNoIndent() {
        // TODO don't print `Any?` as upper bound?
        printAnnotationsWithNoIndent()
        when (this) {
            is IrSimpleType -> {
                // TODO abbreviation

                val dnn = classifier is IrTypeParameterSymbol && nullability == SimpleTypeNullability.DEFINITELY_NOT_NULL
                if (dnn) {
                    p.printWithNoIndent("(")
                }

                p.printWithNoIndent(classifier.safeName)

                if (arguments.isNotEmpty()) {
                    p.printWithNoIndent("<")
                    arguments.forEachIndexed { i, typeArg ->
                        p(i > 0, ",")

                        typeArg.printTypeArgumentWithNoIndent()
                    }
                    p.printWithNoIndent(">")
                }

                if (dnn) {
                    p.printWithNoIndent(" & Any)")
                }

                if (isMarkedNullable()) p.printWithNoIndent("?")
            }
            is IrDynamicType ->
                p.printWithNoIndent("dynamic")
            is IrErrorType ->
                p.printWithNoIndent("ErrorType")
        }
    }

    private fun IrTypeArgument.printTypeArgumentWithNoIndent() {
        when (this) {
            is IrStarProjection ->
                p.printWithNoIndent("*")
            is IrTypeProjection -> {
                variance.printVarianceWithNoIndent()
                type.printTypeWithNoIndent()
            }
        }
    }

    override fun visitTypeAlias(declaration: IrTypeAlias, data: IrDeclaration?) = wrap(declaration, data) {
        declaration.printlnAnnotations()
        p.printIndent()

        printVisibility(declaration.visibility)
        p(declaration.isActual, "actual")

        p.printWithNoIndent("typealias ")
        p.printWithNoIndent(declaration.name.asString())
        declaration.printTypeParametersWithNoIndent()
        p.printWithNoIndent(" = ")
        declaration.expandedType.printTypeWithNoIndent()

        p.printlnWithNoIndent()
    }

    override fun visitEnumEntry(declaration: IrEnumEntry, data: IrDeclaration?) = wrap(declaration, data) {
        // TODO better rendering for enum entries

        declaration.correspondingClass?.let { p.println() }

        declaration.printlnAnnotations()
        p.printIndent()
        p.printWithNoIndent(declaration.name)
        declaration.initializerExpression?.let {
            if (options.bodyPrintingStrategy == BodyPrintingStrategy.PRINT_BODIES) {
                // it's not valid kotlin
                p.printWithNoIndent(" = ")
            }
            it.accept(this, declaration)
        }
        p.println()

        declaration.correspondingClass?.accept(this, declaration)

        p.println()
    }

    override fun visitAnonymousInitializer(declaration: IrAnonymousInitializer, data: IrDeclaration?) = wrap(declaration, data) {
        // TODO no tests, looks like IrAnonymousInitializer has annotations accidentally.
        declaration.printlnAnnotations()
        p.printIndent()

        // TODO no tests, looks like there are no irText tests for isStatic flag
        p(declaration.isStatic, customModifier("static"))
        p.printWithNoIndent("init")
        if (options.bodyPrintingStrategy == BodyPrintingStrategy.PRINT_BODIES) {
            p.printWithNoIndent(" ")
        }
        declaration.body.accept(this, declaration)

        p.printlnWithNoIndent()
    }

    override fun visitSimpleFunction(declaration: IrSimpleFunction, data: IrDeclaration?) {
        if (declaration.isExpect && !options.printExpectDeclarations) return
        val keyword = buildString {
            if (declaration.isStatic) {
                append(customModifier("static"))
                append(' ')
            }
            append("fun ")
        }
        declaration.printSimpleFunction(
            data,
            keyword,
            declaration.name.asString(),
            printTypeParametersAndExtensionReceiver = true,
            printSignatureAndBody = true,
            printExtraTrailingNewLine = true,
        )
    }

    override fun visitConstructor(declaration: IrConstructor, data: IrDeclaration?) = wrap(declaration, data) {
        // TODO name?
        // TODO is it worth to merge code for IrConstructor and IrSimpleFunction?
        // TODO dispatchReceiverParameter -- outer `this` for inner classes
        // TODO return type?

        declaration.printlnAnnotations()
        declaration.printContextParameters()
        p.printIndent()

        declaration.run {
            printModifiersWithNoIndent(
                this,
                Modifiers(
                    visibility = visibility,
                    isExpect = isExpect,
                    isExternal = isExternal,
                    isInline = isInline,
                ),
            )
        }

        p.printWithNoIndent("constructor")
        declaration.printTypeParametersWithNoIndent()
        declaration.printRegularParametersWithNoIndent()
        declaration.printWhereClauseIfNeededWithNoIndent()
        if (declaration.isPrimary) {
            p.printWithNoIndent(" ", customModifier("primary"))
        }
        declaration.body?.let {
            if (options.bodyPrintingStrategy == BodyPrintingStrategy.PRINT_BODIES) {
                p.printWithNoIndent(" ")
            }
            it.accept(this, declaration)
        }
        p.printlnWithNoIndent()
        p.printlnWithNoIndent()
    }

    private fun IrSimpleFunction.printSimpleFunction(
        data: IrDeclaration?,
        keyword: String,
        name: String,
        printTypeParametersAndExtensionReceiver: Boolean,
        printSignatureAndBody: Boolean,
        printExtraTrailingNewLine: Boolean,
        finishWithNewLine: Boolean = true
    ) {
        /* TODO
            correspondingProperty
            overridden
            dispatchReceiverParameter
         */

        if (options.printFakeOverridesStrategy == FakeOverridesStrategy.NONE && isFakeOverride ||
            options.printFakeOverridesStrategy == FakeOverridesStrategy.ALL_EXCEPT_ANY && isFakeOverriddenFromAny()
        ) {
            return
        }

        wrap(this, data) {
            printlnAnnotations()

            if (printSignatureAndBody) {
                printContextParameters()
            }

            p.print("")

            printModifiersWithNoIndent(
                this,
                Modifiers(
                    visibility = visibility,
                    isExpect = isExpect,
                    modality = modality,
                    isExternal = isExternal,
                    isOverride = overriddenSymbols.isNotEmpty(),
                    isFakeOverride = isFakeOverride,
                    isLateinit = isTailrec,
                    isSuspend = isSuspend,
                    isInline = isInline,
                    isInfix = isInfix,
                    isOperator = isOperator,
                ),
            )

            p.printWithNoIndent(keyword)

            if (printTypeParametersAndExtensionReceiver) printTypeParametersWithNoIndent(postfix = " ")

            if (printTypeParametersAndExtensionReceiver) {
                parameters.firstOrNull { it.kind == IrParameterKind.ExtensionReceiver }?.printExtensionReceiverParameter()
            }

            p.printWithNoIndent(name)

            if (printSignatureAndBody) {
                printRegularParametersWithNoIndent()

                if (options.printUnitReturnType || !returnType.isUnit()) {
                    p.printWithNoIndent(": ")
                    returnType.printTypeWithNoIndent()
                }
                printWhereClauseIfNeededWithNoIndent()

                body?.let {
                    if (options.bodyPrintingStrategy == BodyPrintingStrategy.PRINT_BODIES) {
                        p.printWithNoIndent(" ")
                    }
                    it.accept(this@KotlinLikeDumper, null)
                }

            }

            if (!printSignatureAndBody || body == null || options.bodyPrintingStrategy != BodyPrintingStrategy.PRINT_BODIES) {
                if (finishWithNewLine) {
                    p.printlnWithNoIndent()
                }
            }

            if (printExtraTrailingNewLine)
                p.printlnWithNoIndent()
        }
    }

    private fun IrValueParameter.printExtensionReceiverParameter() {
        type.printTypeWithNoIndent()
        p.printWithNoIndent(".")
    }

    private fun IrFunction.printRegularParametersWithNoIndent() {
        p.printWithNoIndent("(")
        parameters.filter { it.kind == IrParameterKind.Regular }.forEachIndexed { i, param ->
            p(i > 0, ",")
            param.printAValueParameterWithNoIndent(this)
        }
        p.printWithNoIndent(")")
    }

    private fun IrFunction.printContextParameters() {
        val contextParameters = parameters.filter { it.kind == IrParameterKind.Context }
        if (contextParameters.isNotEmpty()) {
            p.print("context(")
            contextParameters.forEachIndexed { i, param ->
                p(i > 0, ",")
                param.printAValueParameterWithNoIndent(this)
            }
            p.printlnWithNoIndent(")")
        }
    }

    private fun IrValueParameter.printAValueParameterWithNoIndent(data: IrDeclaration?) {
        printAnnotationsWithNoIndent()

        printParameterModifiersWithNoIndent(
            isVararg = varargElementType != null,
            isCrossinline,
            isNoinline,
            // TODO no test
            isHidden,
            // TODO no test
            isAssignable
        )

        p.printWithNoIndent(name.asString())
        p.printWithNoIndent(": ")
        (varargElementType ?: type).printTypeWithNoIndent()
        // TODO print it.type too for varargs?

        defaultValue?.let { v ->
            if (options.bodyPrintingStrategy == BodyPrintingStrategy.PRINT_BODIES) {
                p.printWithNoIndent(" = ")
            }
            v.accept(this@KotlinLikeDumper, data)
        }
    }

    override fun visitTypeParameter(declaration: IrTypeParameter, data: IrDeclaration?) = wrap(declaration, data) {
        declaration.printATypeParameterWithNoIndent()
        if (declaration.superTypes.size > 1) declaration.printWhereClauseTypesWithNoIndent(true)
    }

    override fun visitValueParameter(declaration: IrValueParameter, data: IrDeclaration?) = wrap(declaration, data) {
        // TODO index?
        declaration.printAValueParameterWithNoIndent(data)
    }

    override fun visitProperty(declaration: IrProperty, data: IrDeclaration?) = wrap(declaration, data) {
        if (options.printFakeOverridesStrategy == FakeOverridesStrategy.NONE && declaration.isFakeOverride) return

        declaration.printlnAnnotations()
        p.printIndent()

        // TODO better rendering for modifiers on property and accessors
        //  modifiers that could be different between accessors and property have a comment
        declaration.run {
            printModifiersWithNoIndent(
                this,
                Modifiers(
                    // accessors by default have same visibility, but the can define own value
                    visibility = visibility,
                    isExpect = isExpect,
                    modality = modality,
                    isExternal = isExternal,
                    // couldn't be different for getter, possible for set, but it's invalid kotlin
                    isOverride = getter?.overriddenSymbols?.isNotEmpty() == true,
                    isFakeOverride = isFakeOverride,
                    isLateinit = isLateinit,
                    isSuspend = getter?.isSuspend == true,
                    // could be used on property if all accessors have same state, otherwise must be defined on each accessor
                    isInline = false,
                ),
            )
        }

        // TODO don't print borrowed flags and assert that they are same with a setter or don't borrow?
        // TODO we can omit type for set parameter

        p(declaration.isConst, "const")
        p(declaration.getter?.isStatic == true, customModifier("static"))
        p.printWithNoIndent(if (declaration.isVar) "var" else "val")
        p.printWithNoIndent(" ")

        // TODO assert that typeparameters and receiver are ~same for getter ans setter

        declaration.getter?.printTypeParametersWithNoIndent(postfix = " ")

        declaration.getter?.parameters?.firstOrNull { it.kind == IrParameterKind.ExtensionReceiver }?.printExtensionReceiverParameter()

        p.printWithNoIndent(declaration.name.asString())

        val backingField = declaration.backingField

        if (backingField != null && !declaration.isDelegated) {
            p.printWithNoIndent(": ")
            backingField.type.printTypeWithNoIndent()
        } else {
            declaration.getter?.returnType?.let {
                p.printWithNoIndent(": ")
                it.printTypeWithNoIndent()
            }
        }

        /* TODO better rendering for
            * field
            * isDelegated
            * delegated w/o backing field
            * provideDelegate
         */

        if (declaration.isDelegated) {
            p.printWithNoIndent(" ", commentBlock("by"))
        }

        p.printlnWithNoIndent()

        if (options.printMemberDeclarations) {
            p.pushIndent()

            // TODO share code with visitField?
            // it's not valid kotlin
            declaration.backingField?.initializer?.let {
                if (options.bodyPrintingStrategy != BodyPrintingStrategy.NO_BODIES) {
                    // If the strategy is PRINT_ONLY_LOCAL_CLASSES_AND_FUNCTIONS, the local declarations in the backing field initializer
                    // will be printed under 'field'.
                    p.print("field")
                }
                if (options.bodyPrintingStrategy == BodyPrintingStrategy.PRINT_BODIES) {
                    p.printWithNoIndent(" = ")
                }
                it.accept(this, declaration)
                if (options.bodyPrintingStrategy != BodyPrintingStrategy.NO_BODIES) {
                    p.printlnWithNoIndent()
                }
            }

            // TODO generate better name for set parameter `<set-?>`?
            declaration.getter?.printAccessor("get", declaration)
            declaration.setter?.printAccessor("set", declaration)

            p.popIndent()
            p.printlnWithNoIndent()
        }
    }

    private fun IrSimpleFunction.printAccessor(s: String, property: IrDeclaration) {
        val isCustomAccessor = origin != IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR
        printSimpleFunction(
            property,
            keyword = "",
            name = s,
            printTypeParametersAndExtensionReceiver = false,
            printSignatureAndBody = isCustomAccessor,
            printExtraTrailingNewLine = false,
        )
    }

    override fun visitField(declaration: IrField, data: IrDeclaration?) = wrap(declaration, data) {
        declaration.printlnAnnotations()
        p.printIndent()

        declaration.run {
            printModifiersWithNoIndent(
                this,
                Modifiers(
                    visibility = visibility,
                    isExternal = isExternal,
                ),
            )
        }

        if (declaration.isStatic || declaration.isFinal) {
            // it's not valid kotlin unless it's commented
            p.printWithNoIndent(CUSTOM_MODIFIER_START)
            p(declaration.isStatic, "static")
            p(declaration.isFinal, "final")
            p.printWithNoIndent("field")
            p.printWithNoIndent(CUSTOM_MODIFIER_END)
            p.printWithNoIndent(" ")
        }

        p.printWithNoIndent(
            when {
                declaration.correspondingPropertySymbol != null -> "field "
                declaration.isFinal -> "val "
                else -> "var "
            }
        )
        p.printWithNoIndent(declaration.name.asString() + ": ")
        declaration.type.printTypeWithNoIndent()

        declaration.initializer?.let {
            if (options.bodyPrintingStrategy == BodyPrintingStrategy.PRINT_BODIES) {
                p.printWithNoIndent(" = ")
            }
            it.accept(this, declaration)
        }

        // TODO correspondingPropertySymbol

        p.printlnWithNoIndent()
    }

    override fun visitVariable(declaration: IrVariable, data: IrDeclaration?) = wrap(declaration, data) {
        declaration.printlnAnnotations()
        p.printIndent()

        p(declaration.isLateinit, "lateinit")
        p(declaration.isConst, "const")
        declaration.run { printVariable(isVar, normalizedName(variableNameData), type) }

        declaration.initializer?.let {
            p.printWithNoIndent(" = ")
            it.accept(this, declaration)
        }
    }

    override fun visitLocalDelegatedProperty(declaration: IrLocalDelegatedProperty, data: IrDeclaration?) = wrap(declaration, data) {
        declaration.printlnAnnotations()
        p.printIndent()

        // TODO think about better rendering
        declaration.run { printVariable(isVar, name.asString(), type) }

        p.printlnWithNoIndent()
        p.pushIndent()

        declaration.delegate.accept(this, declaration)
        p.printlnWithNoIndent()

        declaration.getter.printAccessor("get", declaration)
        declaration.setter?.printAccessor("set", declaration)

        p.popIndent()
        p.printlnWithNoIndent()
    }

    private fun printVariable(isVar: Boolean, name: String, type: IrType) {
        p.printWithNoIndent(if (isVar) "var" else "val")
        p.printWithNoIndent(" ")
        p.printWithNoIndent(name)
        p.printWithNoIndent(": ")
        type.printTypeWithNoIndent()
    }

    private fun <Body : IrBody> printBody(body: Body, data: IrDeclaration?, actuallyPrint: () -> Unit) = wrap(body, data) {
        when (options.bodyPrintingStrategy) {
            BodyPrintingStrategy.NO_BODIES -> {}
            BodyPrintingStrategy.PRINT_ONLY_LOCAL_CLASSES_AND_FUNCTIONS -> body.acceptChildren(
                // Don't print bodies, but print local classes and functions declared in those bodies
                object : IrVisitor<Unit, IrDeclaration?>() {
                    override fun visitElement(element: IrElement, data: IrDeclaration?) {
                        element.acceptChildren(this, data)
                    }

                    override fun visitDeclaration(declaration: IrDeclarationBase, data: IrDeclaration?) {
                        p.println()
                        p.pushIndent()
                        declaration.accept(this@KotlinLikeDumper, data)
                        p.popIndent()
                    }

                    override fun visitVariable(declaration: IrVariable, data: IrDeclaration?) {
                        declaration.acceptChildren(this, data)
                    }

                    override fun visitLocalDelegatedProperty(declaration: IrLocalDelegatedProperty, data: IrDeclaration?) {
                        declaration.acceptChildren(this, data)
                    }
                },
                data
            )
            BodyPrintingStrategy.PRINT_BODIES -> actuallyPrint()
        }
    }

    override fun visitExpressionBody(body: IrExpressionBody, data: IrDeclaration?) {
        printBody(body, data) {
            // TODO should we print something here?
            body.expression.accept(this, data)
        }
    }

    override fun visitBlockBody(body: IrBlockBody, data: IrDeclaration?) {
        printBody(body, data) {
            body.printStatementContainer("{", "}", data)
            p.printlnWithNoIndent()
        }
    }

    override fun visitComposite(expression: IrComposite, data: IrDeclaration?) = wrap(expression, data) {
        expression.printStatementContainer("// COMPOSITE {", "// }", data, withIndentation = false)
    }

    override fun visitBlock(expression: IrBlock, data: IrDeclaration?) = wrap(expression, data) {
        // TODO special blocks using `origin`
        // TODO inlinedFunctionSymbol for IrReturnableBlock
        // TODO no tests for IrReturnableBlock?
        if (expression.origin == OBJECT_LITERAL && options.collapseObjectLiteralBlock) {
            p.printWithNoIndent("<anonymous object>")
            return
        }
        val kind = when (expression) {
            is IrReturnableBlock -> "RETURNABLE BLOCK"
            is IrInlinedFunctionBlock -> "INLINED FUNCTION BLOCK"
            else -> "BLOCK"
        }
        // it's not valid kotlin
        expression.printStatementContainer("{ // $kind", "}", data)
    }

    private fun IrStatementContainer.printStatementContainer(
        before: String,
        after: String,
        data: IrDeclaration?,
        withIndentation: Boolean = true
    ) {
        // TODO type for IrContainerExpression
        p.printlnWithNoIndent(before)
        if (withIndentation) p.pushIndent()

        statements.forEach {
            if (it is IrExpression) p.printIndent()
            it.accept(this@KotlinLikeDumper, data)
            p.printlnWithNoIndent()
        }

        if (withIndentation) p.popIndent()
        p.print(after)
    }

    override fun visitSyntheticBody(body: IrSyntheticBody, data: IrDeclaration?) {
        printBody(body, data) {
            // it's not valid kotlin
            p.printlnWithNoIndent("/* Synthetic body for ${body.kind} */")
        }
    }

    override fun visitCall(expression: IrCall, data: IrDeclaration?) = wrap(expression, data) {
        // TODO process specially builtin symbols
        expression.printMemberAccessExpressionWithNoIndent(
            expression.symbol.safeName,
            expression.symbol.safeParameters,
            expression.superQualifierSymbol,
            omitAllBracketsIfNoArguments = false,
            data = data,
        )
    }

    override fun visitConstructorCall(expression: IrConstructorCall, data: IrDeclaration?) = wrap(expression, data) {
        expression.printMemberAccessExpressionWithNoIndent(
            expression.symbol.safeParentClassName,
            expression.symbol.safeParameters,
            superQualifierSymbol = null,
            omitAllBracketsIfNoArguments = expression.symbol.safeParentClassOrNull?.isAnnotationClass == true,
            data = data,
        )
    }

    private fun IrMemberAccessExpression<*>.printMemberAccessExpressionWithNoIndent(
        name: String,
        valueParameters: List<IrValueParameter>?,
        superQualifierSymbol: IrClassSymbol?,
        omitAllBracketsIfNoArguments: Boolean,
        data: IrDeclaration?,
        accessOperator: String = ".",
        omitAccessOperatorIfNoReceivers: Boolean = true,
        wrapArguments: Boolean = false
    ) {
        // TODO origin

        val dispatchReceiverParameter = valueParameters?.getOrNull(0)?.takeIf { it.kind == IrParameterKind.DispatchReceiver }
        if (superQualifierSymbol != null) {
            // TODO which super? smart mode?
            p.printWithNoIndent("super<${superQualifierSymbol.safeName}>")
        } else if (dispatchReceiverParameter != null) {
            val dispatchReceiver = arguments.getOrNull(0)
            if (dispatchReceiver != null) {
                dispatchReceiver.accept(this@KotlinLikeDumper, data)
            } else if (this is IrCallableReference<*>) {
                // TODO where from to get type arguments for a class?
                // TODO render it also for static members (from java)
                symbol.safeParentClassOrNull?.let {
                    p.printWithNoIndent(it.name.asString())
                }
            } else {
                p.printWithNoIndent("<missing-receiver>")
            }
        }

        if (!omitAccessOperatorIfNoReceivers || (dispatchReceiverParameter != null || superQualifierSymbol != null)) {
            p.printWithNoIndent(accessOperator)
        }

        p.printWithNoIndent(name)

        if (omitAllBracketsIfNoArguments &&
            typeArguments.isEmpty() &&
            (valueParameters.orEmpty() zip this.arguments)
                .filter { it.first.kind != IrParameterKind.DispatchReceiver }
                .all { it.second == null }
        ) {
            return
        }

        if (wrapArguments) p.printWithNoIndent("/*")

        if (typeArguments.isNotEmpty()) {
            p.printWithNoIndent("<")
            for ((i, param) in typeArguments.withIndex()) {
                p(i > 0, ",")
                // TODO flag to print type param name?
                param?.printTypeWithNoIndent() ?: p.printWithNoIndent(commentBlock("null"))
            }
            p.printWithNoIndent(">")
        }

        p.printWithNoIndent("(")
        var isCommentOpen = false
        var printComma = false
        for (paramIdx in 0..<maxOf(valueParameters?.size ?: 0, arguments.size)) {
            val param = valueParameters?.getOrNull(paramIdx)
            if (param?.kind == IrParameterKind.DispatchReceiver) {
                continue
            }

            // TODO should we print something for omitted arguments (== null)?
            val arg = arguments.getOrNull(paramIdx)
            if (arg != null) {
                if (printComma) p.printWithNoIndent(",")
                when (param?.kind) {
                    IrParameterKind.DispatchReceiver -> {}
                    IrParameterKind.Context, IrParameterKind.ExtensionReceiver -> {
                        if (!wrapArguments && !isCommentOpen) {
                            p.printWithNoIndent("/* ")
                            isCommentOpen = true
                        }
                    }
                    IrParameterKind.Regular, null -> {
                        if (!wrapArguments && isCommentOpen) {
                            p.printWithNoIndent(" */")
                            isCommentOpen = false
                        }
                    }
                }
                if (printComma) p.printWithNoIndent(' ')

                when {
                    param != null -> p.printWithNoIndent(param.name.asString() + " = ")
                    valueParameters != null -> p.printWithNoIndent("\$EXCESSIVE\$ = ")
                    else -> {}
                }

                if (paramIdx > arguments.lastIndex) {
                    p.printWithNoIndent("<missing-argument>")
                } else {
                    arg.accept(this@KotlinLikeDumper, data)
                }

                printComma = true
            }
        }
        if (!wrapArguments && isCommentOpen) p.printWithNoIndent(" */")
        p.printWithNoIndent(")")

        if (wrapArguments) p.printWithNoIndent(" */")
    }

    override fun visitDelegatingConstructorCall(expression: IrDelegatingConstructorCall, data: IrDeclaration?) = wrap(expression, data) {
        // TODO skip call to Any?
        expression.printConstructorCallWithNoIndent(data)
    }

    override fun visitEnumConstructorCall(expression: IrEnumConstructorCall, data: IrDeclaration?) = wrap(expression, data) {
        // TODO skip call to Enum?
        expression.printConstructorCallWithNoIndent(data)
    }

    private fun IrFunctionAccessExpression.printConstructorCallWithNoIndent(
        data: IrDeclaration?
    ) {
        // TODO flag to omit comment block?
        val delegatingClass = symbol.safeParentClassOrNull
        val currentClass = data?.parent as? IrClass
        val delegatingClassName = symbol.safeParentClassName

        val name = if (data is IrConstructor) {
            when (currentClass) {
                // it's not valid kotlin, it's fallback for the case when data wasn't provided
                null -> "delegating/*$delegatingClassName*/"
                delegatingClass -> "this/*$delegatingClassName*/"
                else -> "super/*$delegatingClassName*/"
            }
        } else {
            delegatingClassName // required only for IrEnumConstructorCall
        }

        printMemberAccessExpressionWithNoIndent(
            name,
            symbol.safeParameters,
            superQualifierSymbol = null,
            omitAllBracketsIfNoArguments = false,
            data = data,
        )
    }

    override fun visitInstanceInitializerCall(expression: IrInstanceInitializerCall, data: IrDeclaration?) = wrap(expression, data) {
        // TODO assert that `expression.classSymbol.owner == data.parentAsClass
        // TODO better rendering
        p.printlnWithNoIndent("/* <init>() */")
    }

    override fun visitFunctionExpression(expression: IrFunctionExpression, data: IrDeclaration?) {
        // TODO omit the name when it's possible
        // TODO Is there a difference between `<anonymous>` and `<no name provided>`?
        // TODO Is name of function used somewhere? How it's important?
        // TODO Use lambda syntax when possible
        // TODO don't print visibility?
        p.withholdIndentOnce()
        expression.function.printSimpleFunction(
            data,
            "fun ",
            expression.function.name.asString(),
            printTypeParametersAndExtensionReceiver = true,
            printSignatureAndBody = true,
            printExtraTrailingNewLine = false,
        )
    }

    private fun IrSimpleFunction.dumpForBoundReference(data: IrDeclaration?) = printSimpleFunction(
        data = data,
        keyword = "fun",
        name = "",
        printTypeParametersAndExtensionReceiver = true,
        printSignatureAndBody = true,
        printExtraTrailingNewLine = false,
        finishWithNewLine = false
    )

    override fun visitRichFunctionReference(expression: IrRichFunctionReference, data: IrDeclaration?) = wrap(expression, data) {
        p.printWithNoIndent(expression.type.dumpKotlinLike())
        p.printlnWithNoIndent("(")
        p.pushIndent()
        p.print("/* bound = */ [")
        expression.boundValues.forEachIndexed { index, value ->
            if (index != 0) p.printWithNoIndent(",")
            value.accept(this, data)
        }
        p.printlnWithNoIndent("],")
        p.print("/* invoke = */")
        expression.invokeFunction.dumpForBoundReference(data)
        p.popIndent()
        p.println(")")
    }

    override fun visitRichPropertyReference(expression: IrRichPropertyReference, data: IrDeclaration?) = wrap(expression, data) {
        p.printWithNoIndent(expression.type.dumpKotlinLike())
        p.printlnWithNoIndent("(")
        p.pushIndent()
        p.print("/* bound = */ [")
        expression.boundValues.forEachIndexed { index, value ->
            if (index != 0) p.printWithNoIndent(",")
            value.accept(this, data)
        }
        p.printlnWithNoIndent("],")
        p.print("/* getter = */")
        expression.getterFunction.dumpForBoundReference(data)
        expression.setterFunction?.let {
            p.print("/* setter = */")
            it.dumpForBoundReference(data)
        }
        p.popIndent()
        p.println(")")
    }

    override fun visitGetField(expression: IrGetField, data: IrDeclaration?) = wrap(expression, data) {
        expression.printFieldAccess(data)
    }

    override fun visitSetField(expression: IrSetField, data: IrDeclaration?) = wrap(expression, data) {
        expression.printFieldAccess(data)
        p.printWithNoIndent(" = ")
        expression.value.accept(this, data)
    }

    private fun IrFieldAccessExpression.printFieldAccess(data: IrDeclaration?) {
        // it's not valid kotlin
        receiver?.accept(this@KotlinLikeDumper, data)
        superQualifierSymbol?.let {
            // TODO which supper? smart mode?
            // it's not valid kotlin
            if (receiver != null) p.printWithNoIndent("(")
            p.printWithNoIndent("super<${it.safeName}>")
            if (receiver != null) p.printWithNoIndent(")")
        }

        if (receiver != null || superQualifierSymbol != null) {
            p.printWithNoIndent(".")
        }

        // it's not valid kotlin
        p.printWithNoIndent("#" + symbol.safeName)
    }

    override fun visitGetValue(expression: IrGetValue, data: IrDeclaration?) = wrap(expression, data) {
        p.printWithNoIndent(expression.symbol.safeName)
    }

    override fun visitSetValue(expression: IrSetValue, data: IrDeclaration?) = wrap(expression, data) {
        p.printWithNoIndent(expression.symbol.safeName + " = ")
        expression.value.accept(this, data)
    }

    override fun visitGetObjectValue(expression: IrGetObjectValue, data: IrDeclaration?) = wrap(expression, data) {
        // TODO what if symbol is unbound?
        expression.symbol.defaultType.printTypeWithNoIndent()
    }

    override fun visitGetEnumValue(expression: IrGetEnumValue, data: IrDeclaration?) = wrap(expression, data) {
        p.printWithNoIndent(expression.symbol.safeParentClassName)
        p.printWithNoIndent(".")
        p.printWithNoIndent(expression.symbol.safeName)
    }

    override fun visitRawFunctionReference(expression: IrRawFunctionReference, data: IrDeclaration?) = wrap(expression, data) {
        // TODO support
        // TODO no test
        // it's not valid kotlin
        p.printWithNoIndent("&")
        super.visitRawFunctionReference(expression, data)
    }

    override fun visitReturn(expression: IrReturn, data: IrDeclaration?) = wrap(expression, data) {
        // TODO label
        // TODO optionally don't print Unit when return type of returnTargetSymbol is Unit
        p.printWithNoIndent("return ")
        expression.value.accept(this, data)
    }

    override fun visitThrow(expression: IrThrow, data: IrDeclaration?) {
        p.printWithNoIndent("throw ")
        expression.value.accept(this, data)
    }

    override fun visitStringConcatenation(expression: IrStringConcatenation, data: IrDeclaration?) = wrap(expression, data) {
        // TODO use triple quotes when possible?
        // TODO optionally each argument at a separate line, another option add a wrapping
        expression.arguments.forEachIndexed { i, e ->
            p(i > 0, " +")
            e.accept(this, data)
        }
    }

    override fun visitConst(expression: IrConst, data: IrDeclaration?) = wrap(expression, data) {
        val kind = expression.kind

        val (prefix, postfix) = when (kind) {
            is IrConstKind.Null -> "" to ""
            is IrConstKind.Boolean -> "" to ""
            is IrConstKind.Char -> "'" to "'"
            // it's not valid kotlin
            is IrConstKind.Byte -> "" to "B"
            // it's not valid kotlin
            is IrConstKind.Short -> "" to "S"
            is IrConstKind.Int -> "" to ""
            is IrConstKind.Long -> "" to "L"
            is IrConstKind.String -> "\"" to "\""
            is IrConstKind.Float -> "" to "F"
            is IrConstKind.Double -> "" to ""
        }

        val value = expression.value.toString()
        val safeValue = when (kind) {
            // TODO no tests for escaping quotes (',")
            is IrConstKind.Char -> StringUtil.escapeCharCharacters(value)
            is IrConstKind.String -> StringUtil.escapeStringCharacters(value)
            else -> value
        }

        p.printWithNoIndent(prefix, safeValue, postfix)
    }

    override fun visitVararg(expression: IrVararg, data: IrDeclaration?) = wrap(expression, data) {
        // TODO better rendering?
        // TODO varargElementType
        // it's not valid kotlin
        p.printWithNoIndent("[")
        expression.elements.forEachIndexed { i, e ->
            p(i > 0, ",")
            e.accept(this, data)
        }
        p.printWithNoIndent("]")
    }

    override fun visitSpreadElement(spread: IrSpreadElement, data: IrDeclaration?) = wrap(spread, data) {
        p.printWithNoIndent("*")
        spread.expression.accept(this, data)
    }

    override fun visitTypeOperator(expression: IrTypeOperatorCall, data: IrDeclaration?) = wrap(expression, data) {
        val (operator, after) = when (expression.operator) {
            IrTypeOperator.CAST -> "as" to ""
            IrTypeOperator.IMPLICIT_CAST -> "/*as" to " */"
            IrTypeOperator.IMPLICIT_NOTNULL -> "/*!!" to " */"
            IrTypeOperator.IMPLICIT_COERCION_TO_UNIT -> "/*~>" to " */"
            IrTypeOperator.SAFE_CAST -> "as?" to ""
            IrTypeOperator.INSTANCEOF -> "is" to ""
            IrTypeOperator.NOT_INSTANCEOF -> "!is" to ""
            IrTypeOperator.IMPLICIT_INTEGER_COERCION -> "/*~>" to " */"
            IrTypeOperator.SAM_CONVERSION -> "/*->" to " */"
            IrTypeOperator.IMPLICIT_DYNAMIC_CAST -> "/*~>" to " */"
            IrTypeOperator.REINTERPRET_CAST -> "/*=>" to " */"
        }

        expression.argument.accept(this, data)
        p.printWithNoIndent(" $operator ")
        expression.typeOperand.printTypeWithNoIndent()
        p.printWithNoIndent(after)

    }

    override fun visitWhen(expression: IrWhen, data: IrDeclaration?) = wrap(expression, data) {
        // TODO print if when possible?
        p.printlnWithNoIndent("when {")
        p.pushIndent()

        val savedWhenStmt = currentWhenStmt
        currentWhenStmt = expression
        expression.branches.forEach { it.accept(this, data) }
        currentWhenStmt = savedWhenStmt

        p.popIndent()
        p.print("}")
    }

    override fun visitBranch(branch: IrBranch, data: IrDeclaration?) = wrap(branch, data) {
        p.printIndent()
        branch.condition.let {
            // Deserialized IR contains no IrElseBranch nodes. They are represented with IrBranch(condition=true)
            // To match Kotlin-like IR dump before serialization, the following logic tried to infer IR node which was before serialization
            if (options.inferElseBranches && it is IrConst && it.value == true && branch == currentWhenStmt?.branches?.last())
                p.printWithNoIndent("else")
            else
                it.accept(this, data)
        }
        p.printWithNoIndent(" -> ")
        branch.result.accept(this, data)
        p.println()
    }

    override fun visitElseBranch(branch: IrElseBranch, data: IrDeclaration?) = wrap(branch, data) {
        p.printIndent()
        if ((branch.condition as? IrConst)?.value == true) {
            p.printWithNoIndent("else")
        } else {
            p.printWithNoIndent("/* else */ ")
            branch.condition.accept(this, data)
        }
        p.printWithNoIndent(" -> ")
        branch.result.accept(this, data)
        p.println()
    }

    private fun IrLoop.printLabel() {
        label?.let {
            p.printWithNoIndent(it)
            p.printWithNoIndent("@ ")
        }
    }

    override fun visitWhileLoop(loop: IrWhileLoop, data: IrDeclaration?) = wrap(loop, data) {
        loop.printLabel()

        p.printWithNoIndent("while (")
        loop.condition.accept(this, data)
        p.printWithNoIndent(") ")

        loop.body?.accept(this, data)
    }

    override fun visitDoWhileLoop(loop: IrDoWhileLoop, data: IrDeclaration?) = wrap(loop, data) {
        loop.printLabel()

        p.printWithNoIndent("do")

        loop.body?.accept(this, data)

        p.print("while (")
        loop.condition.accept(this, data)
        p.printWithNoIndent(")")

    }

    override fun visitBreakContinue(jump: IrBreakContinue, data: IrDeclaration?) = wrap(jump, data) {
        // TODO render loop reference
        p.printWithNoIndent(if (jump is IrContinue) "continue" else "break")
        jump.label?.let {
            p.printWithNoIndent("@")
            p.printWithNoIndent(it)
        }
    }

    override fun visitTry(aTry: IrTry, data: IrDeclaration?) = wrap(aTry, data) {
        p.printWithNoIndent("try ")
        aTry.tryResult.accept(this, data)
        p.printlnWithNoIndent()

        aTry.catches.forEach { it.accept(this, data) }

        aTry.finallyExpression?.let {
            p.print("finally ")
            it.accept(this, data)
        }
    }

    override fun visitCatch(aCatch: IrCatch, data: IrDeclaration?) = wrap(aCatch, data) {
        p.print("catch (")
        aCatch.catchParameter.run {
            p.printWithNoIndent(name.asString())
            p.printWithNoIndent(": ")
            type.printTypeWithNoIndent()
        }
        p.printWithNoIndent(")")
        aCatch.result.accept(this, data)
        p.printlnWithNoIndent()
    }

    override fun visitGetClass(expression: IrGetClass, data: IrDeclaration?) = wrap(expression, data) {
        expression.argument.accept(this, data)
        p.printWithNoIndent("::class")
    }

    override fun visitClassReference(expression: IrClassReference, data: IrDeclaration?) = wrap(expression, data) {
        expression.classType.printTypeWithNoIndent()
        p.printWithNoIndent("::class")
    }

    override fun visitFunctionReference(expression: IrFunctionReference, data: IrDeclaration?) = wrap(expression, data) {
        // TODO reflectionTarget
        expression.printCallableReferenceWithNoIndent(expression.symbol.safeParameters, data)
    }

    override fun visitPropertyReference(expression: IrPropertyReference, data: IrDeclaration?) = wrap(expression, data) {
        // TODO do we need additional fields (field, getter, setter)?
        expression.printCallableReferenceWithNoIndent(expression.getter?.safeParameters, data)
    }

    override fun visitLocalDelegatedPropertyReference(expression: IrLocalDelegatedPropertyReference, data: IrDeclaration?) =
        wrap(expression, data) {
            // TODO do we need additional fields (delegate, getter, setter)?
            expression.printCallableReferenceWithNoIndent(expression.getter.safeParameters, data)
        }

    private fun IrCallableReference<*>.printCallableReferenceWithNoIndent(valueParameters: List<IrValueParameter>?, data: IrDeclaration?) {
        val target = symbol.owner as IrDeclarationWithName
        val name = if (target is IrConstructor) target.constructedClass.name else target.name
        printMemberAccessExpressionWithNoIndent(
            name.asString(),
            valueParameters,
            superQualifierSymbol = null,
            omitAllBracketsIfNoArguments = true,
            data = data,
            accessOperator = "::",
            omitAccessOperatorIfNoReceivers = false,
            wrapArguments = true
        )
    }

    override fun visitDynamicOperatorExpression(expression: IrDynamicOperatorExpression, data: IrDeclaration?) = wrap(expression, data) {
        // TODO marker to show that it's dynamic call
        val s = when (val op = expression.operator) {
            IrDynamicOperator.ARRAY_ACCESS -> "[" to "]"
            IrDynamicOperator.INVOKE -> "(" to ")"

            // assert that arguments size is 0
            IrDynamicOperator.UNARY_PLUS,
            IrDynamicOperator.UNARY_MINUS,
            IrDynamicOperator.EXCL,
            IrDynamicOperator.PREFIX_INCREMENT,
            IrDynamicOperator.PREFIX_DECREMENT -> {
                p.printWithNoIndent(op.image)
                "" to ""
            }

            // assert that arguments size is 0
            IrDynamicOperator.POSTFIX_INCREMENT,
            IrDynamicOperator.POSTFIX_DECREMENT -> {
                op.image to ""
            }

            // assert that arguments size is 1
            else -> " ${op.image} " to ""
        }

        expression.receiver.accept(this, data)
        p.printWithNoIndent(s.first)
        expression.arguments.forEachIndexed { i, e ->
            p(i > 0, ",")
            e.accept(this, data)
        }
        p.printWithNoIndent(s.second)
    }

    override fun visitDynamicMemberExpression(expression: IrDynamicMemberExpression, data: IrDeclaration?) = wrap(expression, data) {
        expression.receiver.accept(this, data)
        p.printWithNoIndent(".")
        p.printWithNoIndent(expression.memberName)
    }

    override fun visitErrorExpression(expression: IrErrorExpression, data: IrDeclaration?) = wrap(expression, data) {
        // TODO description
        p.printWithNoIndent("error(\"\") /* ErrorExpression */")
    }

    override fun visitErrorCallExpression(expression: IrErrorCallExpression, data: IrDeclaration?) = wrap(expression, data) {
        // TODO description
        // TODO better rendering
        p.printWithNoIndent("error(\"\") /* ErrorCallExpression */")
        expression.explicitReceiver?.let {
            it.accept(this, data)
            p.printWithNoIndent("; ")
        }
        expression.arguments.forEach { arg ->
            arg.accept(this, data)
            p.printWithNoIndent("; ")
        }
    }

    private fun p(condition: Boolean, s: String) {
        if (condition) p.printWithNoIndent("$s ")
    }

    private fun <T : Any> p(value: T?, defaultValue: T? = null, getString: T.() -> String) {
        if (value == null) return
        p(value != defaultValue, value.getString())
    }

    private fun commentBlock(text: String) = "/* $text */"

    // it's not valid kotlin unless it's commented
    //  ^^^ it's applied to all usages of this function
    private fun customModifier(text: String): String {
        return CUSTOM_MODIFIER_START + text + CUSTOM_MODIFIER_END
    }

    private companion object {
        private const val CUSTOM_MODIFIER_START = "/* "
        private const val CUSTOM_MODIFIER_END = " */"
    }
}
