package org.jetbrains.skia.impl

import kotlinx.cinterop.*
import org.jetbrains.skia.ExternalSymbolName
import kotlin.native.internal.NativePtr

actual abstract class Native actual constructor(ptr: NativePointer) {
    internal actual var _ptr: NativePointer

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (null == other) return false
        if (other !is Native) return false
        return if (_ptr == other._ptr) true else nativeEquals(other)
    }

    override fun hashCode(): Int {
        return _ptr.toLong().hashCode()
    }

    internal actual open fun nativeEquals(other: Native?): Boolean {
        return false
    }

    actual companion object {
        init {
            initCallbacks(
                staticCFunction(::callBooleanCallback),
                staticCFunction(::callIntCallback),
                staticCFunction(::callNativePtrCallback),
                staticCFunction(::callVoidCallback),
                staticCFunction(::disposeCallback),
            )
        }

        actual val NullPointer: NativePointer
            get() = NativePtr.NULL
    }

    actual override fun toString(): String {
        return this::class.simpleName + "(_ptr=0x" + _ptr.toString() + ")"
    }

    init {
        if (ptr == NativePtr.NULL) throw RuntimeException("Can't wrap nullptr")
        _ptr = ptr
    }
}

actual typealias NativePointer = NativePtr
actual typealias InteropPointer = NativePtr

internal actual fun reachabilityBarrier(obj: Any?) {
    // TODO: implement native barrier
}

internal actual inline fun <T> interopScope(block: InteropScope.() -> T): T {
    val scope = InteropScope()
    try {
        return scope.block()
    } finally {
        scope.release()
    }
}

