import Foundation
import Combine
import libxml2

final class ParserMiddleware {
    private let parser: XMLParser
    private var callback: StoreCallback?
    private var xmlnss = [String: String]()
    private var el_stack = [XMLElement]()

    init() {
        parser = XMLParser()
        parser.delegate = self
    }

    func setCallback(_ callback: @escaping StoreCallback) {
        self.callback = callback
    }

    func middleware(state: ClientState, action: ClientAction) -> AnyPublisher<ClientAction, Never> {
        switch action {
        case .connectionAction(.connectionReceived(let data)):
            return Future<ClientAction, Never> { promise in
                Task {
                    do {
                        try self.parser.parse(data: data)
                        promise(.success(.empty))
                    } catch {
                        promise(.success(.clientError(error as? ClientError ?? .generalError)))
                    }
                }
            }
                .eraseToAnyPublisher()

        case .parserAction(.restartParser):
            el_stack = []
            parser.restart()
            return Empty().eraseToAnyPublisher()

        default:
            return Empty().eraseToAnyPublisher()
        }
    }
}

extension ParserMiddleware: XMLParserDelegate {
    func startElement(name elementName: String, prefix: String?, namespaces: [String: String]?, attributes: [String: String]) {
        // for session start
        if elementName == "stream" && prefix == "stream" {
            var attrs = attributes
            if let namespaces {
                for (key, value) in namespaces {
                    attrs[key] = value
                }
            }
            callback?(.streamAction(.streamOpened(attrs)))
            return
        }

        // for others elements
        if let namespaces {
            for (key, value) in namespaces where !key.isEmpty {
                xmlnss[key] = value
            }
        }

        var xmlns: String?
        if let prefix {
            xmlns = xmlnss[prefix] ?? namespaces?[""]
        } else {
            xmlns = namespaces?[""]
        }

        var name: String
        if let prefix, xmlns == nil {
            name = prefix + ":" + elementName
        } else {
            name = elementName
        }

        var element = XMLElement(name: name, xmlns: xmlns, attributes: attributes, nodes: [])
        if !el_stack.isEmpty {
            element.updateXmlns(el_stack.last?.xmlns)
        }
        if xmlns != nil {
            element.updateXmlns(xmlns)
        }

        el_stack.append(element)
    }

    func endElement(name elementName: String, prefix: String?) {
        if elementName == "stream" && prefix == "stream" {
            callback?(.streamAction(.streamClosed))
            return
        }
        if let element = el_stack.popLast() {
            if el_stack.isEmpty {
                callback?(.parserAction(.elementParsed(element)))
            } else {
                el_stack[el_stack.count - 1].addNode(element)
            }
        }
    }

    func charactersFound(_ value: String) {
        guard !el_stack.isEmpty else {
            return
        }
        el_stack[el_stack.count - 1].updateContent(value)
    }
}
