package ac.mdiq.podcini.net.feed.parser

import ac.mdiq.podcini.net.feed.parser.utils.getMimeType
import ac.mdiq.podcini.net.feed.parser.utils.isImageFile
import ac.mdiq.podcini.net.feed.parser.utils.isMediaFile
import ac.mdiq.podcini.net.feed.parser.utils.parseDate
import ac.mdiq.podcini.storage.model.Chapter
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.specs.FeedFunding
import ac.mdiq.podcini.utils.Logd
import ac.mdiq.podcini.utils.Logs
import androidx.core.text.HtmlCompat
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.Reader
import java.util.Stack
import java.util.concurrent.TimeUnit
import javax.xml.parsers.ParserConfigurationException
import javax.xml.parsers.SAXParserFactory
import org.apache.commons.io.input.XmlStreamReader
import org.jsoup.Jsoup
import org.xml.sax.Attributes
import org.xml.sax.InputSource
import org.xml.sax.SAXException
import org.xml.sax.helpers.DefaultHandler
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.util.Date

class FeedHandler {
    @Throws(SAXException::class, IOException::class, ParserConfigurationException::class, UnsupportedFeedtypeException::class)
    fun parseFeed(feed: Feed): FeedHandlerResult {
//        val tg = TypeGetter()
        val type = getType(feed)
        val handler = SyndHandler(feed, type)
        if (feed.fileUrl != null) {
            val factory = SAXParserFactory.newInstance()
            factory.isNamespaceAware = true
            val saxParser = factory.newSAXParser()
//            saxParser.parse(File(feed.file_url!!), handler)

            val inputStreamReader: Reader = XmlStreamReader(File(feed.fileUrl!!))
            val inputSource = InputSource(inputStreamReader)
            Logd("FeedHandler", "parseFeed starting saxParser.parse")
            saxParser.parse(inputSource, handler)
            inputStreamReader.close()
        }
        return FeedHandlerResult(handler.state.feed, handler.state.alternateUrls, handler.state.redirectUrl ?: "")
    }

    @Throws(UnsupportedFeedtypeException::class)
    fun getType(feed: Feed): Type {
        if (feed.type != null) return Type.fromName(feed.type!!)

        val factory: XmlPullParserFactory
        if (feed.fileUrl != null) {
            var reader: Reader? = null
            try {
                factory = XmlPullParserFactory.newInstance()
                factory.isNamespaceAware = true
                val xpp = factory.newPullParser()
                reader = createReader(feed)
                xpp.setInput(reader)
                var eventType = xpp.eventType
                while (eventType != XmlPullParser.END_DOCUMENT) {
                    if (eventType == XmlPullParser.START_TAG) {
                        when (val tag = xpp.name) {
                            ATOM_ROOT -> {
                                feed.type = Feed.FeedType.ATOM1.name
                                Logd(TAG, "getType Recognized type Atom")
                                val strLang = xpp.getAttributeValue("http://www.w3.org/XML/1998/namespace", "lang")
                                if (strLang != null) feed.languages.add(strLang)
                                return Type.ATOM
                            }
                            RSS_ROOT -> {
                                val strVersion = xpp.getAttributeValue(null, "version")
                                when (strVersion) {
                                    null -> {
                                        feed.type = Feed.FeedType.RSS.name
                                        Logd(TAG, "getType Assuming type RSS 2.0")
                                        return Type.RSS20
                                    }
                                    "2.0" -> {
                                        feed.type = Feed.FeedType.RSS.name
                                        Logd(TAG, "getType Recognized type RSS 2.0")
                                        return Type.RSS20
                                    }
                                    "0.91", "0.92" -> {
                                        Logd(TAG, "getType Recognized type RSS 0.91/0.92")
                                        return Type.RSS091
                                    }
                                    else -> throw UnsupportedFeedtypeException("Unsupported rss version")
                                }
                            }
                            else -> {
                                Logd(TAG, "getType Type is invalid")
                                throw UnsupportedFeedtypeException(Type.INVALID, tag)
                            }
                        }
                    } else {
                        // Apparently exception happens on some devices...
                        try { eventType = xpp.next() } catch (e: RuntimeException) {
                            Logs(TAG, e, "Unable to get type")
                            throw UnsupportedFeedtypeException("Unable to get type") }
                    }
                }
            } catch (e: XmlPullParserException) {
                Logs(TAG, e, "getType for feed failed")
                // XML document might actually be a HTML document -> try to parse as HTML
                var rootElement: String? = null
                try {
                    Jsoup.parse(File(feed.fileUrl!!))
                    rootElement = "html"
                } catch (e1: IOException) { Logs(TAG, e1, "IOException: " + feed.fileUrl) }
                throw UnsupportedFeedtypeException(Type.INVALID, rootElement)
            } catch (e: IOException) { Logs(TAG, e, "IOException: " + feed.fileUrl)
            } finally {
                if (reader != null) {
                    try { reader.close() } catch (e: IOException) { Logs(TAG, e, "IOException: $reader") }
                }
            }
        }
        Logd(TAG, "getType Type is invalid")
        throw UnsupportedFeedtypeException(Type.INVALID)
    }