internal actual class InteropScope actual constructor() {
    actual fun toInterop(string: String?): InteropPointer {
        return if (string != null) {
            val pinned = convertToZeroTerminatedString(string).pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun toInterop(array: ByteArray?): InteropPointer {
        return if (array != null && array.isNotEmpty()) {
            val pinned = array.pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun toInteropForResult(array: ByteArray?): InteropPointer = toInterop(array)

    actual fun InteropPointer.fromInterop(result: ByteArray) {}

    actual fun toInterop(array: ShortArray?): InteropPointer {
        return if (array != null && array.isNotEmpty()) {
            val pinned = array.pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun toInteropForResult(array: ShortArray?): InteropPointer = toInterop(array)

    actual fun InteropPointer.fromInterop(result: ShortArray) {}

    actual fun toInterop(array: IntArray?): InteropPointer {
        return if (array != null && array.isNotEmpty()) {
            val pinned = array.pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun toInteropForResult(array: IntArray?): InteropPointer = toInterop(array)

    actual fun InteropPointer.fromInterop(result: IntArray) {}

    actual fun toInterop(array: LongArray?): InteropPointer {
        return if (array != null && array.isNotEmpty()) {
            val pinned = array.pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun InteropPointer.fromInterop(result: LongArray) {}

    actual fun toInterop(array: FloatArray?): InteropPointer {
        return if (array != null && array.isNotEmpty()) {
            val pinned = array.pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun toInteropForResult(array: FloatArray?): InteropPointer = toInterop(array)

    actual fun InteropPointer.fromInterop(result: FloatArray) {}

    actual fun toInterop(array: DoubleArray?): InteropPointer {
        return if (array != null && array.isNotEmpty()) {
            val pinned = array.pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun toInteropForResult(array: DoubleArray?): InteropPointer = toInterop(array)

    actual fun InteropPointer.fromInterop(result: DoubleArray) {}

    actual fun toInterop(array: NativePointerArray?): InteropPointer {
        return if (array != null && array.size > 0) {
            // We pass it as LongArray via boundary.
            val pinned = array.backing.pin()
            elements.add(pinned)
            val result = pinned.addressOf(0).rawValue
            result
        } else {
            NativePtr.NULL
        }
    }

    actual fun toInteropForResult(array: NativePointerArray?): InteropPointer = toInterop(array)

    actual fun InteropPointer.fromInterop(result: NativePointerArray) {}

    actual fun toInterop(stringArray: Array<String>?): InteropPointer {
        if (stringArray == null || stringArray.isEmpty()) return NativePtr.NULL

        val pins = stringArray.toList()
            .map { convertToZeroTerminatedString(it).pin() }

        val nativePointerArray = NativePointerArray(stringArray.size)
        pins.forEachIndexed { index, pin ->
            elements.add(pin)
            nativePointerArray[index] = pin.addressOf(0).rawValue
        }
        return toInterop(nativePointerArray)
    }

    actual inline fun <reified T> InteropPointer.fromInterop(decoder: ArrayInteropDecoder<T>): Array<T> {
        val size = decoder.getArraySize(this)
        val result = Array<T>(size) {
            decoder.getArrayElement(this, it)
        }
        decoder.disposeArray(this)
        return result
    }

    actual fun InteropPointer.fromInteropNativePointerArray(): NativePointerArray {
        TODO("implement native fromInteropNativePointerArray")
    }

    actual fun toInteropForArraysOfPointers(interopPointers: Array<InteropPointer>): InteropPointer {
        return toInterop(interopPointers.map { it.toLong() }.toLongArray())
    }

    actual fun callback(callback: (() -> Unit)?) = callbackImpl(callback)
    actual fun intCallback(callback: (() -> Int)?) = callbackImpl(callback)
    actual fun nativePointerCallback(callback: (() -> NativePointer)?) = callbackImpl(callback)
    actual fun interopPointerCallback(callback: (() -> InteropPointer)?) = callbackImpl(callback)
    actual fun booleanCallback(callback: (() -> Boolean)?) = callbackImpl(callback)

    actual fun virtual(method: () -> Unit) = callbackImpl(method)
    actual fun virtualInt(method: () -> Int) = callbackImpl(method)
    actual fun virtualNativePointer(method: () -> NativePointer) = callbackImpl(method)
    actual fun virtualInteropPointer(method: () -> InteropPointer) = callbackImpl(method)
    actual fun virtualBoolean(method: () -> Boolean) = callbackImpl(method)

    actual fun release()  {
        elements.forEach {
            it.unpin()
        }
    }

    private fun <T> callbackImpl(callback: (() -> T)?): InteropPointer = callback?.let {
        val ptr = StableRef.create(it).asCPointer()
        NativePtr.NULL.plus(ptr.toLong())
    } ?: NativePtr.NULL

    private val elements = mutableListOf<Pinned<*>>()
}

// Ugly! NativePtrArray in stdlib is unfortunately internal, don't have ctor and cannot be used.
actual class NativePointerArray actual constructor(size: Int) {
    internal val backing = LongArray(size)
    actual operator fun get(index: Int): NativePointer {
        return NativePtr.NULL + backing[index]
    }

    actual operator fun set(index: Int, value: NativePointer) {
        backing[index] = value.toLong()
    }

    actual val size: Int
        get() = backing.size
}

// Callbacks support

private fun callVoidCallback(ptr: COpaquePointer) {
    ptr.asStableRef<() -> Unit>().get().invoke()
}

private fun callBooleanCallback(ptr: COpaquePointer): Boolean {
    return ptr.asStableRef<() -> Boolean>().get().invoke()
}

private fun callIntCallback(ptr: COpaquePointer): Int {
    return ptr.asStableRef<() -> Int>().get().invoke()
}

private fun callNativePtrCallback(ptr: COpaquePointer): Long {
    return ptr.asStableRef<() -> NativePointer>().get().invoke().toLong()
}

private fun disposeCallback(ptr: COpaquePointer) {
    ptr.asStableRef<Any>().dispose()
}

@ExternalSymbolName("skiko_initCallbacks")
private external fun initCallbacks(
    callBoolean: COpaquePointer,
    callInt: COpaquePointer,
    callNativePointer: COpaquePointer,
    callVoid: COpaquePointer,
    dispose: COpaquePointer
)

/**
 * Converts String to zero-terminated utf-8 byte array.
 */
private fun convertToZeroTerminatedString(string: String): ByteArray {
    //  C++ needs char* with zero byte at the end. So we need to copy array with an extra zero byte.

    val utf8 = string.encodeToByteArray() // encodeToByteArray encodes to utf8
    // TODO Remove array copy, use `skString(data, length)` instead of `skString(data)`
    return utf8.copyOf(utf8.size + 1)
}
