package com.jihe.punchnet.sdlan.network

import com.google.protobuf.kotlin.toByteString
import com.jihe.punchnet.protobuf.PunchProto.SDLData
import com.jihe.punchnet.sdlan.config.EtherType
import com.jihe.punchnet.sdlan.config.SDLanConfig
import com.jihe.punchnet.sdlan.config.toIPV4String
import com.jihe.punchnet.sdlan.logs.TerminalLogger
import com.jihe.punchnet.sdlan.utils.ArpHdr
import com.jihe.punchnet.sdlan.utils.EthHdr
import com.jihe.punchnet.sdlan.utils.ipToString
import com.jihe.punchnet.sdlan.utils.isMultiBroadcast
import com.jihe.punchnet.sdlan.utils.macToString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.InputStreamReader
import java.net.Inet4Address
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.zip.CRC32

interface Iface {
    suspend fun doInit()
    suspend fun recv(): ByteArray
    suspend fun send(content: ByteArray)
    suspend fun reload_config(config: DeviceConfig)

    fun close() {}
    suspend fun handleDataFromNet(node: Node, data: ByteArray)
    suspend fun handleDataFromDevice(node: Node, data: ByteArray)
}

interface IfaceTap: Iface {
    override suspend fun handleDataFromNet(node: Node, data: ByteArray) {
        TerminalLogger.debugf { "in tap mode" }
    }

    override suspend fun handleDataFromDevice(node: Node, data: ByteArray) {
    }
}

interface IfaceTun: Iface {
    /*
    override suspend fun recv(): ByteArray
    override suspend fun send(content: ByteArray)
    override suspend fun reload_config(config: DeviceConfig)
     */
    val arpTable: ARPTable
    val arpWaitList: ARPWaitList

    override suspend fun doInit() {
        arpTable.agingARP()
    }

    override suspend fun handleDataFromNet(node: Node, data: ByteArray) {
        TerminalLogger.debugf { "in tun mode" }
        var size = data.size
        if (size < 14) {
            TerminalLogger.errorf { "packet size error: ${data.size}" }
        }
        val buff = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN)
        val dstmac = ByteArray(6)
        val srcmac = ByteArray(6)
        buff.get(dstmac)
        buff.get(srcmac)
        val k = dstmac.toByteString()
        val etherType = buff.getShort()

        if (k != node.mac && !isMultiBroadcast(k)) {
            TerminalLogger.errorf { "packet to ${macToString(k)} is not direct to us"}
            return
        }