    private fun createReader(feed: Feed): Reader? {
        if (feed.fileUrl.isNullOrBlank()) return null
        try {
            val reader: Reader = XmlStreamReader(File(feed.fileUrl!!))
            return reader
        } catch (e: FileNotFoundException) { Logs(TAG, e, "FileNotFoundException: " + feed.fileUrl)
        } catch (e: IOException) { Logs(TAG, e, "IOException: " + feed.fileUrl) }
        return null
    }

    enum class Type {
        RSS20, RSS091, ATOM, YOUTUBE, INVALID;

        companion object {
            fun fromName(name: String): Type {
                for (t in entries) {
                    if (t.name == name) return t
                }
                return INVALID
            }
        }
    }

    /**
     * Contains all relevant information to describe the current state of a SyndHandler.
     * Feed that the Handler is currently processing.
     */
    class HandlerState( var feed: Feed) {
        /**
         * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the
         * URL of the feed, the value is the title
         */
        
        val alternateUrls: MutableMap<String, String> = mutableMapOf()
        
        var redirectUrl: String? = null
        
        val items: MutableList<Episode> = mutableListOf()
        
        var currentItem: Episode? = null
        
        var currentFunding: FeedFunding? = null
        
        val tagstack: Stack<SyndElement> = Stack()
        /**
         * Namespaces that have been defined so far.
         */
        
        val namespaces: MutableMap<String, Namespace> = mutableMapOf()
        
        val defaultNamespaces: Stack<Namespace> = Stack()
        /**
         * Buffer for saving characters.
         */
        
        var contentBuf: StringBuilder? = null
        /**
         * Temporarily saved objects.
         */
        
        val tempObjects: MutableMap<String, Any> = mutableMapOf()
        /**
         * Returns the SyndElement that comes after the top element of the tagstack.
         */
        val secondTag: SyndElement
            get() {
                val top = tagstack.pop()
                val second = tagstack.peek()
                tagstack.push(top)
                return second
            }

        val thirdTag: SyndElement
            get() {
                val top = tagstack.pop()
                val second = tagstack.pop()
                val third = tagstack.peek()
                tagstack.push(second)
                tagstack.push(top)
                return third
            }

        fun addAlternateFeedUrl(title: String, url: String) {
            alternateUrls[url] = title
        }
    }

    /** Superclass for all SAX Handlers which process Syndication formats  */
    class SyndHandler(feed: Feed, type: Type) : DefaultHandler() {
        
        val state: HandlerState = HandlerState(feed)

        init {
            if (type == Type.RSS20 || type == Type.RSS091) state.defaultNamespaces.push(Rss20())
        }

