//
//  XML.swift
//  Conduit
//
//  Created by John Hammerlund on 12/19/16.
//  Copyright © 2017 MINDBODY. All rights reserved.
//

import Foundation

public enum XMLError: Error {
    case nodeNotFound(name: String)
    case invalidDataType
}

/// Represents an XML document
public final class XML {

    /// The root of the document
    public var root: XMLNode?
    /// A list of processing instructions at the root of the document
    public var processingInstructions: [XMLNode]

    /// Produces an XML document with the provided root and processing instructions.
    ///
    /// - Parameter root: The root of the document
    /// - Parameter processingInstructions: Processing instructions at the root of the document
    public init(root: XMLNode, processingInstructions: [XMLNode] = []) {
        let containesNotProcessingInstruction = processingInstructions.contains { $0.isProcessingInstruction == false }
        precondition(containesNotProcessingInstruction == false)
        self.root = root
        self.processingInstructions = processingInstructions
    }

}

// MARK: - CustomStringConvertible

extension XML: CustomStringConvertible {

    /// Serialized XML string output
    public var description: String {
        return xmlString(format: .condensed)
    }

}

// MARK: - LosslessStringConvertible

extension XML: LosslessStringConvertible {
    /// Attempts to produce an XML document with the provided XML string
    ///
    /// - Parameter description: The XML string to deserialize
    public convenience init?(_ description: String) {
        let parser = XML.Parser(xmlString: description)
        guard let xml = parser?.parse(), let root = xml.root else {
            return nil
        }
        self.init(root: root)
    }
}

// MARK: XML string parser

extension XML {

    private class Parser: NSObject, XMLParserDelegate {
        private let xmlParser: XMLParser
        private var workingTree = [XMLNode]()
        private var activeNode: XMLNode?
        private var root: XMLNode?

        init?(xmlString: String) {
            guard let data = xmlString.data(using: .utf8) else {
                return nil
            }
            xmlParser = XMLParser(data: data)
            super.init()
            xmlParser.delegate = self
        }

        func parse() -> XML? {
            if xmlParser.parse(), let root = root {
                return XML(root: root)
            }
            return nil
        }

        func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?,
                    attributes attributeDict: [String: String] = [:]) {
            let attributes = XMLNodeAttributes(attributes: attributeDict.map { (attribute: $0.key, value: $0.value) })
            let node = XMLNode(name: elementName, attributes: attributes)
            if let parentNode = workingTree.popLast() {
                workingTree.append(parentNode)
            }
            workingTree.append(node)
        }

        func parser(_ parser: XMLParser, foundCharacters string: String) {
            if let activeNode = workingTree.popLast() {
                let text = activeNode.text ?? ""
                activeNode.text = text + string
                workingTree.append(activeNode)
            }
        }

        func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
            let finishedNode = workingTree.popLast()
            let parentNode = workingTree.popLast()
            if let parentNode = parentNode {
                if let finishedNode = finishedNode {
                    var children = parentNode.children
                    children.append(finishedNode)
                    parentNode.children = children
                }
                workingTree.append(parentNode)
            }
            else {
                root = finishedNode
            }
        }
    }

}
