//
//  SWXMLHash.swift
//
//  Copyright (c) 2014 David Mohundro
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.

// swiftlint:disable all

import Foundation

let rootElementName = "SWXMLHash_Root_Element"

/// Parser options
public class SWXMLHashOptions {
  internal init() {}

  /// determines whether to parse the XML with lazy parsing or not
  public var shouldProcessLazily = false

  /// determines whether to parse XML namespaces or not (forwards to
  /// `XMLParser.shouldProcessNamespaces`)
  public var shouldProcessNamespaces = false
}

/// Simple XML parser
public class SWXMLHash {
  let options: SWXMLHashOptions

  private init(_ options: SWXMLHashOptions = SWXMLHashOptions()) {
    self.options = options
  }

  /**
   Method to configure how parsing works.

   - parameters:
   - configAction: a block that passes in an `SWXMLHashOptions` object with
   options to be set
   - returns: an `SWXMLHash` instance
   */
  class public func config(_ configAction: (SWXMLHashOptions) -> Void) -> SWXMLHash {
    let opts = SWXMLHashOptions()
    configAction(opts)
    return SWXMLHash(opts)
  }

  /**
   Begins parsing the passed in XML string.

   - parameters:
   - xml: an XML string. __Note__ that this is not a URL but a
   string containing XML.
   - returns: an `XMLIndexer` instance that can be iterated over
   */
  public func parse(_ xml: String) -> XMLIndexer {
    return parse(xml.data(using: String.Encoding.utf8)!)
  }

  /**
   Begins parsing the passed in XML string.

   - parameters:
   - data: a `Data` instance containing XML
   - returns: an `XMLIndexer` instance that can be iterated over
   */
  public func parse(_ data: Data) -> XMLIndexer {
    let parser: SimpleXmlParser = options.shouldProcessLazily
      ? LazyXMLParser(options)
      : FullXMLParser(options)
    return parser.parse(data)
  }

  /**
   Method to parse XML passed in as a string.

   - parameter xml: The XML to be parsed
   - returns: An XMLIndexer instance that is used to look up elements in the XML
   */
  class public func parse(_ xml: String) -> XMLIndexer {
    return SWXMLHash().parse(xml)
  }

  /**
   Method to parse XML passed in as a Data instance.

   - parameter data: The XML to be parsed
   - returns: An XMLIndexer instance that is used to look up elements in the XML
   */
  class public func parse(_ data: Data) -> XMLIndexer {
    return SWXMLHash().parse(data)
  }

  /**
   Method to lazily parse XML passed in as a string.

   - parameter xml: The XML to be parsed
   - returns: An XMLIndexer instance that is used to look up elements in the XML
   */
  class public func lazy(_ xml: String) -> XMLIndexer {
    return config { conf in conf.shouldProcessLazily = true }.parse(xml)
  }

  /**
   Method to lazily parse XML passed in as a Data instance.

   - parameter data: The XML to be parsed
   - returns: An XMLIndexer instance that is used to look up elements in the XML
   */
  class public func lazy(_ data: Data) -> XMLIndexer {
    return config { conf in conf.shouldProcessLazily = true }.parse(data)
  }
}

struct Stack<T> {
  var items = [T]()
  mutating func push(_ item: T) {
    items.append(item)
  }
  mutating func pop() -> T {
    return items.removeLast()
  }
  mutating func drop() {
    let _ = pop()
  }
  mutating func removeAll() {
    items.removeAll(keepingCapacity: false)
  }
  func top() -> T {
    return items[items.count - 1]
  }
}

protocol SimpleXmlParser {
  init(_ options: SWXMLHashOptions)
  func parse(_ data: Data) -> XMLIndexer
}