        @Throws(SAXException::class)
        override fun startElement(uri: String, localName: String, qualifiedName: String, attributes: Attributes) {
            state.contentBuf = StringBuilder()
            val handler = getHandlingNamespace(uri, qualifiedName)
            if (handler != null) {
                val element = handler.handleElementStart(localName, state, attributes)
                state.tagstack.push(element)
            }
        }
        @Throws(SAXException::class)
        override fun characters(ch: CharArray, start: Int, length: Int) {
            if (state.tagstack.size >= 2 && state.contentBuf != null) state.contentBuf!!.appendRange(ch, start, start + length)
        }
        @Throws(SAXException::class)
        override fun endElement(uri: String, localName: String, qualifiedName: String) {
            val handler = getHandlingNamespace(uri, qualifiedName)
            if (handler != null) {
                handler.handleElementEnd(localName, state)
                state.tagstack.pop()
            }
            state.contentBuf = null
        }
        @Throws(SAXException::class)
        override fun endPrefixMapping(prefix: String) {
            if (state.defaultNamespaces.size > 1 && prefix == DEFAULT_PREFIX) state.defaultNamespaces.pop()
        }
        @Throws(SAXException::class)
        override fun startPrefixMapping(prefix: String, uri: String) {
            // Find the right namespace
            if (!state.namespaces.containsKey(uri)) {
                when {
                    uri == Atom.NSURI -> {
                        when (prefix) {
                            DEFAULT_PREFIX -> state.defaultNamespaces.push(Atom())
                            Atom.NSTAG -> {
                                state.namespaces[uri] = Atom()
//                                Logd(TAG, "startPrefixMapping Recognized Atom namespace")
                            }
                        }
                    }
                    uri == Content.NSURI && prefix == Content.NSTAG -> {
                        state.namespaces[uri] = Content()
//                        Logd(TAG, "startPrefixMapping Recognized Content namespace")
                    }
                    uri == Itunes.NSURI && prefix == Itunes.NSTAG -> {
                        state.namespaces[uri] = Itunes()
//                        Logd(TAG, "startPrefixMapping Recognized ITunes namespace")
                    }
                    uri == SimpleChapters.NSURI && prefix.matches(SimpleChapters.NSTAG.toRegex()) -> {
                        state.namespaces[uri] = SimpleChapters()
//                        Logd(TAG, "startPrefixMapping Recognized SimpleChapters namespace")
                    }
                    uri == Media.NSURI && prefix == Media.NSTAG -> {
                        state.namespaces[uri] = Media()
//                        Logd(TAG, "startPrefixMapping Recognized media namespace")
                    }
                    uri == DublinCore.NSURI && prefix == DublinCore.NSTAG -> {
                        state.namespaces[uri] = DublinCore()
//                        Logd(TAG, "startPrefixMapping Recognized DublinCore namespace")
                    }
                    uri == PodcastIndex.NSURI || uri == PodcastIndex.NSURI2 && prefix == PodcastIndex.NSTAG -> {
                        state.namespaces[uri] = PodcastIndex()
//                        Logd(TAG, "startPrefixMapping Recognized PodcastIndex namespace")
                    }
                    else -> Logd(TAG, "startPrefixMapping can not handle uri: $uri")
                }
            }
        }
        private fun getHandlingNamespace(uri: String, qualifiedName: String): Namespace? {
            var handler = state.namespaces[uri]
            if (handler == null && !state.defaultNamespaces.empty() && !qualifiedName.contains(":")) handler = state.defaultNamespaces.peek()
            return handler
        }
        @Throws(SAXException::class)
        override fun endDocument() {
            super.endDocument()
            state.feed.episodes.clear()
            state.feed.episodes.addAll(state.items)
        }

        companion object {
            private val TAG: String = SyndHandler::class.simpleName ?: "Anonymous"
            private const val DEFAULT_PREFIX = ""
        }
    }

    class UnsupportedFeedtypeException : Exception {
        val type: Type
        var rootElement: String? = null
            private set
        override var message: String? = null
            get() {
                return when {
                    field != null -> field!!
                    type == Type.INVALID -> "Invalid type"
                    else -> "Type $type not supported"
                }
            }
        constructor(type: Type) : super() {
            this.type = type
        }
        constructor(type: Type, rootElement: String?) {
            this.type = type
            this.rootElement = rootElement
        }
        constructor(message: String?) {
            this.message = message
            type = Type.INVALID
        }

        companion object {
            private const val serialVersionUID = 9105878964928170669L
        }
    }

    class FeedHandlerResult(
        val feed: Feed,
        val alternateFeedUrls: Map<String, String>,
        val redirectUrl: String)

    abstract class Namespace {
        /** Called by a Feedhandler when in startElement and it detects a namespace element
         * @return The SyndElement to push onto the stack */
        abstract fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement

        /** Called by a Feedhandler when in endElement and it detects a namespace element */
        abstract fun handleElementEnd(localName: String, state: HandlerState)

        /** Trims all whitespace from beginning and ending of a String. {[String.trim]} only trims spaces. */
        fun trimAllWhitespace(string: String): String {
            return string.replace("(^\\s*)|(\\s*$)".toRegex(), "")
        }
    }

    /** Defines a XML Element that is pushed on the tagstack  */
    open class SyndElement( val name: String, val namespace: Namespace)

