// val lambdaName: (inputType) -> outputType = { inputVariable: inputType -> body }

// example:
val square: (Int) -> Int = { number: Int -> number * number }

// equivalent to:
fun square2(number: Int): Int {
    return number * number
}

// lambda function can be assigned to a variable
val square3 = { number: Int -> number * number }

// can be passed as a parameter to another function - calculate is a higher order function
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
    return operation(x, y)
}

// can be returned from another function
fun operation(): (Int) -> Int {
    return square
}

// common higher order functions
// forEach - applies the lambda function to each element of the collection
fun forEachExample() {
    val numbers = arrayListOf(1, 2, 3, 4, 5)
    numbers.forEach { number: Int -> println(number) }
    // if a lambda has a single parameter, the parameter can be omitted and the keyword it can be
    // used instead
    // numbers.forEach { println(it) }

}

// map - applies the lambda function to each element of the collection and returns a new collection
fun mapExample() {
    val numbers = arrayListOf(1, 2, 3, 4, 5)
    val squaredNumbers = numbers.map { number: Int -> number * number }
    // using it:
    // val squaredNumbers = numbers.map { it * it }
    println(squaredNumbers)
}

// filter - applies the lambda function to each element of the collection and returns a new
// collection
fun filterExample() {
    val numbers = arrayListOf(1, 2, 3, 4, 5)
    numbers.filter { number: Int -> number % 2 == 0 }.forEach { println(it) }
}

// sortedBy - applies the lambda function to each element of the collection and returns a new
// collection
fun sortedByExample() {
    val names = arrayListOf("John", "Mary", "Mike", "Anne", "Peter")
    val sortedNumbers = names.sortedBy { it.length }
    println(sortedNumbers)
}

// maxBy - applies the lambda function to each element of the collection and returns the element
// for which the lambda function returns the largest value
fun maxByExample() {
    val names = arrayListOf("John", "Mary", "Mike", "Anne", "Peter")
    val longestName = names.maxBy { it.length } // returns the element with the longest name
    println(longestName)
}

// minBy - applies the lambda function to each element of the collection and returns the element
// for which the lambda function returns the smallest value
fun minByExample() {
    val names = arrayListOf("John", "Mary", "Mike", "Anne", "Peter")
    val shortestName = names.minBy { it.length } // returns the element with the shortest name
    println(shortestName)
}

/*
cerate a higher ored funciton that takes a mutable collection fo integers and a lambda function.
It then applies the lambda function to every even element of that collection
cerate a labda function that when applied to an integer it returns that integer divided by 10
all the HOF using the lambda and display the results
*/
fun update(collection: ArrayList<Int>, lambda: (Int) -> Int): ArrayList<Int> {
    for (i in 0 until collection.size - 1) {
        if (collection[i] % 2 == 0) {
            collection[i] = lambda(collection[i])
        }
    }
    return collection
}

// create a higher order function that takes a collection of strings and a lambda function.
fun getMessages(clients: Collection<String>, getMessage: (String) -> String): ArrayList<String> {
    val messages = ArrayList<String>()
    for (client in clients) {
        messages.add(getMessage(client))
    }
    return messages
}

// given a set of random integers, print out a subset that consists of only double digit numbers
fun doubleDigit(numbers: Set<Int>): Set<Int> {
    return numbers.filter { it in 10..99 }.toSet()
}

// given a list of Strings, sort by the last char
fun sortLast(strings: List<String>): List<String> {
    return strings.sortedBy { it[it.length - 1] } // -1 because the index starts at 0
}

// given a list of 3-digit integers, print ouf the ones that have the biggest middle digit
fun biggestMiddle(numbers: List<Int>): Int {
    return numbers.filter { it in 100..999 }.maxBy { (it / 10) % 10 }
    // /10%10 gives us the middle integer of each number
}

/*
 * perform map and filter operations on the following collections of numbers
 *
 * if a number is odd, double it
 * if a number is even, half it
 *
 * @param numbers: a collection of integers
 * @return a subset of the collection that has numbers greater than 25
 */
fun mapFilter(numbers: Set<Int>): Set<Int> {
    return numbers.map { if (it % 2 == 0) it / 2 else it * 2 }.filter { it > 25 }.toSet()
}

fun main() {
    // val numbers: ArrayList<Int> = arrayListOf(1, 2, 33, 4, 54, 66, 37, 48, 9)
    // println(numbers)
    // val updatedNumbers = update(numbers) { number: Int -> number / 10 }
    // println(updatedNumbers)

    // val clients = arrayListOf("John", "Mary", "Mike", "Anne", "Peter")
    // val messages =
    //         getMessages(clients) { client: String ->
    //             "Hello $client" // lambda function body
    //         }
    // println(clients)
    // println(messages)

    // for (i in 0 until clients.size) {
    //     println("${clients[i]}: ${messages[i]}")
    // }

    // forEachExample()
    // mapExample()
    // filterExample()

    // val numbers2 = setOf(3, 456, 67, 4, 567, 34, 6789)
    // println(doubleDigit(numbers2))

    // val strings = listOf("John", "Mary", "Mike", "Anne", "Peter")
    // println(sortLast(strings))

    // val ints = listOf(345, 654, 234, 868, 546, 987)
    // println(biggestMiddle(ints))

    // val numbers3 = setOf(34, 76, 234, 435, 675, 12, 54, 87, 98, 65)
    // println(mapFilter(numbers3))

    println(calculateCircleArea(30))
}