#if os(Linux)

  extension XMLParserDelegate {

    func parserDidStartDocument(_ parser: Foundation.XMLParser) { }
    func parserDidEndDocument(_ parser: Foundation.XMLParser) { }

    func parser(_ parser: Foundation.XMLParser,
                foundNotationDeclarationWithName name: String,
                publicID: String?,
                systemID: String?) { }

    func parser(_ parser: Foundation.XMLParser,
                foundUnparsedEntityDeclarationWithName name: String,
                publicID: String?,
                systemID: String?,
                notationName: String?) { }

    func parser(_ parser: Foundation.XMLParser,
                foundAttributeDeclarationWithName attributeName: String,
                forElement elementName: String,
                type: String?,
                defaultValue: String?) { }

    func parser(_ parser: Foundation.XMLParser,
                foundElementDeclarationWithName elementName: String,
                model: String) { }

    func parser(_ parser: Foundation.XMLParser,
                foundInternalEntityDeclarationWithName name: String,
                value: String?) { }

    func parser(_ parser: Foundation.XMLParser,
                foundExternalEntityDeclarationWithName name: String,
                publicID: String?,
                systemID: String?) { }

    func parser(_ parser: Foundation.XMLParser,
                didStartElement elementName: String,
                namespaceURI: String?,
                qualifiedName qName: String?,
                attributes attributeDict: [String : String]) { }

    func parser(_ parser: Foundation.XMLParser,
                didEndElement elementName: String,
                namespaceURI: String?,
                qualifiedName qName: String?) { }

    func parser(_ parser: Foundation.XMLParser,
                didStartMappingPrefix prefix: String,
                toURI namespaceURI: String) { }

    func parser(_ parser: Foundation.XMLParser, didEndMappingPrefix prefix: String) { }

    func parser(_ parser: Foundation.XMLParser, foundCharacters string: String) { }

    func parser(_ parser: Foundation.XMLParser,
                foundIgnorableWhitespace whitespaceString: String) { }

    func parser(_ parser: Foundation.XMLParser,
                foundProcessingInstructionWithTarget target: String,
                data: String?) { }

    func parser(_ parser: Foundation.XMLParser, foundComment comment: String) { }

    func parser(_ parser: Foundation.XMLParser, foundCDATA CDATABlock: Data) { }

    func parser(_ parser: Foundation.XMLParser,
                resolveExternalEntityName name: String,
                systemID: String?) -> Data? { return nil }

    func parser(_ parser: Foundation.XMLParser, parseErrorOccurred parseError: NSError) { }

    func parser(_ parser: Foundation.XMLParser,
                validationErrorOccurred validationError: NSError) { }
  }

#endif

/// The implementation of XMLParserDelegate and where the lazy parsing actually happens.
class LazyXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
  required init(_ options: SWXMLHashOptions) {
    self.options = options
    super.init()
  }

  var root = XMLElement(name: rootElementName)
  var parentStack = Stack<XMLElement>()
  var elementStack = Stack<String>()

  var data: Data?
  var ops: [IndexOp] = []
  let options: SWXMLHashOptions

  func parse(_ data: Data) -> XMLIndexer {
    self.data = data
    return XMLIndexer(self)
  }

  func startParsing(_ ops: [IndexOp]) {
    // clear any prior runs of parse... expected that this won't be necessary,
    // but you never know
    parentStack.removeAll()
    root = XMLElement(name: rootElementName)
    parentStack.push(root)

    self.ops = ops
    let parser = Foundation.XMLParser(data: data!)
    parser.shouldProcessNamespaces = options.shouldProcessNamespaces
    parser.delegate = self
    _ = parser.parse()
  }

  func parser(_ parser: Foundation.XMLParser,
              didStartElement elementName: String,
              namespaceURI: String?,
              qualifiedName qName: String?,
              attributes attributeDict: [String: String]) {

    elementStack.push(elementName)

    if !onMatch() {
      return
    }
    #if os(Linux)
      let attributeNSDict = NSDictionary(
        objects: attributeDict.values.flatMap({ $0 as? AnyObject }),
        forKeys: attributeDict.keys.map({ NSString(string: $0) as NSObject })
      )
      let currentNode = parentStack.top().addElement(elementName, withAttributes: attributeNSDict)
    #else
      let currentNode = parentStack
        .top()
        .addElement(elementName, withAttributes: attributeDict as NSDictionary)
    #endif
    parentStack.push(currentNode)
  }

  func parser(_ parser: Foundation.XMLParser, foundCharacters string: String) {
    if !onMatch() {
      return
    }

    let current = parentStack.top()

    current.addText(string)
  }

  func parser(_ parser: Foundation.XMLParser,
              didEndElement elementName: String,
              namespaceURI: String?,
              qualifiedName qName: String?) {

    let match = onMatch()

    elementStack.drop()

    if match {
      parentStack.drop()
    }
  }

  func onMatch() -> Bool {
    // we typically want to compare against the elementStack to see if it matches ops, *but*
    // if we're on the first element, we'll instead compare the other direction.
    if elementStack.items.count > ops.count {
      return elementStack.items.starts(with: ops.map { $0.key })
    } else {
      return ops.map { $0.key }.starts(with: elementStack.items)
    }
  }
}