    /** Represents Atom Element which contains text (content, title, summary).  */
    class AtomText(
            name: String,
            namespace: Namespace,
            private val type: String?) : SyndElement(name, namespace) {

        private var content: String? = null

        val processedContent: String?
            /** Processes the content according to the type and returns it.  */
            get() = when (type) {
                null -> content
                TYPE_HTML -> HtmlCompat.fromHtml(content!!, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
                TYPE_XHTML -> content
                // Handle as text by default
                else -> content
            }

        fun setContent(content: String?) {
            this.content = content
        }

        companion object {
            const val TYPE_HTML: String = "html"
            private const val TYPE_XHTML = "xhtml"
        }
    }

    class Atom : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
//        Logd(TAG, "handleElementStart $localName")
            when {
                ENTRY == localName -> {
                    state.currentItem = Episode()
                    state.items.add(state.currentItem!!)
//                state.currentItem!!.feed = state.feed
                }
                localName.matches(isText.toRegex()) -> {
                    val type: String? = attributes.getValue(TEXT_TYPE)
                    return AtomText(localName, this, type)
                }
                LINK == localName -> {
                    val href: String? = attributes.getValue(LINK_HREF)
                    val rel: String? = attributes.getValue(LINK_REL)
                    val parent = state.tagstack.peek()
                    when {
                        parent.name.matches(isFeedItem.toRegex()) -> {
                            when (rel) {
                                null, LINK_REL_ALTERNATE -> if (state.currentItem != null) state.currentItem!!.link = href
                                LINK_REL_ENCLOSURE -> {
                                    val strSize: String? = attributes.getValue(LINK_LENGTH)
                                    var size: Long = 0
                                    try { if (strSize != null) size = strSize.toLong() } catch (e: NumberFormatException) { Logs(TAG, e, "Length attribute could not be parsed.") }
                                    val mimeType: String? = getMimeType(attributes.getValue(LINK_TYPE), href)
                                    val currItem = state.currentItem
                                    if (isMediaFile(mimeType) && currItem != null) currItem.fillMedia(href, size, mimeType)
                                }
                                LINK_REL_PAYMENT -> if (state.currentItem != null) state.currentItem!!.paymentLink = href
                            }
                        }
                        parent.name.matches(isFeed.toRegex()) -> {
                            when (rel) {
                                null, LINK_REL_ALTERNATE -> {
                                    val type: String? = attributes.getValue(LINK_TYPE)
//                                 Use as link if
//                                a) no type-attribute is given and feed-object has no link yet
//                                b) type of link is LINK_TYPE_HTML or LINK_TYPE_XHTML
                                    when {
                                        type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> state.feed.link = href
                                        LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
                                            // treat as podlove alternate feed
                                            var title: String? = attributes.getValue(LINK_TITLE)
                                            if (title.isNullOrEmpty()) title = href?:""
                                            if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
                                        }
                                    }
                                }
                                LINK_REL_ARCHIVES -> {
                                    val type: String? = attributes.getValue(LINK_TYPE)
                                    when (type) {
                                        LINK_TYPE_ATOM, LINK_TYPE_RSS -> {
                                            var title: String? = attributes.getValue(LINK_TITLE)
                                            if (title.isNullOrEmpty()) title = href?:""
                                            if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
                                        }
                                        //A Link such as to a directory such as iTunes
                                        LINK_TYPE_HTML, LINK_TYPE_XHTML -> {}
                                    }
                                }
                                LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, ""))
                                LINK_REL_NEXT -> {
                                    state.feed.isPaged = true
                                    state.feed.nextPageLink = href
                                }
                            }
                        }
                    }
                }
            }
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {
//        Logd(TAG, "handleElementEnd $localName")
            if (ENTRY == localName) {
                if (state.currentItem != null && state.tempObjects.containsKey(Itunes.DURATION)) {
                    val currentItem = state.currentItem
                    if (currentItem != null) {
                        val duration = state.tempObjects[Itunes.DURATION] as Int?
                        if (duration != null) currentItem.duration = (duration)
                    }
                    state.tempObjects.remove(Itunes.DURATION)
                }
                state.currentItem = null
            }

            if (state.tagstack.size >= 2) {
                var textElement: AtomText? = null
                val contentRaw = if (state.contentBuf != null) state.contentBuf.toString() else ""
                val content = trimAllWhitespace(contentRaw)
                val topElement = state.tagstack.peek()
                val top = topElement.name
                val secondElement = state.secondTag
                val second = secondElement.name

                if (top.matches(isText.toRegex())) {
                    textElement = topElement as AtomText
                    textElement.setContent(content)
                }
                when (top) {
                    ID -> {
                        when (second) {
                            FEED -> state.feed.identifier = contentRaw
                            ENTRY if state.currentItem != null -> state.currentItem!!.identifier = contentRaw
                        }
                    }
                    TITLE if textElement != null -> {
                        when (second) {
                            FEED -> state.feed.title = textElement.processedContent
                            ENTRY if state.currentItem != null -> state.currentItem!!.title = textElement.processedContent
                        }
                    }
                    SUBTITLE if FEED == second && textElement != null -> state.feed.description = textElement.processedContent
                    CONTENT if ENTRY == second && textElement != null && state.currentItem != null -> state.currentItem!!.setDescriptionIfLonger(textElement.processedContent)
                    SUMMARY if ENTRY == second && textElement != null && state.currentItem != null -> state.currentItem!!.setDescriptionIfLonger(textElement.processedContent)
                    UPDATED if ENTRY == second && state.currentItem != null && state.currentItem!!.pubDate == 0L -> state.currentItem!!.pubDate = parseOrNullIfFuture(content)?.time ?: 0
                    PUBLISHED if ENTRY == second && state.currentItem != null -> state.currentItem!!.pubDate = parseOrNullIfFuture(content)?.time ?: 0
                    IMAGE_LOGO if state.feed.imageUrl == null -> state.feed.imageUrl = content
                    IMAGE_ICON -> state.feed.imageUrl = content
                    AUTHOR_NAME if AUTHOR == second && state.currentItem == null -> {
                        val currentName = state.feed.author
                        if (currentName == null) state.feed.author = content
                        else state.feed.author = "$currentName, $content"
                    }
                }
            }
        }

        companion object {
            private val TAG: String = Atom::class.simpleName ?: "Anonymous"
            const val NSTAG: String = "atom"
            const val NSURI: String = "http://www.w3.org/2005/Atom"

            private const val FEED = "feed"
            private const val ID = "id"
            private const val TITLE = "title"
            private const val ENTRY = "entry"
            private const val LINK = "link"
            private const val UPDATED = "updated"
            private const val AUTHOR = "author"
            private const val AUTHOR_NAME = "name"
            private const val CONTENT = "content"
            private const val SUMMARY = "summary"
            private const val IMAGE_LOGO = "logo"
            private const val IMAGE_ICON = "icon"
            private const val SUBTITLE = "subtitle"
            private const val PUBLISHED = "published"

            private const val TEXT_TYPE = "type"

            // Link
            private const val LINK_HREF = "href"
            private const val LINK_REL = "rel"
            private const val LINK_TYPE = "type"
            private const val LINK_TITLE = "title"
            private const val LINK_LENGTH = "length"

            // rel-values
            private const val LINK_REL_ALTERNATE = "alternate"
            private const val LINK_REL_ARCHIVES = "archives"
            private const val LINK_REL_ENCLOSURE = "enclosure"
            private const val LINK_REL_PAYMENT = "payment"
            private const val LINK_REL_NEXT = "next"

            // type-values
            private const val LINK_TYPE_ATOM = "application/atom+xml"
            private const val LINK_TYPE_HTML = "text/html"
            private const val LINK_TYPE_XHTML = "application/xml+xhtml"

            private const val LINK_TYPE_RSS = "application/rss+xml"

            /**
             * Regexp to test whether an Element is a Text Element.
             */
            private const val isText = ("$TITLE|$CONTENT|$SUBTITLE|$SUMMARY")

            private const val isFeed = FEED + "|" + Rss20.CHANNEL
            private const val isFeedItem = ENTRY + "|" + Rss20.ITEM
        }
    }

    class Content : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {
            if (ENCODED == localName && state.contentBuf != null)
                state.currentItem?.setDescriptionIfLonger(state.contentBuf.toString())
        }

        companion object {
            const val NSTAG: String = "content"
            const val NSURI: String = "http://purl.org/rss/1.0/modules/content/"

            private const val ENCODED = "encoded"
        }
    }

    class Itunes : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
            if (IMAGE == localName) {
                val url: String? = attributes.getValue(IMAGE_HREF)
                if (state.currentItem != null) state.currentItem!!.imageUrl = url
                // this is the feed image
                // prefer to all other images
                else if (!url.isNullOrEmpty()) state.feed.imageUrl = url
            }
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {
            if (state.contentBuf == null) return
            val content = state.contentBuf.toString()
            if (content.isEmpty()) return

            when (localName) {
                AUTHOR if state.tagstack.size <= 3 -> state.feed.author = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
                DURATION -> try { state.tempObjects[DURATION] = inMillis(content).toInt() } catch (e: NumberFormatException) { Logs(NSTAG, e, String.format("Duration '%s' could not be parsed", content)) }
                SUBTITLE -> {
                    when {
                        state.currentItem != null && state.currentItem?.description.isNullOrEmpty() -> state.currentItem!!.setDescriptionIfLonger(content)
                        state.feed.description.isNullOrEmpty() -> state.feed.description = content
                    }
                }
                SUMMARY -> {
                    when {
                        state.currentItem != null -> state.currentItem!!.setDescriptionIfLonger(content)
                        Rss20.CHANNEL == state.secondTag.name -> state.feed.description = content
                    }
                }
                NEW_FEED_URL if content.trim { it <= ' ' }.startsWith("http") -> state.redirectUrl = content.trim { it <= ' ' }
            }
        }

        @Throws(NumberFormatException::class)
        fun inMillis(durationStr: String): Long {
            if (durationStr.contains("minute") || durationStr.contains("second")) {
                val parts = durationStr.split(",").map { it.trim() }
                var hours = 0L
                var minutes = 0L
                var seconds = 0L
                parts.forEach { part ->
                    val tokens = part.split(" ").filter { it.isNotEmpty() }
                    if (tokens.size >= 2) {
                        val num = tokens[0].toLongOrNull() ?: return@forEach
                        val unit = tokens[1]
                        when {
                            unit.startsWith("hour") -> hours = num
                            unit.startsWith("minute") -> minutes = num
                            unit.startsWith("second") -> seconds = num
                        }
                    }
                }
                if (hours == 0L && minutes == 0L && seconds == 0L) throw NumberFormatException()
                return toMillis(hours, minutes, seconds)
            } else {
                val parts = durationStr.trim { it <= ' ' }.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                return when (parts.size) {
                    1 -> toMillis(parts[0])
                    2 -> toMillis("0", parts[0], parts[1])
                    3 -> toMillis(parts[0], parts[1], parts[2])
                    else -> throw NumberFormatException()
                }
            }
        }

        private fun toMillis(hours: Long, minutes: Long, seconds: Long): Long {
            return (TimeUnit.HOURS.toMillis(hours) + TimeUnit.MINUTES.toMillis(minutes) + TimeUnit.SECONDS.toMillis(seconds))
        }

        private fun toMillis(hours: String, minutes: String, seconds: String): Long {
            return (TimeUnit.HOURS.toMillis(hours.toLong()) + TimeUnit.MINUTES.toMillis(minutes.toLong()) + toMillis(seconds))
        }

        private fun toMillis(seconds: String): Long {
            if (seconds.contains(".")) {
                val value = seconds.toFloat()
                val millis = value % 1
                return TimeUnit.SECONDS.toMillis(value.toLong()) + (millis * 1000).toLong()
            } else return TimeUnit.SECONDS.toMillis(seconds.toLong())
        }

        companion object {
            const val NSTAG: String = "itunes"
            const val NSURI: String = "http://www.itunes.com/dtds/podcast-1.0.dtd"

            private const val IMAGE = "image"
            private const val IMAGE_HREF = "href"

            private const val AUTHOR = "author"
            const val DURATION: String = "duration"
            private const val SUBTITLE = "subtitle"
            private const val SUMMARY = "summary"
            private const val NEW_FEED_URL = "new-feed-url"
        }
    }

    /** Processes tags from the http://search.yahoo.com/mrss/ namespace.  */
    class Media : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
