import Foundation
import libxml2

protocol XMLParserDelegate: AnyObject {
    func startElement(name elementName: String, prefix: String?, namespaces: [String: String]?, attributes: [String: String])
    func endElement(name elementName: String, prefix: String?)
    func charactersFound(_ value: String)
}

final class XMLParser {
    var ctx: xmlParserCtxtPtr?
    var delegate: XMLParserDelegate?

    init() {
        ctx = xmlCreatePushParserCtxt(&saxHandler, Mem.bridge(self), nil, 0, nil)
    }

    deinit {
        xmlFreeParserCtxt(ctx)
        ctx = nil
    }

    func parse(data: Data) throws {
        try data.withUnsafeBytes { (ptr) -> Void in
            if let addr = ptr.baseAddress {
                let err = xmlParseChunk(ctx, addr.assumingMemoryBound(to: CChar.self), Int32(data.count), 0)
                if err > 0 {
                    if err == 64 {
                        if let rng = "<?xml ".data(using: .utf8) {
                            if let position = data.range(of: rng)?.lowerBound {
                                throw ClientError.xmlDeclarationInside(Int(err), position)
                            }
                        }
                    }
                    throw ClientError.xmlUnknown(Int(err))
                }
            }
        }
    }

    func restart() {
        xmlFreeParserCtxt(ctx)
        ctx = xmlCreatePushParserCtxt(&saxHandler, Mem.bridge(self), nil, 0, nil)
    }
}

// =========================================================
// libxml2-specific stuff
private struct Attr {
    var name: UnsafePointer<UInt8>?
    var prefix: UnsafePointer<UInt8>?
    var attrUri: UnsafePointer<UInt8>?
    var valueBegin: UnsafePointer<UInt8>?
    var valueEnd: UnsafePointer<UInt8>?
}

private struct Nss {
    var prefix: UnsafePointer<UInt8>?
    var uri: UnsafePointer<UInt8>?
}

private func strFromCUtf8(_ ptr: UnsafePointer<UInt8>?) -> String? {
    if let ptr {
        return String(cString: ptr)
    }
    return nil
}

private var saxHandler = xmlSAXHandler(
    internalSubset: nil,
    isStandalone: nil,
    hasInternalSubset: nil,
    hasExternalSubset: nil,
    resolveEntity: nil,
    getEntity: nil,
    entityDecl: nil,
    notationDecl: nil,
    attributeDecl: nil,
    elementDecl: nil,
    unparsedEntityDecl: nil,
    setDocumentLocator: nil,
    startDocument: nil,
    endDocument: nil,
    startElement: nil,
    endElement: nil,
    reference: nil,
    characters: SAX_charactersFound,
    ignorableWhitespace: nil,
    processingInstruction: nil,
    comment: nil,
    warning: nil,
    error: nil, // unsafeBitCast(SAX_error, to: errorSAXFunc.self),
    fatalError: nil,
    getParameterEntity: nil,
    cdataBlock: nil,
    externalSubset: nil,
    initialized: XML_SAX2_MAGIC,
    _private: nil,
    startElementNs: SAX_startElement,
    endElementNs: SAX_endElement,
    serror: nil
)

private let SAX_charactersFound: charactersSAXFunc = { ctx_, chars_, len_ in
    guard let ctx_, let chars_ else {
        return
    }
    if let chars = String(data: Data(bytes: UnsafePointer<UInt8>(chars_), count: Int(len_)), encoding: String.Encoding.utf8) {
        let parser = unsafeBitCast(ctx_, to: XMLParser.self)
        parser.delegate?.charactersFound(chars)
    }
}

private let SAX_startElement: startElementNsSAX2Func = { ctx_, localName, prefix_, _, nb_namespaces, namespaces_, nb_attributes, _, attributes_ in
    guard let name = strFromCUtf8(localName), let ctx_ else {
        return
    }
    let prefix = strFromCUtf8(prefix_)
    var attributes: [String: String] = [:]
    var indx = 0
    let parser = unsafeBitCast(ctx_, to: XMLParser.self)

    // attributes
    if let attributes_ {
        attributes_.withMemoryRebound(to: Attr.self, capacity: Int(nb_attributes)) {
            var attrsPtr = $0
            while indx < Int(nb_attributes) {
                if let name = strFromCUtf8(attrsPtr.pointee.name), let beginPtr = attrsPtr.pointee.valueBegin, let endPtr = attrsPtr.pointee.valueEnd {
                    let data = Data(bytes: UnsafePointer<UInt8>(beginPtr), count: endPtr - beginPtr)
                    if var value = String(data: data, encoding: .utf8) {
                        value = XMLEscaping.unescape(value)
                        if let prefix = strFromCUtf8(attrsPtr.pointee.prefix) {
                            attributes[prefix + ":" + name] = value
                        } else {
                            attributes[name] = value
                        }
                    }
                }
                attrsPtr = attrsPtr.successor()
                indx += 1
            }
        }
    }

    // namespaces
    if nb_namespaces > 0, let namespaces_ {
        var namespaces: [String: String] = [:]
        namespaces_.withMemoryRebound(to: Nss.self, capacity: Int(nb_namespaces)) {
            var nsPtr = $0
            indx = 0
            while indx < Int(nb_namespaces) {
                let prefix = strFromCUtf8(nsPtr.pointee.prefix) ?? ""
                if var uri = strFromCUtf8(nsPtr.pointee.uri) {
                    uri = XMLEscaping.unescape(uri)
                    namespaces[prefix] = uri
                }
                nsPtr = nsPtr.successor()
                indx += 1
            }
        }
        parser.delegate?.startElement(name: name, prefix: prefix, namespaces: namespaces, attributes: attributes)
    } else {
        parser.delegate?.startElement(name: name, prefix: prefix, namespaces: nil, attributes: attributes)
    }
}

private let SAX_endElement: endElementNsSAX2Func = { ctx_, localName, prefix_, _ in
    guard let name = strFromCUtf8(localName), let ctx_ else {
        return
    }
    let prefix = strFromCUtf8(prefix_)
    let parser = unsafeBitCast(ctx_, to: XMLParser.self)
    parser.delegate?.endElement(name: name, prefix: prefix)
}