/// The implementation of XMLParserDelegate and where the parsing actually happens.
class FullXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
  required init(_ options: SWXMLHashOptions) {
    self.options = options
    super.init()
  }

  var root = XMLElement(name: rootElementName)
  var parentStack = Stack<XMLElement>()
  let options: SWXMLHashOptions

  func parse(_ data: Data) -> XMLIndexer {
    // clear any prior runs of parse... expected that this won't be necessary,
    // but you never know
    parentStack.removeAll()

    parentStack.push(root)

    let parser = Foundation.XMLParser(data: data)
    parser.shouldProcessNamespaces = options.shouldProcessNamespaces
    parser.delegate = self
    _ = parser.parse()

    return XMLIndexer(root)
  }

  func parser(_ parser: Foundation.XMLParser,
              didStartElement elementName: String,
              namespaceURI: String?,
              qualifiedName qName: String?,
              attributes attributeDict: [String: String]) {
    #if os(Linux)
      let attributeNSDict = NSDictionary(
        objects: attributeDict.values.flatMap({ $0 as? AnyObject }),
        forKeys: attributeDict.keys.map({ NSString(string: $0) as NSObject })
      )
      let currentNode = parentStack.top().addElement(elementName, withAttributes: attributeNSDict)
    #else
      let currentNode = parentStack
        .top()
        .addElement(elementName, withAttributes: attributeDict as NSDictionary)
    #endif
    parentStack.push(currentNode)
  }

  func parser(_ parser: Foundation.XMLParser, foundCharacters string: String) {
    let current = parentStack.top()

    current.addText(string)
  }

  func parser(_ parser: Foundation.XMLParser,
              didEndElement elementName: String,
              namespaceURI: String?,
              qualifiedName qName: String?) {

    parentStack.drop()
  }
}

/// Represents an indexed operation against a lazily parsed `XMLIndexer`
public class IndexOp {
  var index: Int
  let key: String

  init(_ key: String) {
    self.key = key
    self.index = -1
  }

  func toString() -> String {
    if index >= 0 {
      return key + " " + index.description
    }

    return key
  }
}

/// Represents a collection of `IndexOp` instances. Provides a means of iterating them
/// to find a match in a lazily parsed `XMLIndexer` instance.
public class IndexOps {
  var ops: [IndexOp] = []

  let parser: LazyXMLParser

  init(parser: LazyXMLParser) {
    self.parser = parser
  }

  func findElements() -> XMLIndexer {
    parser.startParsing(ops)
    let indexer = XMLIndexer(parser.root)
    var childIndex = indexer
    for op in ops {
      childIndex = childIndex[op.key]
      if op.index >= 0 {
        childIndex = childIndex[op.index]
      }
    }
    ops.removeAll(keepingCapacity: false)
    return childIndex
  }

  func stringify() -> String {
    var s = ""
    for op in ops {
      s += "[" + op.toString() + "]"
    }
    return s
  }
}

/// Error type that is thrown when an indexing or parsing operation fails.
public enum IndexingError: Error {
  case Attribute(attr: String)
  case AttributeValue(attr: String, value: String)
  case Key(key: String)
  case Index(idx: Int)
  case Init(instance: AnyObject)
  case Error
}

/// Returned from SWXMLHash, allows easy element lookup into XML data.
public enum XMLIndexer: Sequence {
  case element(XMLElement)
  case list([XMLElement])
  case stream(IndexOps)
  case xmlError(IndexingError)

  /// The underlying XMLElement at the currently indexed level of XML.
  public var element: XMLElement? {
    switch self {
    case .element(let elem):
      return elem
    case .stream(let ops):
      let list = ops.findElements()
      return list.element
    default:
      return nil
    }
  }

  /// All elements at the currently indexed level
  public var all: [XMLIndexer] {
    switch self {
    case .list(let list):
      var xmlList = [XMLIndexer]()
      for elem in list {
        xmlList.append(XMLIndexer(elem))
      }
      return xmlList
    case .element(let elem):
      return [XMLIndexer(elem)]
    case .stream(let ops):
      let list = ops.findElements()
      return list.all
    default:
      return []
    }
  }

  /// All child elements from the currently indexed level
  public var children: [XMLIndexer] {
    var list = [XMLIndexer]()
    for elem in all.map({ $0.element! }).compactMap({ $0 }) {
      for elem in elem.xmlChildren {
        list.append(XMLIndexer(elem))
      }
    }
    return list
  }

  /**
   Allows for element lookup by matching attribute values.

   - parameters:
   - attr: should the name of the attribute to match on
   - value: should be the value of the attribute to match on
   - throws: an XMLIndexer.xmlError if an element with the specified attribute isn't found
   - returns: instance of XMLIndexer
   */
  public func withAttr(_ attr: String, _ value: String) throws -> XMLIndexer {
    switch self {
    case .stream(let opStream):
      let match = opStream.findElements()
      return try match.withAttr(attr, value)
    case .list(let list):
      if let elem = list.first(where: {$0.attribute(by: attr)?.text == value}) {
        return .element(elem)
      }
      throw IndexingError.AttributeValue(attr: attr, value: value)
    case .element(let elem):
      if elem.attribute(by: attr)?.text == value {
        return .element(elem)
      }
      throw IndexingError.AttributeValue(attr: attr, value: value)
    default:
      throw IndexingError.Attribute(attr: attr)
    }
  }