//        Logd(TAG, "handleElementStart $localName")
            when (localName) {
                CONTENT -> {
                    val url: String? = attributes.getValue(DOWNLOAD_URL)
                    val defaultStr: String? = attributes.getValue(DEFAULT)
                    val medium: String? = attributes.getValue(MEDIUM)
                    var validTypeMedia = false
                    var validTypeImage = false
                    val isDefault = "true" == defaultStr
                    var mimeType = getMimeType(attributes.getValue(MIME_TYPE), url)

                    when (medium) {
                        MEDIUM_AUDIO -> {
                            validTypeMedia = true
                            mimeType = "audio/*"
                        }
                        MEDIUM_VIDEO -> {
                            validTypeMedia = true
                            mimeType = "video/*"
                        }
                        MEDIUM_IMAGE if (mimeType == null || (!mimeType.startsWith("audio/") && !mimeType.startsWith("video/"))) -> { // Apparently, some publishers explicitly specify the audio file as an image
                            validTypeImage = true
                            mimeType = "image/*"
                        }
                        else -> {
                            when {
                                isMediaFile(mimeType) -> validTypeMedia = true
                                isImageFile(mimeType) -> validTypeImage = true
                            }
                        }
                    }

                    when {
                        state.currentItem != null && isDefault && url != null && validTypeMedia -> {
                            var size: Long = 0
                            val sizeStr: String? = attributes.getValue(SIZE)
                            if (!sizeStr.isNullOrEmpty()) {
                                try { size = sizeStr.toLong() } catch (e: NumberFormatException) { Logs(TAG, e, "Size \"$sizeStr\" could not be parsed.") }
                            }
                            var durationMs = 0
                            val durationStr: String? = attributes.getValue(DURATION)
                            if (!durationStr.isNullOrEmpty()) {
                                try {
                                    val duration = durationStr.toLong()
                                    durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt()
                                } catch (e: NumberFormatException) { Logs(TAG, e, "Duration string $durationStr could not be parsed") }
                            }
                            Logd(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
                            state.currentItem?.fillMedia(url, size, mimeType)
                            if (durationMs > 0) state.currentItem?.duration = ( durationMs)
                        }
                        state.currentItem != null && url != null && validTypeImage -> state.currentItem!!.imageUrl = url
                    }
                }
                IMAGE -> {
                    val url: String? = attributes.getValue(IMAGE_URL)
                    if (url != null) {
                        when {
                            state.currentItem != null -> state.currentItem!!.imageUrl = url
                            else -> if (state.feed.imageUrl == null) state.feed.imageUrl = url
                        }
                    }
                }
                DESCRIPTION -> {
                    val type: String? = attributes.getValue(DESCRIPTION_TYPE)
                    return AtomText(localName, this, type)
                }
            }
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {
//        Logd(TAG, "handleElementEnd $localName")
            if (DESCRIPTION == localName) {
                val content = state.contentBuf.toString()
                state.currentItem?.setDescriptionIfLonger(content)
            }
        }

        companion object {
            private val TAG: String = Media::class.simpleName ?: "Anonymous"

            const val NSTAG: String = "media"
            const val NSURI: String = "http://search.yahoo.com/mrss/"

            private const val CONTENT = "content"
            private const val DOWNLOAD_URL = "url"
            private const val SIZE = "fileSize"
            private const val MIME_TYPE = "type"
            private const val DURATION = "duration"
            private const val DEFAULT = "isDefault"
            private const val MEDIUM = "medium"

            private const val MEDIUM_IMAGE = "image"
            private const val MEDIUM_AUDIO = "audio"
            private const val MEDIUM_VIDEO = "video"

            private const val IMAGE = "thumbnail"
            private const val IMAGE_URL = "url"

            private const val DESCRIPTION = "description"
            private const val DESCRIPTION_TYPE = "type"
        }
    }

    class PodcastIndex : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
            when (localName) {
                FUNDING -> {
                    val href: String? = attributes.getValue(URL)
                    val funding = FeedFunding(href, "")
                    state.currentFunding = funding
                    state.feed.addPayment(state.currentFunding!!)
                }
                CHAPTERS -> {
                    val href: String? = attributes.getValue(URL)
                    if (state.currentItem != null && !href.isNullOrEmpty()) state.currentItem!!.podcastIndexChapterUrl = href
                }
            }
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {
            if (state.contentBuf == null) return
            val content = state.contentBuf.toString()
            if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) state.currentFunding!!.content = content
        }

        companion object {
            const val NSTAG: String = "podcast"
            const val NSURI: String = "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md"
            const val NSURI2: String = "https://podcastindex.org/namespace/1.0"
            private const val URL = "url"
            private const val FUNDING = "funding"
            private const val CHAPTERS = "chapters"
        }
    }

    /**
     * SAX-Parser for reading RSS-
     */
    class Rss20 : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
