package com.agineek.agineek_pad_plugin

import android.app.Activity // not really used though
import android.content.Context // solely used for making NsdConnection.
import android.content.Intent // used for broadcasting found https://developer.android.com/reference/android/content/Intent#developer-guides
import android.net.nsd.NsdManager // not used here
import android.net.nsd.NsdManager.RegistrationListener // not used here
import android.net.nsd.NsdServiceInfo // not used here
import android.os.Handler // not used here
import android.os.Looper // not used here
import android.util.Log // you know what it does found https://developer.android.com/reference/android/util/Log
import androidx.annotation.NonNull // denotes a parameter, field, or method can never be null
import androidx.core.content.ContextCompat.getSystemService // not used here
import io.flutter.embedding.engine.plugins.FlutterPlugin // found https://api.flutter.dev/javadoc/io/flutter/embedding/engine/plugins/FlutterPlugin.html
import io.flutter.plugin.common.EventChannel // found https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.html
import io.flutter.plugin.common.EventChannel.StreamHandler // found https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.StreamHandler.html
import io.flutter.plugin.common.MethodCall // found https://api.flutter.dev/javadoc/io/flutter/plugin/common/MethodCall.html
import io.flutter.plugin.common.MethodChannel // found https://api.flutter.dev/javadoc/io/flutter/plugin/common/MethodChannel.html
import io.flutter.plugin.common.MethodChannel.MethodCallHandler // found https://api.flutter.dev/javadoc/io/flutter/plugin/common/MethodChannel.MethodCallHandler.html
import io.flutter.plugin.common.MethodChannel.Result // found https://api.flutter.dev/javadoc/io/flutter/plugin/common/MethodChannel.Result.html
import io.flutter.plugin.common.PluginRegistry.Registrar // not used
import java.lang.Runtime // not used here
import java.net.ServerSocket // found https://courses.cs.washington.edu/courses/cse341/98au/java/jdk1.2beta4/docs/api/java/net/ServerSocket.html
import java.io.IOException // you know what this is.
import java.util.concurrent.atomic.AtomicBoolean // found https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicBoolean.html
import kotlin.collections.LinkedHashSet // methods found https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-linked-hash-set/
import kotlin.collections.LinkedHashMap // methods found https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-linked-hash-map/

// also used EventChannel.EventSink: https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html
// TODO: BUG: flutter app must be run first, then pad_emulator.py, not the other way around
// TODO: BUG: running start search button twice, does not connect pads (prolly a service name problem?)
// TODO: BUG: after first connection, plugin.pads seems to not change size from 4 after stop search.

/**
 * AgineekPadPlugin
 *
 * @author Joseph R. Quinn
 * @since v1.0.0
 */