  /**
   Initializes the XMLIndexer

   - parameter _: should be an instance of XMLElement, but supports other values for error handling
   - throws: an Error if the object passed in isn't an XMLElement or LaxyXMLParser
   */
  public init(_ rawObject: AnyObject) throws {
    switch rawObject {
    case let value as XMLElement:
      self = .element(value)
    case let value as LazyXMLParser:
      self = .stream(IndexOps(parser: value))
    default:
      throw IndexingError.Init(instance: rawObject)
    }
  }

  /**
   Initializes the XMLIndexer

   - parameter _: an instance of XMLElement
   */
  public init(_ elem: XMLElement) {
    self = .element(elem)
  }

  init(_ stream: LazyXMLParser) {
    self = .stream(IndexOps(parser: stream))
  }

  /**
   Find an XML element at the current level by element name

   - parameter key: The element name to index by
   - returns: instance of XMLIndexer to match the element (or elements) found by key
   - throws: Throws an XMLIndexingError.Key if no element was found
   */
  public func byKey(_ key: String) throws -> XMLIndexer {
    switch self {
    case .stream(let opStream):
      let op = IndexOp(key)
      opStream.ops.append(op)
      return .stream(opStream)
    case .element(let elem):
      let match = elem.xmlChildren.filter({ $0.name == key })
      if !match.isEmpty {
        if match.count == 1 {
          return .element(match[0])
        } else {
          return .list(match)
        }
      }
      fallthrough
    default:
      throw IndexingError.Key(key: key)
    }
  }

  /**
   Find an XML element at the current level by element name

   - parameter key: The element name to index by
   - returns: instance of XMLIndexer to match the element (or elements) found by
   */
  public subscript(key: String) -> XMLIndexer {
    do {
      return try self.byKey(key)
    } catch let error as IndexingError {
      return .xmlError(error)
    } catch {
      return .xmlError(IndexingError.Key(key: key))
    }
  }

  /**
   Find an XML element by index within a list of XML Elements at the current level

   - parameter index: The 0-based index to index by
   - throws: XMLIndexer.xmlError if the index isn't found
   - returns: instance of XMLIndexer to match the element (or elements) found by index
   */
  public func byIndex(_ index: Int) throws -> XMLIndexer {
    switch self {
    case .stream(let opStream):
      opStream.ops[opStream.ops.count - 1].index = index
      return .stream(opStream)
    case .list(let list):
      if index <= list.count {
        return .element(list[index])
      }
      return .xmlError(IndexingError.Index(idx: index))
    case .element(let elem):
      if index == 0 {
        return .element(elem)
      }
      fallthrough
    default:
      return .xmlError(IndexingError.Index(idx: index))
    }
  }

  /**
   Find an XML element by index

   - parameter index: The 0-based index to index by
   - returns: instance of XMLIndexer to match the element (or elements) found by index
   */
  public subscript(index: Int) -> XMLIndexer {
    do {
      return try byIndex(index)
    } catch let error as IndexingError {
      return .xmlError(error)
    } catch {
      return .xmlError(IndexingError.Index(idx: index))
    }
  }

  typealias GeneratorType = XMLIndexer

  /**
   Method to iterate (for-in) over the `all` collection

   - returns: an array of `XMLIndexer` instances
   */
  public func makeIterator() -> IndexingIterator<[XMLIndexer]> {
    return all.makeIterator()
  }
}

/// XMLIndexer extensions
/*
 extension XMLIndexer: Boolean {
 /// True if a valid XMLIndexer, false if an error type
 public var boolValue: Bool {
 switch self {
 case .xmlError:
 return false
 default:
 return true
 }
 }
 }
 */

extension XMLIndexer: CustomStringConvertible {
  /// The XML representation of the XMLIndexer at the current level
  public var description: String {
    switch self {
    case .list(let list):
      return list.map { $0.description }.joined(separator: "")
    case .element(let elem):
      if elem.name == rootElementName {
        return elem.children.map { $0.description }.joined(separator: "")
      }

      return elem.description
    default:
      return ""
    }
  }
}