//        Logd(TAG, "handleElementStart $localName")
            when (localName) {
                ITEM if CHANNEL == state.tagstack.lastElement()?.name -> {
                    state.currentItem = Episode()
                    state.items.add(state.currentItem!!) //                state.currentItem!!.feed = state.feed
                }
                ENCLOSURE if ITEM == state.tagstack.peek()?.name -> {
                    val url: String? = attributes.getValue(ENC_URL)
                    val mimeType: String? = getMimeType(attributes.getValue(ENC_TYPE), url)
                    val validUrl = !url.isNullOrBlank()
                    if (isMediaFile(mimeType) && validUrl) {
                        var size: Long = 0
                        try {
                            size = attributes.getValue(ENC_LEN)?.toLong() ?: 0 // less than 16kb is suspicious, check manually
                            if (size < 16384) size = 0
                        } catch (e: NumberFormatException) { Logs(TAG, e, "Length attribute could not be parsed.") }
                        state.currentItem?.fillMedia(url, size, mimeType)
                    }
                }
            }
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {
//        Logd(TAG, "handleElementEnd $localName")
            when {
                ITEM == localName -> {
                    if (state.currentItem != null) {
                        val currentItem = state.currentItem!!
                        // the title tag is optional in RSS 2.0. The description is used
                        // as a title if the item has no title-tag.
                        if (currentItem.title == null) currentItem.title = currentItem.description

                        if (state.tempObjects.containsKey(Itunes.DURATION)) {
                            val duration = state.tempObjects[Itunes.DURATION] as? Int
                            if (duration != null) currentItem.duration = duration
                            state.tempObjects.remove(Itunes.DURATION)
                        }
                    }
                    state.currentItem = null
                }
                state.tagstack.size >= 2 && state.contentBuf != null -> {
                    val contentRaw = state.contentBuf.toString()
                    val content = trimAllWhitespace(contentRaw)
                    val topElement = state.tagstack.peek()
                    val top = topElement.name
                    val secondElement = state.secondTag
                    val second = secondElement.name
                    var third: String? = null
                    if (state.tagstack.size >= 3) third = state.thirdTag.name

                    when {
                        // some feed creators include an empty or non-standard guid-element in their feed,
                        // which should be ignored
                        GUID == top && ITEM == second -> if (contentRaw.isNotEmpty() && state.currentItem != null) state.currentItem!!.identifier = contentRaw
                        TITLE == top -> {
                            val contentFromHtml = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
                            when (second) {
                                ITEM if state.currentItem != null -> state.currentItem!!.title = contentFromHtml
                                CHANNEL -> state.feed.title = contentFromHtml
                            }
                        }
                        LINK == top -> {
                            when (second) {
                                CHANNEL -> state.feed.link = content
                                ITEM if state.currentItem != null -> state.currentItem!!.link = content
                            }
                        }
                        PUBDATE == top && ITEM == second && state.currentItem != null -> state.currentItem!!.pubDate = parseOrNullIfFuture(content)?.time ?: 0
                        // prefer itunes:image
                        URL == top && IMAGE == second && CHANNEL == third -> if (state.feed.imageUrl == null) state.feed.imageUrl = content
                        DESCR == localName -> {
                            when (second) {
                                CHANNEL -> state.feed.description = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
                                ITEM if state.currentItem != null -> state.currentItem!!.setDescriptionIfLonger(content) // fromHtml here breaks \n when not html
                            }
                        }
                        LANGUAGE == localName -> state.feed.languages.add(content.lowercase())
                    }
                }
            }
        }

        companion object {
            private val TAG: String = Rss20::class.simpleName ?: "Anonymous"

            const val CHANNEL: String = "channel"
            const val ITEM: String = "item"
            private const val GUID = "guid"
            private const val TITLE = "title"
            private const val LINK = "link"
            private const val DESCR = "description"
            private const val PUBDATE = "pubDate"
            private const val ENCLOSURE = "enclosure"
            private const val IMAGE = "image"
            private const val URL = "url"
            private const val LANGUAGE = "language"

            private const val ENC_URL = "url"
            private const val ENC_LEN = "length"
            private const val ENC_TYPE = "type"
        }
    }

    class SimpleChapters : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
            val currentItem = state.currentItem
            if (currentItem != null) {
                when (localName) {
                    CHAPTERS -> currentItem.chapters.clear()
                    CHAPTER if !attributes.getValue(START).isNullOrEmpty() -> { // if the chapter's START is empty, we don't need to do anything
                        try {
                            val start= parseTimeString(attributes.getValue(START))
                            val title: String? = attributes.getValue(TITLE)
                            val link: String? = attributes.getValue(HREF)
                            val imageUrl: String? = attributes.getValue(IMAGE)
                            val chapter = Chapter(start, title, link, imageUrl)
                            currentItem.chapters.add(chapter)
                        } catch (e: NumberFormatException) { Logs(TAG, e, "Unable to read chapter. ${e.message}") }
                    }
                }
            }
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {}

        companion object {
            private val TAG: String = SimpleChapters::class.simpleName ?: "Anonymous"

            const val NSTAG: String = "psc|sc"
            const val NSURI: String = "http://podlove.org/simple-chapters"

            private const val CHAPTERS = "chapters"
            private const val CHAPTER = "chapter"
            private const val START = "start"
            private const val TITLE = "title"
            private const val HREF = "href"
            private const val IMAGE = "image"
        }
    }

    class DublinCore : Namespace() {
        override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
            return SyndElement(localName, this)
        }

        override fun handleElementEnd(localName: String, state: HandlerState) {
            if (state.currentItem != null && state.contentBuf != null && state.tagstack.size >= 2) {
                val currentItem = state.currentItem
                val top = state.tagstack.peek().name
                val second = state.secondTag.name
                if (DATE == top && ITEM == second) {
                    val content = state.contentBuf.toString()
                    currentItem!!.pubDate = parseOrNullIfFuture(content)?.time ?: 0
                }
            }
        }

        companion object {
            const val NSTAG: String = "dc"
            const val NSURI: String = "http://purl.org/dc/elements/1.1/"

            private const val ITEM = "item"
            private const val DATE = "date"
        }
    }

    companion object {
        private val TAG: String = FeedHandler::class.simpleName ?: "Anonymous"
        private const val ATOM_ROOT = "feed"
        private const val RSS_ROOT = "rss"

        /**
         * Parses the date but if the date is in the future, returns null.
         */
        fun parseOrNullIfFuture(input: String?): Date? {
            val date = parseDate(input) ?: return null
            val now = Date()
            if (date.after(now)) return null
            return date
        }

        /**
         * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to
         * milliseconds.
         *
         * @throws java.lang.NumberFormatException if the number segments contain invalid numbers.
         */
        fun parseTimeString(time: String): Long {
            val parts = time.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
            var result: Long = 0
            var idx = 0
            if (parts.size == 3) {
                // string has hours
                result += parts[idx].toInt() * 3600000L
                idx++
            }
            if (parts.size >= 2) {
                result += parts[idx].toInt() * 60000L
                idx++
                result += (parts[idx].toFloat() * 1000L).toLong()
            }
            return result
        }
    }
}