        if (etherType == EtherType.ARP) {
            if (size < 42) {
                TerminalLogger.errorf { "packet size error: invalid arp length" }
                return
            }
            val arp = ArpHdr.parseFrom(EthHdr(dstmac, srcmac, etherType), buff)
            when (arp.opcode) {
                EtherType.ARP_REQUEST -> {
                    TerminalLogger.debugf { "got arp request: dstmac=${macToString(dstmac.toByteString())}" }

                    if (!dstmac.contentEquals(SDLanConfig.BROADCASTMAC_BYTEARRAY)) {
                        TerminalLogger.errorf { "arp request should be broadcast" }
                    }
                    if (arp.dipaddr == node.deviceConfig.ip.netAddr) {
                        val macBytes = node.mac.toByteArray()
                        arpTable.addToARPTable(arp.sipaddr, arp.shwaddr)
                        arp.opcode = EtherType.ARP_REPLY
                        arp.dhwaddr = arp.shwaddr
                        arp.shwaddr = macBytes
                        arp.ethhdr.src = macBytes
                        arp.ethhdr.dest = arp.dhwaddr

                        arp.dipaddr = arp.sipaddr
                        arp.sipaddr = node.deviceConfig.ip.netAddr

                        val bytes = arp.marshalToBytes()
                        val encrypted = node.aes.encrypt(bytes)
                        if (encrypted != null) {
                            val dstmac = arp.dhwaddr.toByteString()
                            val data = SDLData.newBuilder()
                                .setIsP2P(true)
                                .setTtl(2)
                                .setNetworkId(node.networkID.get())
                                .setSrcMac(node.mac)
                                .setDstMac(dstmac)
                                .setData(encrypted.toByteString())
                                .build()

                            TerminalLogger.debugf {
                                "send arp reply to ${macToString(dstmac)}"
                            }
                            val content = encodeToUDPMessage(data, PacketType.Data)
                            sendPacketToNet(node, dstmac, content, 0)
                        }
                    }
                }
                EtherType.ARP_REPLY -> {
                    TerminalLogger.debugf { "got arp shwaddr: ${macToString(arp.shwaddr.toByteString())}" }
                    TerminalLogger.debugf { "got arp dhwaddr: ${macToString(arp.dhwaddr.toByteString())}" }

                    TerminalLogger.debugf { "got arp sipaddr: ${ipToString(arp.sipaddr)}" }
                    TerminalLogger.debugf { "got arp dipaddr: ${ipToString(arp.dipaddr)}" }

                    println("self_ip = ${ipToString(node.deviceConfig.ip.netAddr)}")
                    if (arp.dipaddr == node.deviceConfig.ip.netAddr) {
                        TerminalLogger.debugf {"arp reply"}
                        arpTable.addToARPTable(arp.sipaddr, arp.shwaddr)
                        arpWaitList.arpArrived(node, arp.sipaddr, arp.shwaddr.toByteString())
                    }
                }
                else -> {
                    TerminalLogger.errorf { "unknown ARP type" }
                }
            }
        } else {
            if (data.size < 34) {
                TerminalLogger.errorf { "packet size error: ${data.size}" }
                return
            }

            val rest = data.sliceArray(14..data.size-1)
            val buffer = ByteBuffer.wrap(rest, 12, 8)
            val srcip = buffer.getInt()
            if (!isMultiBroadcast(srcmac.toByteString())) {
                arpTable.addToARPTable(srcip, srcmac)
            }

            send(rest)
        }
    }

    override suspend fun handleDataFromDevice(node: Node, data: ByteArray) {
        if (data.size < 20) {
            TerminalLogger.errorf { "too short ip packet" }
            return
        }
        if (!node.aes.isAuthorized()) {
            TerminalLogger.infof { "dropping tun packet due to not authed" }
            return
        }
        val buffer = ByteBuffer.wrap(data, 12, 8).order(ByteOrder.BIG_ENDIAN)
        val srcip = buffer.getInt()
        val dstip = buffer.getInt()

        TerminalLogger.debugf { "got ${data.size} bytes from tun" }

        if (!node.config.allowRouting && (srcip != node.deviceConfig.ip.netAddr)) {
            TerminalLogger.infof { "dropping routed packet from tun" }
            return
        }

        val (arpinfo, gwip) = arpTable.getMacFromIP(dstip)
        if (arpinfo == null) {
            println("arp info is null")
            // arp not found
            arpWaitList.addToWaitList(gwip, data)
            TerminalLogger.debugf { "added to wait list" }
            sendArpRequest(node, gwip)
            TerminalLogger.debugf { "sent arp request" }
        } else {
            println("mac is ${macToString(arpinfo.toByteString())}")
            val buffer = arpinfo + node.mac.toByteArray() +
                    byteArrayOf((EtherType.IPV4.toInt() shr 8).toByte(), EtherType.IPV4.toByte()) +
                    data

            val crc = CRC32()
            crc.update(buffer)
            val cksum = ByteBuffer.allocate(4)
                .putInt(crc.value.toInt())
                .array()
            val packet = buffer + cksum
            val size = packet.size
            val encrypted = node.aes.encrypt(packet)
            if (encrypted != null) {
                val mac = arpinfo.toByteString()
                val data = SDLData.newBuilder()
                    .setIsP2P(true)
                    .setNetworkId(node.networkID.get())
                    .setTtl(2)
                    .setSrcMac(node.mac)
                    .setDstMac(mac)
                    .setData(encrypted.toByteString())
                    .build()
                val msg = encodeToUDPMessage(data, PacketType.Data)
                sendPacketToNet(node, mac, msg, size.toLong())
            }
        }
    }
}

class IfaceMock: IfaceTun {
    val deviceName = "dev0"
    override val arpTable = ARPTable(CoroutineScope(Dispatchers.IO))
    override val arpWaitList = ARPWaitList()

    val sock = Socket(Inet4Address.getByName("127.0.0.1"), 1234)

    val input: DataInputStream = DataInputStream(sock.getInputStream())
    val output: DataOutputStream = DataOutputStream(sock.getOutputStream())

    var config: DeviceConfig = DeviceConfig(0)

    override fun close() {
    }

    override suspend fun recv(): ByteArray {
        val result = withContext(Dispatchers.IO) {
            val size = input.readInt()
            var result = ByteArray(size)
            input.read(result)
            result
        }
        return result
    }

    override suspend fun send(content: ByteArray) {

        withContext(Dispatchers.IO) {
            val size = content.size
            TerminalLogger.debugf {"sending ${content.size} bytes to tun"}
            output.writeInt(size)
            output.write(content)
        }
    }

    override suspend fun reload_config(config: DeviceConfig) {
        this.config = config
        val ip = ipInt2ByteArray(config.ip.netAddr).toIPV4String()
        var command = "ifconfig ${deviceName} $ip"
        command += " netmask ${ipInt2ByteArray(netmaskBit2Int(config.ip.netBitLen)).toIPV4String()}"
        command += " mtu 1400"
        command += " up"

        TerminalLogger.debugf { "executing command: ${command}" }
        runCommand(command)
    }
}

fun String.execute(): Process {
    val runtime = Runtime.getRuntime()
    return runtime.exec(this)
}

fun Process.text(): String {
    val inputStream = this.inputStream
    val insReader = InputStreamReader(inputStream)
    val bufReader = BufferedReader(insReader)

    var output = ""
    var line: String? = ""
    while(line != null) {
        line = bufReader.readLine()
        output += line + "\n"
    }
    return output
}

fun runCommand(command: String): String {
    val process = command.execute()
    val exitCode = process.waitFor()
    val text = process.text()
    println("exit code: $exitCode")
    return text
}

fun netmaskBit2Int(len: Byte): Int {
    var res = 0
    for (i in 1..len) {
        res = res or (1 shl (32 - i))
    }
    return res
}

/*
fun macToString(mac: Mac): String {
    var result = mutableListOf<String>()
    for (m in mac) {
        result.add(m.toString(16))
    }
    return result.joinToString(":")
}
 */