extension IndexingError: CustomStringConvertible {
  /// The description for the `IndexingError`.
  public var description: String {
    switch self {
    case .Attribute(let attr):
      return "XML Attribute Error: Missing attribute [\"\(attr)\"]"
    case let .AttributeValue(attr, value):
      return "XML Attribute Error: Missing attribute [\"\(attr)\"] with value [\"\(value)\"]"
    case .Key(let key):
      return "XML Element Error: Incorrect key [\"\(key)\"]"
    case .Index(let index):
      return "XML Element Error: Incorrect index [\"\(index)\"]"
    case .Init(let instance):
      return "XML Indexer Error: initialization with Object [\"\(instance)\"]"
    case .Error:
      return "Unknown Error"
    }
  }
}

/// Models content for an XML doc, whether it is text or XML
public protocol XMLContent: CustomStringConvertible { }

/// Models a text element
public class TextElement: XMLContent {
  /// The underlying text value
  public let text: String
  init(text: String) {
    self.text = text
  }
}

public struct XMLAttribute {
  public let name: String
  public let text: String
  init(name: String, text: String) {
    self.name = name
    self.text = text
  }
}

/// Models an XML element, including name, text and attributes
public class XMLElement: XMLContent {
  /// The name of the element
  public let name: String

  // swiftlint:disable line_length
  /// The attributes of the element
  @available(*, deprecated, message: "See `allAttributes` instead, which introduces the XMLAttribute type over a simple String type")
  public var attributes: [String:String] {
    var attrMap = [String: String]()
    for (name, attr) in allAttributes {
      attrMap[name] = attr.text
    }
    return attrMap
  }
  // swiftlint:enable line_length

  /// All attributes
  public var allAttributes = [String: XMLAttribute]()

  public func attribute(by name: String) -> XMLAttribute? {
    return allAttributes[name]
  }

  /// The inner text of the element, if it exists
  public var text: String? {
    return children
      .map({ $0 as? TextElement })
      .compactMap({ $0 })
      .reduce("", { $0 + $1.text })
  }

  /// All child elements (text or XML)
  public var children = [XMLContent]()
  var count: Int = 0
  var index: Int

  var xmlChildren: [XMLElement] {
    return children.map { $0 as? XMLElement }.compactMap { $0 }
  }

  /**
   Initialize an XMLElement instance

   - parameters:
   - name: The name of the element to be initialized
   - index: The index of the element to be initialized
   */
  init(name: String, index: Int = 0) {
    self.name = name
    self.index = index
  }

  /**
   Adds a new XMLElement underneath this instance of XMLElement

   - parameters:
   - name: The name of the new element to be added
   - withAttributes: The attributes dictionary for the element being added
   - returns: The XMLElement that has now been added
   */
  func addElement(_ name: String, withAttributes attributes: NSDictionary) -> XMLElement {
    let element = XMLElement(name: name, index: count)
    count += 1

    children.append(element)

    for (keyAny, valueAny) in attributes {
      if let key = keyAny as? String,
        let value = valueAny as? String {
        element.allAttributes[key] = XMLAttribute(name: key, text: value)
      }
    }

    return element
  }

  func addText(_ text: String) {
    let elem = TextElement(text: text)

    children.append(elem)
  }
}

extension TextElement: CustomStringConvertible {
  /// The text value for a `TextElement` instance.
  public var description: String {
    return text
  }
}

extension XMLAttribute: CustomStringConvertible {
  /// The textual representation of an `XMLAttribute` instance.
  public var description: String {
    return "\(name)=\"\(text)\""
  }
}

extension XMLElement: CustomStringConvertible {
  /// The tag, attributes and content for a `XMLElement` instance (<elem id="foo">content</elem>)
  public var description: String {
    var attributesString = allAttributes.map { $0.1.description }.joined(separator: " ")
    if !attributesString.isEmpty {
      attributesString = " " + attributesString
    }

    if !children.isEmpty {
      var xmlReturn = [String]()
      xmlReturn.append("<\(name)\(attributesString)>")
      for child in children {
        xmlReturn.append(child.description)
      }
      xmlReturn.append("</\(name)>")
      return xmlReturn.joined(separator: "")
    }

    if text != nil {
      return "<\(name)\(attributesString)>\(text!)</\(name)>"
    } else {
      return "<\(name)\(attributesString)/>"
    }
  }
}

// Workaround for "'XMLElement' is ambiguous for type lookup in this context" error on macOS.
//
// On macOS, `XMLElement` is defined in Foundation.
// So, the code referencing `XMLElement` generates above error.
// Following code allow to using `SWXMLhash.XMLElement` in client codes.
extension SWXMLHash {
  public typealias XMLElement = SWXMLHashXMLElement
}

public  typealias SWXMLHashXMLElement = XMLElement
