/*
 * Copyright 2010-2015 JetBrains s.r.o.
 *
 * 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 org.jetbrains.kotlin.j2k

import com.intellij.psi.*
import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT
import com.intellij.psi.CommonClassNames.JAVA_LANG_STRING
import com.intellij.psi.impl.PsiExpressionEvaluator
import org.jetbrains.kotlin.j2k.ast.*
import java.io.PrintStream
import java.util.*

enum class SpecialMethod(val qualifiedClassName: String?, val methodName: String, val parameterCount: Int?) {
    CHAR_SEQUENCE_LENGTH(CharSequence::class.java.name, "length", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse()
    },

    COLLECTION_SIZE(Collection::class.java.name, "size", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse()
    },

    COLLECTION_TO_ARRAY(Collection::class.java.name, "toArray", 0) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("toTypedArray", argumentsNotNull())
    },

    COLLECTION_TO_ARRAY_WITH_ARG(Collection::class.java.name, "toArray", 1) {
        override fun ConvertCallData.convertCall() = copy(arguments = emptyList()).convertWithChangedName("toTypedArray", emptyList())
    },

    MAP_SIZE(Map::class.java.name, "size", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse()
    },

    MAP_KEY_SET(Map::class.java.name, "keySet", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse("keys")
    },

    MAP_PUT_IF_ABSENT(Map::class.java.name, "putIfAbsent", 2) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_REMOVE(Map::class.java.name, "remove", 2) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_REPLACE(Map::class.java.name, "replace", 3) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_REPLACE_ALL(Map::class.java.name, "replaceAll", 1) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_COMPUTE(Map::class.java.name, "compute", 2) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_COMPUTE_IF_ABSENT(Map::class.java.name, "computeIfAbsent", 2) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_COMPUTE_IF_PRESENT(Map::class.java.name, "computeIfPresent", 2) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_MERGE(Map::class.java.name, "merge", 3) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_GET_OR_DEFAULT(Map::class.java.name, "getOrDefault", 2) {
        override fun ConvertCallData.convertCall() = convertWithReceiverCast()
    },

    MAP_VALUES(Map::class.java.name, "values", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse()
    },

    MAP_ENTRY_SET(Map::class.java.name, "entrySet", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse("entries")
    },

    ENUM_NAME(Enum::class.java.name, "name", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse("name")
    },

    ENUM_ORDINAL(Enum::class.java.name, "ordinal", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse()
    },

    CHAR_AT(CharSequence::class.java.name, "charAt", 1) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("get", argumentsNotNull())
    },

    NUMBER_BYTE_VALUE(Number::class.java.name, "byteValue", 0) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("toByte", argumentsNotNull())
    },

    NUMBER_SHORT_VALUE(Number::class.java.name, "shortValue", 0) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("toShort", argumentsNotNull())
    },

    NUMBER_INT_VALUE(Number::class.java.name, "intValue", 0) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("toInt", argumentsNotNull())
    },

    NUMBER_LONG_VALUE(Number::class.java.name, "longValue", 0) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("toLong", argumentsNotNull())
    },

    NUMBER_FLOAT_VALUE(Number::class.java.name, "floatValue", 0) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("toFloat", argumentsNotNull())
    },

    NUMBER_DOUBLE_VALUE(Number::class.java.name, "doubleValue", 0) {
        override fun ConvertCallData.convertCall() = convertWithChangedName("toDouble", argumentsNotNull())
    },

    LIST_REMOVE(List::class.java.name, "remove", 1) {
        override fun matches(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher)
                = super.matches(method, superMethodsSearcher) && method.parameterList.parameters.single().type.canonicalText == "int"

        override fun ConvertCallData.convertCall() = convertWithChangedName("removeAt", argumentsNotNull())
    },

    THROWABLE_GET_MESSAGE(Throwable::class.java.name, "getMessage", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse("message")
    },

    THROWABLE_GET_CAUSE(Throwable::class.java.name, "getCause", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse("cause")
    },

    MAP_ENTRY_GET_KEY(Map::class.java.name + ".Entry", "getKey", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse("key")
    },

    MAP_ENTRY_GET_VALUE(Map::class.java.name + ".Entry", "getValue", 0) {
        override fun ConvertCallData.convertCall() = convertMethodCallToPropertyUse("value")
    },

    OBJECT_EQUALS(null, "equals", 1) {
        override fun matches(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher): Boolean
                = super.matches(method, superMethodsSearcher) && method.parameterList.parameters.single().type.canonicalText == JAVA_LANG_OBJECT

        override fun ConvertCallData.convertCall(): Expression? {
            if (qualifier == null || qualifier is PsiSuperExpression) return null
            return BinaryExpression(codeConverter.convertExpression(qualifier), codeConverter.convertExpression(arguments.single()), Operator.EQEQ)
        }
    },

    OBJECT_GET_CLASS(JAVA_LANG_OBJECT, "getClass", 0) {
        override fun ConvertCallData.convertCall(): Expression {
            val identifier = Identifier.withNoPrototype("javaClass", isNullable = false)
            return if (qualifier != null)
                QualifiedExpression(codeConverter.convertExpression(qualifier), identifier, null)
            else
                identifier
        }
    },

    OBJECTS_EQUALS("java.util.Objects", "equals", 2) {
        override fun ConvertCallData.convertCall()
                = BinaryExpression(codeConverter.convertExpression(arguments[0]), codeConverter.convertExpression(arguments[1]), Operator.EQEQ)
    },

    COLLECTIONS_EMPTY_LIST(Collections::class.java.name, "emptyList", 0) {
        override fun ConvertCallData.convertCall()
                = MethodCallExpression.buildNonNull(null, "emptyList", ArgumentList.withNoPrototype(), typeArgumentsConverted)
    },

    COLLECTIONS_EMPTY_SET(Collections::class.java.name, "emptySet", 0) {
        override fun ConvertCallData.convertCall()
                = MethodCallExpression.buildNonNull(null, "emptySet", ArgumentList.withNoPrototype(), typeArgumentsConverted)
    },

    COLLECTIONS_EMPTY_MAP(Collections::class.java.name, "emptyMap", 0) {
        override fun ConvertCallData.convertCall()
                = MethodCallExpression.buildNonNull(null, "emptyMap", ArgumentList.withNoPrototype(), typeArgumentsConverted)
    },

    COLLECTIONS_SINGLETON_LIST(Collections::class.java.name, "singletonList", 1) {
        override fun ConvertCallData.convertCall(): MethodCallExpression {
            val argumentList = ArgumentList.withNoPrototype(codeConverter.convertExpression(arguments.single()))
            return MethodCallExpression.buildNonNull(null, "listOf", argumentList, typeArgumentsConverted)
        }
    },

    COLLECTIONS_SINGLETON(Collections::class.java.name, "singleton", 1) {
        override fun ConvertCallData.convertCall(): MethodCallExpression {
            val argumentList = ArgumentList.withNoPrototype(codeConverter.convertExpression(arguments.single()))
            return MethodCallExpression.buildNonNull(null, "setOf", argumentList, typeArgumentsConverted)
        }
    },

    STRING_TRIM(JAVA_LANG_STRING, "trim", 0) {
        override fun ConvertCallData.convertCall(): Expression? {
            val comparison = BinaryExpression(Identifier.withNoPrototype("it", isNullable = false), LiteralExpression("' '").assignNoPrototype(), Operator(JavaTokenType.LE).assignNoPrototype()).assignNoPrototype()
            val argumentList = ArgumentList.withNoPrototype(LambdaExpression(null, Block.of(comparison).assignNoPrototype()))
            return MethodCallExpression.buildNonNull(codeConverter.convertExpression(qualifier), "trim", argumentList, dotPrototype = dot)
        }
    },

    STRING_REPLACE_ALL(JAVA_LANG_STRING, "replaceAll", 2) {
        override fun ConvertCallData.convertCall(): MethodCallExpression {
            val argumentList = ArgumentList.withNoPrototype(
                    codeConverter.convertToRegex(arguments[0]),
                    codeConverter.convertExpression(arguments[1])
            )
            return MethodCallExpression.buildNonNull(codeConverter.convertExpression(qualifier), "replace", argumentList, dotPrototype = dot)
        }
    },

    STRING_REPLACE_FIRST(JAVA_LANG_STRING, "replaceFirst", 2) {
        override fun ConvertCallData.convertCall(): MethodCallExpression {
            return MethodCallExpression.buildNonNull(
                    codeConverter.convertExpression(qualifier), "replaceFirst",
                    ArgumentList.withNoPrototype(
                            codeConverter.convertToRegex(arguments[0]),
                            codeConverter.convertExpression(arguments[1])
                    ),
                    dotPrototype = dot
            )
        }
    },

    STRING_MATCHES(JAVA_LANG_STRING, "matches", 1) {
        override fun ConvertCallData.convertCall(): MethodCallExpression {
            val argumentList = ArgumentList.withNoPrototype(codeConverter.convertToRegex(arguments.single()))
            return MethodCallExpression.buildNonNull(codeConverter.convertExpression(qualifier), "matches", argumentList, dotPrototype = dot)
        }
    },

    STRING_SPLIT(JAVA_LANG_STRING, "split", 1) {
        override fun ConvertCallData.convertCall(): Expression? {
            val splitCall = MethodCallExpression.buildNonNull(
                    codeConverter.convertExpression(qualifier),
                    "split",
                    ArgumentList.withNoPrototype(codeConverter.convertToRegex(arguments.single())),
                    dotPrototype = dot
            ).assignNoPrototype()
            val isEmptyCall = MethodCallExpression.buildNonNull(Identifier.withNoPrototype("it", isNullable = false), "isEmpty").assignNoPrototype()
            val isEmptyCallBlock = Block.of(isEmptyCall).assignNoPrototype()
            val dropLastCall = MethodCallExpression.buildNonNull(
                    splitCall,
                    "dropLastWhile",
                    ArgumentList.withNoPrototype(LambdaExpression(null, isEmptyCallBlock).assignNoPrototype())
            ).assignNoPrototype()
            return MethodCallExpression.buildNonNull(dropLastCall, "toTypedArray")
        }
    },

    STRING_SPLIT_LIMIT(JAVA_LANG_STRING, "split", 2) {
        override fun ConvertCallData.convertCall(): Expression?  {
            val patternArgument = codeConverter.convertToRegex(arguments[0])
            val limitArgument = codeConverter.convertExpression(arguments[1])
            val evaluator = PsiExpressionEvaluator()
            val limit = evaluator.computeConstantExpression(arguments[1], /* throwExceptionOnOverflow = */ false) as? Int
            val splitArguments = when {
                    limit == null ->  // not a constant
                        listOf(patternArgument, MethodCallExpression.buildNonNull(limitArgument, "coerceAtLeast", ArgumentList.withNoPrototype(LiteralExpression("0").assignNoPrototype())).assignNoPrototype())
                    limit < 0 ->      // negative, same behavior as split(regex) in kotlin
                        listOf(patternArgument)
                    limit == 0 -> { // zero, same replacement as for split without limit
                        val newCallData = copy(arguments = listOf(arguments[0]))
                        return STRING_SPLIT.convertCall(newCallData)
                    }
                    else ->           // positive, same behavior as split(regex, limit) in kotlin
                        listOf(patternArgument, limitArgument)
            }

            val splitCall = MethodCallExpression.buildNonNull(
                    codeConverter.convertExpression(qualifier),
                    "split",
                    ArgumentList.withNoPrototype(splitArguments),
                    dotPrototype = dot
            ).assignNoPrototype()
            return MethodCallExpression.buildNonNull(splitCall, "toTypedArray")
        }
    },

    STRING_JOIN(JAVA_LANG_STRING, "join", 2) {
        override fun matches(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher): Boolean
                = super.matches(method, superMethodsSearcher) && method.parameterList.parameters.last().type.canonicalText == "java.lang.Iterable<? extends java.lang.CharSequence>"

        override fun ConvertCallData.convertCall(): Expression? {
            val argumentList = ArgumentList.withNoPrototype(codeConverter.convertExpressionsInList(arguments.take(1)))
            return MethodCallExpression.buildNonNull(codeConverter.convertExpression(arguments[1]), "joinToString", argumentList)
        }
    },

    STRING_JOIN_VARARG(JAVA_LANG_STRING, "join", null) {
        override fun matches(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher): Boolean
                = super.matches(method, superMethodsSearcher) && method.parameterList.let { it.parametersCount == 2 && it.parameters.last().isVarArgs }

        override fun ConvertCallData.convertCall(): Expression? {
            return if (arguments.size == 2 && arguments.last().isAssignableToCharSequenceArray()) {
                STRING_JOIN.convertCall(this)
            }
            else {
                MethodCallExpression.buildNonNull(
                        MethodCallExpression.buildNonNull(null, "arrayOf", ArgumentList.withNoPrototype(codeConverter.convertExpressionsInList(arguments.drop(1)))).assignNoPrototype(),
                        "joinToString",
                        ArgumentList.withNoPrototype(codeConverter.convertExpressionsInList(arguments.take(1)))
                )
            }
        }

        private fun PsiExpression.isAssignableToCharSequenceArray(): Boolean {
            val charSequenceType = PsiType.getTypeByName("java.lang.CharSequence", project, resolveScope)
            return (type as? PsiArrayType)?.componentType?.let { charSequenceType.isAssignableFrom(it) } ?: false
        }
    },

    STRING_CONCAT(JAVA_LANG_STRING, "concat", 1) {
        override fun ConvertCallData.convertCall()
                = BinaryExpression(codeConverter.convertExpression(qualifier), codeConverter.convertExpression(arguments.single()), Operator(JavaTokenType.PLUS).assignNoPrototype())
    },

    STRING_COMPARE_TO_IGNORE_CASE(JAVA_LANG_STRING, "compareToIgnoreCase", 1) {
        override fun ConvertCallData.convertCall() = convertWithIgnoreCaseArgument("compareTo")
    },

    STRING_EQUALS_IGNORE_CASE(JAVA_LANG_STRING, "equalsIgnoreCase", 1) {
        override fun ConvertCallData.convertCall() = convertWithIgnoreCaseArgument("equals")
    },

    STRING_REGION_MATCHES(JAVA_LANG_STRING, "regionMatches", 5) {
        override fun ConvertCallData.convertCall()
                = copy(arguments = arguments.drop(1)).convertWithIgnoreCaseArgument("regionMatches", ignoreCaseArgument = arguments.first())
    },

    STRING_GET_BYTES(JAVA_LANG_STRING, "getBytes", null) {
        override fun ConvertCallData.convertCall(): MethodCallExpression {
            val charsetArg = arguments.lastOrNull()?.takeIf { it.type?.canonicalText == JAVA_LANG_STRING }
            val convertedArguments = codeConverter.convertExpressionsInList(arguments).map {
                if (charsetArg != null && it.prototypes?.singleOrNull()?.element == charsetArg)
                    MethodCallExpression.buildNonNull(null, "charset", ArgumentList.withNoPrototype(it)).assignNoPrototype()
                else
                    it
            }
            return MethodCallExpression.buildNonNull(
                    codeConverter.convertExpression(qualifier),
                    "toByteArray",
                    ArgumentList.withNoPrototype(convertedArguments),
                    dotPrototype = dot
            )
        }
    },

    STRING_GET_CHARS(JAVA_LANG_STRING, "getChars", 4) {
        override fun ConvertCallData.convertCall(): MethodCallExpression {
            // reorder parameters: srcBegin(0), srcEnd(1), dst(2), dstOffset(3) -> destination(2), destinationOffset(3), startIndex(0), endIndex(1)
            val argumentList = ArgumentList.withNoPrototype(codeConverter.convertExpressionsInList(arguments.slice(listOf(2, 3, 0, 1))))
            return MethodCallExpression.buildNonNull(
                    codeConverter.convertExpression(qualifier),
                    "toCharArray",
                    argumentList,
                    dotPrototype = dot
            )
        }
    },

    STRING_VALUE_OF_CHAR_ARRAY(JAVA_LANG_STRING, "valueOf", null) {
        override fun matches(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher): Boolean {
            return super.matches(method, superMethodsSearcher)
                    && method.parameterList.parametersCount.let { it == 1 || it == 3}
                    && method.parameterList.parameters.first().type.canonicalText == "char[]"
        }

        override fun ConvertCallData.convertCall()
                = MethodCallExpression.buildNonNull(null, "String", ArgumentList.withNoPrototype(codeConverter.convertExpressionsInList(arguments)))
    },

    STRING_COPY_VALUE_OF_CHAR_ARRAY(JAVA_LANG_STRING, "copyValueOf", null) {
        override fun matches(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher): Boolean {
            return super.matches(method, superMethodsSearcher)
                    && method.parameterList.parametersCount.let { it == 1 || it == 3 }
                    && method.parameterList.parameters.first().type.canonicalText == "char[]"
        }

        override fun ConvertCallData.convertCall()
                = STRING_VALUE_OF_CHAR_ARRAY.convertCall(this)
    },

    STRING_VALUE_OF(JAVA_LANG_STRING, "valueOf", 1) {
        override fun ConvertCallData.convertCall()
                = MethodCallExpression.buildNonNull(codeConverter.convertExpression(arguments.single(), shouldParenthesize = true), "toString")
    },

    SYSTEM_OUT_PRINTLN(PrintStream::class.java.name, "println", null) {
        override fun ConvertCallData.convertCall() = convertSystemOutMethodCall(methodName)
    },

    SYSTEM_OUT_PRINT(PrintStream::class.java.name, "print", null) {
        override fun ConvertCallData.convertCall() = convertSystemOutMethodCall(methodName)
    };

    open fun matches(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher): Boolean
            = method.name == methodName && matchesClass(method, superMethodsSearcher) && matchesParameterCount(method)

    protected fun matchesClass(method: PsiMethod, superMethodsSearcher: SuperMethodsSearcher): Boolean {
        if (qualifiedClassName == null) return true
        val superMethods = superMethodsSearcher.findDeepestSuperMethods(method)
        return if (superMethods.isEmpty())
            method.containingClass?.qualifiedName == qualifiedClassName
        else
            superMethods.any { it.containingClass?.qualifiedName == qualifiedClassName }
    }

    protected fun matchesParameterCount(method: PsiMethod) = parameterCount == null || parameterCount == method.parameterList.parametersCount

    data class ConvertCallData(
            val qualifier: PsiExpression?,
            @Suppress("ArrayInDataClass") val arguments: List<PsiExpression>,
            val typeArgumentsConverted: List<Type>,
            val dot: PsiElement?,
            val lPar: PsiElement?,
            val rPar: PsiElement?,
            val codeConverter: CodeConverter
    )

    @JvmName("convertCallPublic")
    fun convertCall(data: ConvertCallData): Expression? = data.convertCall()

    protected abstract fun ConvertCallData.convertCall(): Expression?

    protected fun ConvertCallData.convertMethodCallToPropertyUse(propertyName: String = methodName): Expression {
        val identifier = Identifier.withNoPrototype(propertyName, isNullable = false)
        return if (qualifier != null)
            QualifiedExpression(codeConverter.convertExpression(qualifier), identifier, dot)
        else
            identifier
    }

    protected fun ConvertCallData.argumentsNotNull() = arguments.map { Nullability.NotNull }

    protected fun ConvertCallData.convertWithChangedName(name: String, argumentNullabilities: List<Nullability>): MethodCallExpression {
        assert(argumentNullabilities.size == arguments.size)
        val argumentsConverted = arguments.zip(argumentNullabilities).map {
            codeConverter.convertExpression(it.first, null, it.second).assignPrototype(it.first, CommentsAndSpacesInheritance.LINE_BREAKS)
        }
        val argumentList = ArgumentList(argumentsConverted, LPar.withPrototype(lPar), RPar.withPrototype(rPar)).assignNoPrototype()
        return MethodCallExpression.buildNonNull(
                codeConverter.convertExpression(qualifier),
                name,
                argumentList,
                typeArgumentsConverted,
                dot)
    }

    protected fun ConvertCallData.convertWithReceiverCast(): MethodCallExpression? {
        val convertedArguments = codeConverter.convertExpressionsInList(arguments)
        val qualifierWithCast = castQualifierToType(codeConverter, qualifier!!, qualifiedClassName!!) ?: return null
        return MethodCallExpression.buildNonNull(
                qualifierWithCast,
                methodName,
                ArgumentList.withNoPrototype(convertedArguments),
                typeArgumentsConverted,
                dot)
    }

    private fun castQualifierToType(codeConverter: CodeConverter, qualifier: PsiExpression, type: String): TypeCastExpression? {
        val convertedQualifier = codeConverter.convertExpression(qualifier)
        val qualifierType = codeConverter.typeConverter.convertType(qualifier.type)
        val typeArgs = (qualifierType as? ClassType)?.referenceElement?.typeArgs ?: emptyList()
        val referenceElement = ReferenceElement(Identifier.withNoPrototype(type), typeArgs).assignNoPrototype()
        val newType = ClassType(referenceElement, Nullability.Default, codeConverter.settings).assignNoPrototype()
        return TypeCastExpression(newType, convertedQualifier).assignNoPrototype()
    }

    protected fun ConvertCallData.convertWithIgnoreCaseArgument(methodName: String, ignoreCaseArgument: PsiExpression? = null): Expression {
        val ignoreCaseExpression = ignoreCaseArgument?.let { codeConverter.convertExpression(it) }
                                   ?: LiteralExpression("true").assignNoPrototype()
        val ignoreCaseArgumentExpression = AssignmentExpression(Identifier.withNoPrototype("ignoreCase"), ignoreCaseExpression, Operator.EQ).assignNoPrototype()
        val convertedArguments = arguments.map {
            codeConverter.convertExpression(it, null, Nullability.NotNull).assignPrototype(it, CommentsAndSpacesInheritance.LINE_BREAKS)
        } + ignoreCaseArgumentExpression
        val argumentList = ArgumentList(convertedArguments, LPar.withPrototype(lPar), RPar.withPrototype(rPar)).assignNoPrototype()
        return MethodCallExpression.buildNonNull(
                codeConverter.convertExpression(qualifier),
                methodName,
                argumentList,
                typeArgumentsConverted,
                dot)
    }

    protected fun ConvertCallData.convertSystemOutMethodCall(methodName: String): Expression? {
        if (qualifier !is PsiReferenceExpression) return null
        val qqualifier = qualifier.qualifierExpression as? PsiReferenceExpression ?: return null
        if (qqualifier.canonicalText != "java.lang.System") return null
        if (qualifier.referenceName != "out") return null
        if (typeArgumentsConverted.isNotEmpty()) return null
        val argumentList = ArgumentList(
                codeConverter.convertExpressionsInList(arguments),
                LPar.withPrototype(lPar),
                RPar.withPrototype(rPar)
        ).assignNoPrototype()
        return MethodCallExpression.buildNonNull(null, methodName, argumentList)
    }

    protected fun CodeConverter.convertToRegex(expression: PsiExpression?): Expression
            = MethodCallExpression.buildNonNull(convertExpression(expression, shouldParenthesize = true), "toRegex").assignNoPrototype()

    companion object {
        private val valuesByName = values().groupBy { it.methodName }

        fun match(method: PsiMethod, argumentCount: Int, services: JavaToKotlinConverterServices): SpecialMethod? {
            val candidates = valuesByName[method.name] ?: return null
            return candidates
                    .firstOrNull { it.matches(method, services.superMethodsSearcher) }
                    ?.takeIf { it.parameterCount == null || it.parameterCount == argumentCount } // if parameterCount is specified we should make sure that argument count is correct
        }
    }
}

