package com.vgmlr.wedge
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.ServerSocket
import java.net.Socket
import org.json.JSONObject
import java.net.InetSocketAddress
class PeerManager(
private val getLocalPassKey: () -> String,
private val getLocalLastModified: suspend () -> Long,
private val getAutoMerge: () -> Boolean,
private val onPeerMeta: (Long) -> Unit,
private val onMessageReceived: (String, Long) -> Unit,
private val onLinkReady: () -> Unit,
private val onStatusChanged: (String, Boolean) -> Unit
) {
private var serverSocket: ServerSocket? = null
@Volatile private var clientSocket: Socket? = null
@Volatile private var writer: PrintWriter? = null
@Volatile private var metaGate: CompletableDeferred<Long>? = null
@Volatile var peerAutoMerge: Boolean = false
private set
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@Volatile private var isListening = false
fun isLinkUp(): Boolean {
val s = clientSocket
return writer != null && s != null && s.isConnected && !s.isClosed
}
fun autoMergeReady(): Boolean = isLinkUp() && getAutoMerge() && peerAutoMerge
fun refreshMeta() {
if (!isLinkUp()) return
scope.launch { sendMeta() }
}
fun startListener() {
if (isListening) return
isListening = true
scope.launch {
try {
serverSocket = ServerSocket(35690).apply {
reuseAddress = true
}
while (isActive) {
val socket = serverSocket?.accept() ?: break
if (isLinkUp()) { socket.close(); continue }
attachPeer(socket)
handleClientStream(socket)
}
} catch (_: Exception) {}
}
}
suspend fun dial(host: String, port: Int, timeoutMs: Long = 5000): Boolean {
if (isLinkUp()) return true
return withContext(Dispatchers.IO) {
try {
onStatusChanged("Connecting", false)
val gate = CompletableDeferred<Long>()
metaGate = gate
val socket = Socket()
socket.connect(InetSocketAddress(host, port), timeoutMs.toInt())
attachPeer(socket)
scope.launch { handleClientStream(socket) }
withTimeout(timeoutMs) { gate.await() }
true
} catch (_: Exception) {
onStatusChanged("No Peer", false)
dropPeer()
false
} finally {
metaGate = null
}
}
}
private suspend fun attachPeer(socket: Socket) {
withContext(Dispatchers.IO) {
socket.tcpNoDelay = true
clientSocket = socket
writer = PrintWriter(socket.getOutputStream(), true)
}
onStatusChanged("Connected", true)
sendMeta()
}
private fun dropPeer() {
try { clientSocket?.close() } catch (_: Exception) {}
clientSocket = null
writer = null
peerAutoMerge = false
}
private suspend fun handleClientStream(socket: Socket) {
withContext(Dispatchers.IO) {
try {
val reader = BufferedReader(InputStreamReader(socket.getInputStream(), "UTF-8"))
var line: String? = null
while (isActive && reader.readLine().also { line = it } != null) {
line?.let { base64Payload ->
try {
val decodedBytes = android.util.Base64.decode(base64Payload, android.util.Base64.NO_WRAP)
val jsonString = String(decodedBytes, Charsets.UTF_8)
val json = JSONObject(jsonString)
val receivedKey = json.getString("key")
if (receivedKey != getLocalPassKey()) {
withContext(Dispatchers.Main) {
onStatusChanged("Pass Key Incorrect", false)
}
dropPeer()
return@let
}
val type = json.optString("type", "data")
val peerDate = json.optLong("date", 0L)
peerAutoMerge = json.optBoolean("auto", false)
if (type == "meta") {
withContext(Dispatchers.Main) {
onPeerMeta(peerDate)
onLinkReady()
}
metaGate?.complete(peerDate)
} else {
val cleanMessage = json.getString("data")
withContext(Dispatchers.Main) {
onPeerMeta(peerDate)
onMessageReceived(cleanMessage, peerDate)
}
metaGate?.complete(peerDate)
}
} catch (_: Exception) {
}
}
}
} catch (_: Exception) {
} finally {
withContext(Dispatchers.Main) {
onStatusChanged("Idle", false)
}
dropPeer()
}
}
}
private suspend fun sendMeta() {
try {
val json = JSONObject().apply {
put("type", "meta")
put("key", getLocalPassKey())
put("date", getLocalLastModified())
put("auto", getAutoMerge())
}
val bytes = json.toString().toByteArray(Charsets.UTF_8)
val base64Encoded = android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)
writer?.println(base64Encoded)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun sendTextMessage(message: String, passKey: String, date: Long, onSuccess: () -> Unit) {
val out = writer
if (!isLinkUp() || out == null) {
scope.launch(Dispatchers.Main) { onStatusChanged("Disconnected", false) }
return
}
scope.launch(Dispatchers.IO) {
try {
val json = JSONObject().apply {
put("type", "data")
put("key", passKey)
put("date", date)
put("auto", getAutoMerge())
put("data", message)
}
val bytes = json.toString().toByteArray(Charsets.UTF_8)
val base64Encoded = android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)
out.println(base64Encoded)
if (out.checkError()) {
withContext(Dispatchers.Main) { onStatusChanged("Disconnected", false) }
return@launch
}
withContext(Dispatchers.Main) {
onPeerMeta(date)
onSuccess()
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) { onStatusChanged("Disconnected", false) }
}
}
}
fun stop() {
isListening = false
scope.launch(Dispatchers.IO) {
try {
dropPeer()
serverSocket?.close()
} catch (e: Exception) {
e.printStackTrace()
}
serverSocket = null
}
}
}