/*
 * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
 * that can be found in the LICENSE file.
 */
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)

import kotlinx.cinterop.*
import sockets.*

fun main() {
    memScoped {

        val bufferLength = 100L
        val buffer = allocArray<ByteVar>(bufferLength)
        val serverAddr = alloc<sockaddr_in>()

        val listenFd = socket(AF_INET, SOCK_STREAM, 0)
                .toInt().ensureUnixCallResult { it >= 0 }

        with(serverAddr) {
            memset(this.ptr, 0, sockaddr_in.size.convert())
            sin_family = AF_INET.convert()
            sin_addr.s_addr = htons(0u).convert()
            sin_port = htons(0u)
        }

        bind(listenFd, serverAddr.ptr.reinterpret(), sockaddr_in.size.toUInt())
                .toInt().ensureUnixCallResult { it == 0 }

        listen(listenFd, 10)
                .toInt().ensureUnixCallResult { it == 0 }

        val actualAddr = alloc<sockaddr_in>()
        val actualAddrLen = alloc<UIntVar>()
        val port = with(actualAddr) {
            actualAddrLen.value = sockaddr_in.size.convert()
            memset(this.ptr, 0, sockaddr_in.size.convert())
            getsockname(listenFd, actualAddr.ptr.reinterpret(), actualAddrLen.ptr)
                .toInt().ensureUnixCallResult { it == 0 }
            interop_ntohs(sin_port.toShort()).toUShort()
        }
        println(port)

        val commFd = accept(listenFd, null, null)
                .toInt().ensureUnixCallResult { it >= 0 }

        while (true) {
            val length = read(commFd, buffer, bufferLength.convert())
                    .toInt().ensureUnixCallResult { it >= 0 }

            if (length == 0) {
                break
            }

            write(commFd, buffer, length.convert())
                    .toInt().ensureUnixCallResult { it >= 0 }
        }
    }
}

// Not available through interop because declared as macro:
fun htons(value: UShort) = ((value.toInt() ushr 8) or (value.toInt() shl 8)).toUShort()

fun throwUnixError(): Nothing {
    perror(null) // TODO: store error message to exception instead.
    throw Error("UNIX call failed")
}

inline fun Int.ensureUnixCallResult(predicate: (Int) -> Boolean): Int {
    if (!predicate(this)) {
        throwUnixError()
    }
    return this
}