@kotlin.ExperimentalStdlibApi
class AgineekPadPlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
    companion object {
        @JvmStatic private val Tag = "AgineekPadPlugin"
        @JvmStatic private val serviceStarted: AtomicBoolean = AtomicBoolean(false)
    }
    // The MethodChannel that will the communication between Flutter and native Android
    //
    // This local reference serves to register the plugin with the Flutter Engine and unregister it
    // when the Flutter Engine is detached from the Activity
    private lateinit var channel: MethodChannel
    private lateinit var eventChannel: EventChannel
    private lateinit var context: Context

    // found https://api.flutter.dev/javadoc/io/flutter/plugin/common/EventChannel.EventSink.html
    public var eventSink: EventChannel.EventSink? = null

    public var nsdConnection: NsdConnection? = null

    public var pads: LinkedHashSet<Pad> = LinkedHashSet<Pad>()
    public var playerColors: LinkedHashMap<Int, String> = LinkedHashMap<Int, String>()

    private var activity: Activity? = null
    private var serverThread: AgineekServerThread? = null
    public var serverSocket: ServerSocket? = null

    // flutterpluginBinding found: https://api.flutter.dev/javadoc/io/flutter/embedding/engine/plugins/FlutterPlugin.FlutterPluginBinding.html
    public override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        Log.d(Tag, "onAttachedToEngine(${flutterPluginBinding}) function called")
        context = flutterPluginBinding.getApplicationContext()

        nsdConnection = NsdConnection(context)

        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "agineek_pad_plugin")
        channel.setMethodCallHandler(this)

        eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "agineek_pad_plugin/pads");
        eventChannel.setStreamHandler(this)
    }

    public override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        Log.d(Tag, "onMethodCall(${call}, ${result}) function called")
        when (call.method) {
            /**
             * Clears the pads HashSet (since we want to look for new pads)
             * and start making new sockets for incoming pad connections.
             */
            "startConnectPads" -> {
                pads.clear()
                eventSink?.success(pads.size) // initially display that no pads are connected.
                Log.d(Tag, "Pads in Hashset: ${pads.size}")
                if (serverThread == null) {
                    serverThread = AgineekServerThread(this)
                    serverThread!!.setName("ServerThread")
                }
                if (serverThread!!.getState() != Thread.State.RUNNABLE) {
                    serverThread!!.start()
                }

                result.success(true)
            }
            /**
             * Stops the serverSocket from accepting more threads and making more sockets for them.
             */
            "stopConnectPads" -> {
                if (serverThread?.getState() == Thread.State.RUNNABLE) {
                    serverThread?.interrupt()
                    Log.d(Tag, serverSocket.toString())
                    serverThread = null
                }
                serverThread?.interrupt()
                if (pads.size == 0) {
                    eventSink?.success(null)
                }
                result.success(false)
            }
            "startGame" -> {
                Log.d(Tag, "Wow, you pressed the start button.")
                /**
                 * Start button should:
                 * 0. if no pads are connected, display error box saying no pads connected result.success(false)
                 * 1. Make many (just do 1) Player objects (requires id and hitColor for testing purposes only) that
                 * will participate in the game
                 * 2. Make a PadSequence object with gameMode and gameType (look at PadSequence and respective classes
                 * for more details also for testing purposes only). Also see if pad's uuid are initialized, they kind
                 * of have to be.
                 * 3. Make a Game object with the PadSequence object.
                 * 3.1 Create Server socket again and accept all broadcast connections. (do we need to doe?)
                 * 4. This Server socket has to keep accepting different pads' messages and stuff (may have to implement a new class)
                 * 5. Start the Game Thread (object itself is a thread).
                 * 6. For every Pad message that is accepted by the serverSocket:
                 * 6.1 Check if it is a pad that is in plugin.pads.
                 * 7. Check if it is a pad hit/miss message. If so, communicate that to the dart code with eventSink stuff.
                 * 8. Use this class's broadcast() function... maybe to do so.
                 * 9. Should add a log to Flutter app. Use Set State and StreamBuilder in main.dart like before.
                 * 10. When the game has timed-out, the Game object should call interrupt and properly end the game.
                 * 11. Check when that happens, then the Flutter app should go back to before a game even started (no logs).
                 */

                // Step 0.1: make sure the different pads have different uuids, only for testing purposes.
                var count = 100
                for (pad in pads) {
                    pad.uuid = count.toString()
                    count += 100
                }

                // Step 1
                var p1: Player = Player("Alice", "blue")
                var players: LinkedHashSet<Player> = LinkedHashSet<Player>()
                players.add(p1)

                // Step 2
                var padSequence: PadSequence = PadSequence(GameMode.Random, GameType.Single, pads, players)

                // Step 2.1: make sure all pads have their sequence variable initialized.
                padSequence.sendSeqToPads(padSequence)

                // Step 3: I think these other parameters are in milliseconds.
                var game: Game = Game(padSequence, 20000, 1000, 1000)

                // let's see what happens.
                // TODO: this causes a FAT error.
                game.start()




                result.success(true) // TODO: not actually a success my dude, this is just a placeholder
            }
            "stopGame" -> {
                result.notImplemented()
            }
            "addPlayer" -> {
                val id = call.argument<Int>("id")!!
                val hitColor = call.argument<String>("hitColor")!!
                playerColors.put(id, hitColor)
                result.success(true)
            }
            "removePlayer" -> {
                val id = call.argument<Int>("id")!!
                playerColors.remove(id)
                result.success(true)
            }
            else -> {
                result.notImplemented()
            }
        }
    }

    public override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        Log.d(Tag, "onDetachedFromEngine(${binding}) function called")
        channel.setMethodCallHandler(null)
        eventSink = null
    }

    public override fun onListen(p0: Any?, events: EventChannel.EventSink) {
        Log.d(Tag, "onListen(${p0}, ${events}) function called")
        eventSink = events
        eventSink!!.success(null)
    }

    public override fun onCancel(p0: Any?) {
        Log.d(Tag, "onCancel(${p0}) function called")
        eventSink = null
    }

    public fun broadcast(pad: Pad, intent: String, uuid: String) {
        Log.d(Tag, "broadcast(${pad}, ${intent}, ${uuid}) function called")
        var i = Intent()
        if (intent.equals("PAD_CONNECTION_CHANGE")) {
            Log.d(Tag, "Broadcasting connection change")
            pads.add(pad)
        } else if (intent.equals("PAD_HIT")) {
            i.putExtra("PAD_HIT_UUID", uuid) // found https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20java.lang.String)
            Log.d(Tag, "Pad ${pad.uuid} hit $pads")
        }

        i.setAction(intent) // found https://developer.android.com/reference/android/content/Intent#setAction(java.lang.String)
    }

    // DEBUG METHOD:
    public fun listPadUUIDs() {
        for (pad in pads) {
            Log.d("Here'saPad", "Pad ${pad.uuid}")
        }
    }
}

