package com.fabledsword.minstrel.player.output.upnp.sonos

import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory

/**
 * One Sonos zone group as exposed by ZoneGroupTopology.GetZoneGroupState.
 * Stereo pairs and multi-speaker groups all appear as one group with
 * multiple members; one member is the coordinator we send SOAP to.
 */
data class SonosZoneGroup(
    val coordinatorUdn: String,
    val name: String,
    val members: List<SonosZoneMember>,
)

data class SonosZoneMember(
    val udn: String,
    val roomName: String,
    val location: String?,
    val channelMapSet: String?,
)

object SonosTopology {

    fun parse(xml: String): List<SonosZoneGroup> {
        val effective = if (xml.contains("&lt;ZoneGroup")) {
            unescapeXmlEntities(xml)
        } else {
            xml
        }
        return runCatching { parseStrict(effective) }.getOrDefault(emptyList())
    }

    private fun unescapeXmlEntities(s: String): String = s
        .replace("&lt;", "<")
        .replace("&gt;", ">")
        .replace("&quot;", "\"")
        .replace("&apos;", "'")
        .replace("&amp;", "&")  // must be last to avoid double-decoding

    private fun parseStrict(xml: String): List<SonosZoneGroup> {
        val parser = XmlPullParserFactory.newInstance().newPullParser().apply {
            setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
            setInput(xml.reader())
        }
        val groups = mutableListOf<SonosZoneGroup>()
        var currentCoordinator: String? = null
        var currentMembers: MutableList<SonosZoneMember>? = null
        while (parser.eventType != XmlPullParser.END_DOCUMENT) {
            when (parser.eventType) {
                XmlPullParser.START_TAG -> when (parser.name) {
                    TAG_ZONE_GROUP -> {
                        currentCoordinator = parser.getAttributeValue(null, ATTR_COORDINATOR)
                        currentMembers = mutableListOf()
                    }
                    TAG_ZONE_GROUP_MEMBER -> currentMembers?.add(memberOf(parser))
                }
                XmlPullParser.END_TAG -> if (parser.name == TAG_ZONE_GROUP) {
                    val members = currentMembers ?: emptyList()
                    val coord = currentCoordinator
                    if (coord != null && members.isNotEmpty()) {
                        val name = members.firstOrNull { it.udn == coord }?.roomName
                            ?: members.first().roomName
                        groups.add(SonosZoneGroup(coord, name, members))
                    }
                    currentCoordinator = null
                    currentMembers = null
                }
            }
            parser.next()
        }
        return groups
    }

    private fun memberOf(parser: XmlPullParser): SonosZoneMember = SonosZoneMember(
        udn = parser.getAttributeValue(null, ATTR_UUID).orEmpty(),
        roomName = parser.getAttributeValue(null, ATTR_ZONE_NAME).orEmpty(),
        location = parser.getAttributeValue(null, ATTR_LOCATION),
        channelMapSet = parser.getAttributeValue(null, ATTR_CHANNEL_MAP_SET),
    )

    private const val TAG_ZONE_GROUP = "ZoneGroup"
    private const val TAG_ZONE_GROUP_MEMBER = "ZoneGroupMember"
    private const val ATTR_COORDINATOR = "Coordinator"
    private const val ATTR_UUID = "UUID"
    private const val ATTR_ZONE_NAME = "ZoneName"
    private const val ATTR_LOCATION = "Location"
    private const val ATTR_CHANNEL_MAP_SET = "ChannelMapSet"
}
