package theorycrafter

import theorycrafter.utils.data
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
import java.net.InetAddress
import java.net.ServerSocket
import java.net.Socket
import java.nio.channels.FileLock
import kotlin.concurrent.thread

/**
 * Manages the single instance functionality of the application.
 */
object SingleAppInstanceManager {

    /**
     * The magic number written at the beginning of the lock file.
     */
    private const val LOCK_FILE_MAGIC_NUMBER = 0xC0DE

    /**
     * The version of the protocol between the app instances.
     */
    private const val PROTOCOL_VERSION = 1

    /**
     * The command id to bringn the main window to the front.
     */
    private const val COMMAND_BRING_TO_FRONT = 1

    /**
     * The file to lock to indicate an instance of Theorycrafter is running.
     */
    private var lockFile: RandomAccessFile? = null

    /**
     * The lock obtained on the file.
     */
    private var lock: FileLock? = null

    /**
     * The server socket on which we're listening for other instances to connect.
     *
     */
    private var serverSocket: ServerSocket? = null

    /**
     * The function that brings the main window to the front.
     *
     */
    private var bringToFront: (() -> Unit)? = null

    /**
     * Whether this instance has been closed or is being closed.
     */
    @Volatile
    private var closed = false

    /**
     * Checks whether this process is the first instance of the application running.
     * If another instance is already running, it signals that instance to bring itself to the front.
     *
     * @return whether this is the first instance, false if another instance is already running.
     */
    fun ensureFirstAppInstance(applicationName: String): Boolean {
        // Create a lock file
        val file = File(System.getProperty("java.io.tmpdir"), "$applicationName.lock")

        // Try to get an exclusive lock on the file
        val raf = RandomAccessFile(file, "rw")
        lockFile = raf
        lock = raf.channel.tryLock()

        // If lock is null, another instance has the lock
        if (lock == null) {
            try {
                // Read the port number from the lock file
                val port = readPortFromLockFile(lockFile!!)
                lockFile?.close()

                // Signal the running instance to bring itself to the front
                signalExistingInstance(port)

            } catch (e: IOException) {
                e.printStackTrace()
            }
            return false
        }

        thread(isDaemon = true) {
            // Create a server socket on any available port
            serverSocket = ServerSocket(0, 10, InetAddress.getLoopbackAddress())
            val port = serverSocket!!.localPort

            writePortToLockFile(lockFile!!, port)
            listenToIncomingCommands()
        }


        // Add a shutdown hook to release the lock when the app exits
        Runtime.getRuntime().addShutdownHook(thread(start = false) {
            close()
        })

        return true
    }

    /**
     * Sets the callback to bring the main window to the front.
     */
    fun setBringToFrontCallback(bringToFront: () -> Unit) {
        this.bringToFront = bringToFront
    }

    /**
     * Writes the port number to the lock file.
     */
    private fun writePortToLockFile(file: RandomAccessFile, port: Int) {
        file.setLength(0)
        file.writeInt(LOCK_FILE_MAGIC_NUMBER)
        file.writeInt(port)
        file.getFD().sync() // Ensure the data is written to disk
    }

    /**
     * Reads the port number from the lock file.
     */
    private fun readPortFromLockFile(file: RandomAccessFile): Int {
        file.seek(0)
        if (file.readInt() != LOCK_FILE_MAGIC_NUMBER)
            throw IllegalStateException("Corrupt lock file")
        return file.readInt()
    }

    /**
     * Listening for signals from other instances.
     */
    private fun listenToIncomingCommands(){
        while (!closed) {
            try {
                val socket = serverSocket?.accept() ?: break
                socket.use {
                    with(it.getInputStream().data()) {
                        val version = readInt()
                        if (version != PROTOCOL_VERSION) {
                            System.err.println("Received unexpected version number via socket: $version")
                            return@use
                        }
                        when (val command = readInt()) {
                            COMMAND_BRING_TO_FRONT -> bringToFront?.invoke()
                            else -> System.err.println("Received unknown command via socket: $command")
                        }
                    }
                }
            } catch (e: Exception) {
                if (!closed) {
                    e.printStackTrace()
                }
            }
        }
    }

    /**
     * Signals the existing instance to bring itself to the front.
     */
    private fun signalExistingInstance(port: Int) {
        Socket(InetAddress.getLoopbackAddress(), port).use {
            with(it.outputStream.data()) {
                writeInt(PROTOCOL_VERSION)
                writeInt(COMMAND_BRING_TO_FRONT)
                flush()
            }
        }
    }

    /**
     * Releases resources used by the [SingleAppInstanceManager].
     */
    private fun close() {
        if (closed)
            return
        try {
            closed = true
            serverSocket?.close()
            lock?.release()
            lockFile?.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