@kotlin.ExperimentalStdlibApi
private class AgineekServerThread(
    val plugin: AgineekPadPlugin
): Thread() {
    companion object {
        @JvmStatic private val Tag = "AgineekServerThread"
    }

    private val running: AtomicBoolean = AtomicBoolean(false)

    public fun isRunning(): Boolean {
        return running.get()
    }

    /**
     * This thread just makes a connection for each pad and keeps that connection to each pad alive
     * while a game can be played.
     * Also makes a list of pads from Pad.kt.
     */
    public override fun run() {
        Log.d(Tag, "run() method called")
        running.set(true)
        while(running.get() && !Thread.currentThread().isInterrupted()) {
            try {
                plugin.serverSocket = ServerSocket(0)
                var localPort = plugin.serverSocket!!.getLocalPort()
                plugin.nsdConnection?.registerService(localPort)

                while (!Thread.currentThread().isInterrupted()) {
                    Log.d(Tag, "ServerSocketc created, awaiting connection")
                    var socket = plugin.serverSocket?.accept()
                    Log.d(Tag, "Connection Accepted ${socket!!.getInetAddress()}")
                    var pad = Pad(socket, plugin)
                    pad.connect()
                    plugin.pads.add(pad)
                    Log.d(Tag, "Pad size: ${plugin.pads.size} ")
                    var mainHandlerTwo = Handler(Looper.getMainLooper())
                    var anotherRun = RunEventSink(this)
                    mainHandlerTwo.post(anotherRun)
                }
            } catch (e: IOException) {
                Log.e(Tag, "Error creating ServerSocket: ", e)
                Log.d(Tag, "Caught IOException and about to interupt()")
                val mainHandler = Handler(Looper.getMainLooper())
                var myRunnable = RunInterrupt(this)
                mainHandler.post(myRunnable)
                Log.d(Tag, "Caught IOException and after interrupt()")
            } catch (e: Exception) {
                Log.d(Tag, "Caught Exception and about to interupt()")
                Log.e(Tag, "ServerSocket error: ", e)
                val mainHandler = Handler(Looper.getMainLooper())
                var myRunnable = RunInterrupt(this)
                mainHandler.post(myRunnable)
                Log.d(Tag, "Caught Exception and after interrupt()")
            }
        }
    }

    class RunInterrupt(
        val serverThread1: AgineekServerThread
    ): Runnable {
        override fun run() {
            serverThread1.interrupt()
        }
    }

    class RunEventSink(
        val serverThread1: AgineekServerThread
    ): Runnable {
        override fun run() {
            serverThread1.plugin.eventSink?.success(serverThread1.plugin.pads.size)
        }
    }

    public override fun interrupt() {
        Log.d(Tag, "interrupt() function called")
        running.set(false)
        Log.d("Debug-Level2", "after running.set(false)")
        super.interrupt()
        Log.d("Debug-Level2", "after super.interrupt()")

        // removed loop that would "shutdown" all the pads.

        plugin.eventSink?.success(plugin.pads.size)
        plugin.listPadUUIDs()
        Log.d("Debug-Level2", "after for loop of pads, shutting them down")

        //plugin.pads.clear()

        // nsdConnection.teardown should only call when serverSocket.accept() breaks or when Stop Search is called,
        // exclusively, not both.
        if (!plugin.serverSocket!!.isClosed) {
            plugin.nsdConnection?.tearDown()
        }
        Log.d(Tag, "Closed nsd broadcast")

        // TODO: Do we need to keep the pad connection alive?: what happens when you remove this try catch?
        try {
            plugin.serverSocket?.close()
            Log.d(Tag, "Closed server socket")
        } catch (e: IOException) {
            Log.e(Tag, "Error closing server socket: ", e)
        }
    }
}
