//
//  SwiftSoup.swift
//  Jsoup
//
//  Created by Nabil Chatbi on 29/09/16.
//

import Foundation

/// Parse markup into a Document with automatic format detection. If the input starts with an XML declaration
/// (`<?xml`), the XML parser is used; otherwise the HTML parser is applied. Use ``parseHTML(_:_:)`` or
/// ``parseXML(_:_:)`` to force a specific parser.
///
/// - parameter html:    markup to parse
/// - parameter baseUri: The URL where the markup was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
///   before the markup declares a `<base href>` tag.
/// - returns: parsed Document
public func parse(_ html: String, _ baseUri: String) throws -> Document {
  if looksLikeXml(html) {
    return try Parser.xmlParser().parseInput(html, baseUri)
  }
  return try Parser.parse(html, baseUri)
}

/// Parse Data into a Document with automatic format detection. If the input starts with an XML declaration
/// (`<?xml`), the XML parser is used; otherwise the HTML parser is applied. Use ``parseHTML(_:_:)`` or
/// ``parseXML(_:_:)`` to force a specific parser.
///
/// - parameter data: data to parse
/// - parameter baseUri: The URL where the markup was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
///   before the markup declares a `<base href>` tag.
/// - returns: parsed Document
public func parse(_ data: Data, _ baseUri: String) throws -> Document {
  let bytes = [UInt8](data)
  if looksLikeXml(bytes) {
    return try Parser.xmlParser().parseInput(bytes, baseUri)
  }
  return try Parser.parse(data, baseUri)
}

/// Parse HTML into a Document using a closure-provided byte buffer.
///
/// - parameter baseUri: The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs.
/// - parameter provider: Closure that supplies a byte buffer and invokes the parse callback within the buffer lifetime.
/// - returns: sane HTML
public func parse(
  _ baseUri: String,
  withBytes provider: (_ parse: (UnsafeBufferPointer<UInt8>) throws -> Document) throws -> Document
) rethrows -> Document {
  return try Parser.parse(baseUri, withBytes: provider)
}

/// Parse HTML into a Document using a closure-provided byte buffer.
///
/// - parameter provider: Closure that supplies a byte buffer and invokes the parse callback within the buffer lifetime.
/// - returns: sane HTML
public func parse(
  withBytes provider: (_ parse: (UnsafeBufferPointer<UInt8>) throws -> Document) throws -> Document
) rethrows -> Document {
  return try Parser.parse("", withBytes: provider)
}

/// Parse HTML into a Document, using the provided Parser. You can provide an alternate parser, such as a simple XML
/// (non-HTML) parser.
///
/// - parameter html:    HTML to parse
/// - parameter baseUri: The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
///   before the HTML declares a `<base href>` tag.
/// - parameter parser: alternate parse (e.g. ``Parser/xmlParser()``) to use.
/// - returns: sane HTML
public func parse(_ html: String, _ baseUri: String, _ parser: Parser) throws -> Document {
  return try parser.parseInput(html, baseUri)
}

/// Parse HTML into a Document, using the provided Parser. You can provide an alternate parser, such as a simple XML
/// (non-HTML) parser.
///
/// - parameter html:    HTML to parse
/// - parameter baseUri: The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
///   before the HTML declares a `<base href>` tag.
/// - parameter parser: alternate parser (e.g. ``Parser/xmlParser()``) to use.
/// - returns: sane HTML
public func parse(_ html: [UInt8], _ baseUri: String, _ parser: Parser) throws -> Document {
  return try parser.parseInput(html, baseUri)
}

/// Parse markup into a Document with automatic format detection. As no base URI is specified, absolute URL
/// detection relies on the markup including a `<base href>` tag.
///
/// - parameter html: markup to parse
/// - returns: parsed Document
/// - seealso: ``parse(_:_:)-(String,String)``
public func parse(_ html: String) throws -> Document {
  return try parse(html, "")
}

/// Parse Data into a Document with automatic format detection. As no base URI is specified, absolute URL
/// detection relies on the markup including a `<base href>` tag.
///
/// - parameter data: data to parse
/// - returns: parsed Document
/// - seealso: ``parse(_:_:)-(String,String)``
public func parse(_ data: Data) throws -> Document {
  return try parse(data, "")
}

/// Parse XML into a Document using the XML parser. Unlike ``parse(_:_:)``, this does not apply HTML5 tree-building
/// rules or normalize known HTML tags like `<link>` or `<img>`.
///
/// - parameter xml: XML to parse
/// - parameter baseUri: The URL where the XML was retrieved from. Used to resolve relative URLs to absolute URLs.
/// - returns: parsed XML document
public func parseXML(_ xml: String, _ baseUri: String) throws -> Document {
  return try parse(xml, baseUri, Parser.xmlParser())
}

/// Parse XML into a Document using the XML parser. As no base URI is specified, absolute URL detection relies on the
/// XML including a suitable base URI in its own data.
///
/// - parameter xml: XML to parse
/// - returns: parsed XML document
public func parseXML(_ xml: String) throws -> Document {
  return try parseXML(xml, "")
}

/// Parse XML data into a Document using the XML parser.
///
/// - parameter data: XML data to parse
/// - parameter baseUri: The URL where the XML was retrieved from. Used to resolve relative URLs to absolute URLs.
/// - returns: parsed XML document
public func parseXML(_ data: Data, _ baseUri: String) throws -> Document {
  return try Parser.xmlParser().parseInput([UInt8](data), baseUri)
}

/// Parse XML data into a Document using the XML parser. As no base URI is specified, absolute URL detection relies on
/// the XML including a suitable base URI in its own data.
///
/// - parameter data: XML data to parse
/// - returns: parsed XML document
public func parseXML(_ data: Data) throws -> Document {
  return try parseXML(data, "")
}

/// Fetch a URL as raw bytes and parse it as HTML. This avoids relying on Foundation string encoding detection before
/// parsing.
///
/// - parameter url: URL to fetch and parse
/// - returns: sane HTML
public func parse(_ url: URL) throws -> Document {
  return try parse(Data(contentsOf: url), url.absoluteString)
}

/// Fetch a URL as raw bytes and parse it using the provided parser.
///
/// - parameter url: URL to fetch and parse
/// - parameter parser: alternate parser (e.g. ``Parser/xmlParser()``) to use.
/// - returns: parsed document
public func parse(_ url: URL, _ parser: Parser) throws -> Document {
  return try parser.parseInput([UInt8](Data(contentsOf: url)), url.absoluteString)
}

// MARK: - Explicit HTML parsing

/// Parse HTML into a Document using the HTML parser. Unlike ``parse(_:_:)``, this always uses the HTML parser
/// regardless of the input content (no auto-detection).
///
/// - parameter html: HTML to parse
/// - parameter baseUri: The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs.
/// - returns: parsed HTML document
public func parseHTML(_ html: String, _ baseUri: String) throws -> Document {
  return try Parser.parse(html, baseUri)
}

/// Parse HTML into a Document using the HTML parser. As no base URI is specified, absolute URL detection relies on
/// the HTML including a `<base href>` tag.
///
/// - parameter html: HTML to parse
/// - returns: parsed HTML document
public func parseHTML(_ html: String) throws -> Document {
  return try parseHTML(html, "")
}

/// Parse HTML data into a Document using the HTML parser.
///
/// - parameter data: HTML data to parse
/// - parameter baseUri: The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs.
/// - returns: parsed HTML document
public func parseHTML(_ data: Data, _ baseUri: String) throws -> Document {
  return try Parser.parse(data, baseUri)
}

/// Parse HTML data into a Document using the HTML parser. As no base URI is specified, absolute URL detection relies
/// on the HTML including a `<base href>` tag.
///
/// - parameter data: HTML data to parse
/// - returns: parsed HTML document
public func parseHTML(_ data: Data) throws -> Document {
  return try parseHTML(data, "")
}

// MARK: - Format detection

private func looksLikeXml(_ string: String) -> Bool {
  var i = string.startIndex
  while i < string.endIndex && string[i].isWhitespace {
    i = string.index(after: i)
  }
  return string[i...].hasPrefix("<?xml")
}

private func looksLikeXml(_ bytes: [UInt8]) -> Bool {
  let xmlDecl: [UInt8] = [0x3C, 0x3F, 0x78, 0x6D, 0x6C]  // <?xml
  var i = 0
  while i < bytes.count
    && (bytes[i] == 0x20 || bytes[i] == 0x09 || bytes[i] == 0x0A || bytes[i] == 0x0D)
  {
    i += 1
  }
  guard i + xmlDecl.count <= bytes.count else { return false }
  for j in 0..<xmlDecl.count {
    if bytes[i + j] != xmlDecl[j] { return false }
  }
  return true
}

//todo:
//	/**
//	* Creates a new {@link Connection} to a URL. Use to fetch and parse a HTML page.
//	* <p>
//	* Use examples:
//	* <ul>
//	*  <li><code>Document doc = Jsoup.connect("http://example.com").userAgent("Mozilla").data("name", "jsoup").get();</code></li>
//	*  <li><code>Document doc = Jsoup.connect("http://example.com").cookie("auth", "token").post();</code></li>
//	* </ul>
//	* @param url URL to connect to. The protocol must be {@code http} or {@code https}.
//	* @return the connection. You can add data, cookies, and headers; set the user-agent, referrer, method; and then execute.
//	*/
//	public static Connection connect(String url) {
//		return HttpConnection.connect(url);
//	}

//todo:
//	/**
//	Parse the contents of a file as HTML.
//
//	@param in          file to load HTML from
//	@param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
//	present, or fall back to {@code UTF-8} (which is often safe to do).
//	@param baseUri     The URL where the HTML was retrieved from, to resolve relative links against.
//	@return sane HTML
//
//	@throws IOException if the file could not be found, or read, or if the charsetName is invalid.
//	*/
//	public static Document parse(File in, String charsetName, String baseUri) throws IOException {
//	return DataUtil.load(in, charsetName, baseUri);
//	}

//todo:
//	/**
//	Parse the contents of a file as HTML. The location of the file is used as the base URI to qualify relative URLs.
//
//	@param in          file to load HTML from
//	@param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
//	present, or fall back to {@code UTF-8} (which is often safe to do).
//	@return sane HTML
//
//	@throws IOException if the file could not be found, or read, or if the charsetName is invalid.
//	@see #parse(File, String, String)
//	*/
//	public static Document parse(File in, String charsetName) throws IOException {
//	return DataUtil.load(in, charsetName, in.getAbsolutePath());
//	}

//	/**
//	Read an input stream, and parse it to a Document.
//
//	@param in          input stream to read. Make sure to close it after parsing.
//	@param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
//	present, or fall back to {@code UTF-8} (which is often safe to do).
//	@param baseUri     The URL where the HTML was retrieved from, to resolve relative links against.
//	@return sane HTML
//
//	@throws IOException if the file could not be found, or read, or if the charsetName is invalid.
//	*/
//	public static Document parse(InputStream in, String charsetName, String baseUri) throws IOException {
//	return DataUtil.load(in, charsetName, baseUri);
//	}

//	/**
//	Read an input stream, and parse it to a Document. You can provide an alternate parser, such as a simple XML
//	(non-HTML) parser.
//
//	@param in          input stream to read. Make sure to close it after parsing.
//	@param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
//	present, or fall back to {@code UTF-8} (which is often safe to do).
//	@param baseUri     The URL where the HTML was retrieved from, to resolve relative links against.
//	@param parser alternate {@link Parser#xmlParser() parser} to use.
//	@return sane HTML
//
//	@throws IOException if the file could not be found, or read, or if the charsetName is invalid.
//	*/
//	public static Document parse(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException {
//	return DataUtil.load(in, charsetName, baseUri, parser);
//	}

/// Parse a fragment of HTML, with the assumption that it forms the `body` of the HTML.
///
/// - parameter bodyHtml: body HTML fragment
/// - parameter baseUri:  URL to resolve relative URLs against.
/// - returns: sane HTML document
/// - seealso: ``Document/body()``
public func parseBodyFragment(_ bodyHtml: String, _ baseUri: String) throws -> Document {
  return try Parser.parseBodyFragment(bodyHtml, baseUri)
}

/// Parse a fragment of HTML, with the assumption that it forms the `body` of the HTML.
///
/// - parameter bodyHtml: body HTML fragment
/// - returns: sane HTML document
/// - seealso: ``Document/body()``
public func parseBodyFragment(_ bodyHtml: String) throws -> Document {
  return try Parser.parseBodyFragment(bodyHtml, "")
}

//	/**
//	Fetch a URL, and parse it as HTML. Provided for compatibility; in most cases use {@link #connect(String)} instead.
//	<p>
//	The encoding character set is determined by the content-type header or http-equiv meta tag, or falls back to {@code UTF-8}.
//
//	@param url           URL to fetch (with a GET). The protocol must be {@code http} or {@code https}.
//	@param timeoutMillis Connection and read timeout, in milliseconds. If exceeded, IOException is thrown.
//	@return The parsed HTML.
//
//	@throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
//	@throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
//	@throws UnsupportedMimeTypeException if the response mime type is not supported and those errors are not ignored
//	@throws java.net.SocketTimeoutException if the connection times out
//	@throws IOException if a connection or read error occurs
//
//	@see #connect(String)
//	*/
//	public static func parse(_ url: URL, _ timeoutMillis: Int)throws->Document {
//	Connection con = HttpConnection.connect(url);
//	con.timeout(timeoutMillis);
//	return con.get();
//	}

/// Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
/// tags and attributes.
///
/// - parameter bodyHtml:  input untrusted HTML (body fragment)
/// - parameter baseUri:   URL to resolve relative URLs against
/// - parameter whitelist: white-list of permitted HTML elements
/// - returns: safe HTML (body fragment)
/// - seealso: ``Cleaner/clean(_:)``
public func clean(_ bodyHtml: String, _ baseUri: String, _ whitelist: Whitelist) throws -> String? {
  let dirty: Document = try parseBodyFragment(bodyHtml, baseUri)
  let cleaner: Cleaner = Cleaner(whitelist)
  let clean: Document = try cleaner.clean(dirty)
  let html = try clean.body()?.html()
  return normalizeTextOnlyNbsp(html, whitelist: whitelist)
}

/// Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
/// tags and attributes.
///
/// - parameter bodyHtml:  input untrusted HTML (body fragment)
/// - parameter whitelist: white-list of permitted HTML elements
/// - returns: safe HTML (body fragment)
/// - seealso: ``Cleaner/clean(_:)``
public func clean(_ bodyHtml: String, _ whitelist: Whitelist) throws -> String? {
  return try SwiftSoup.clean(bodyHtml, "", whitelist)
}

/// Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of
/// permitted tags and attributes.
///
/// - parameter bodyHtml: input untrusted HTML (body fragment)
/// - parameter baseUri: URL to resolve relative URLs against
/// - parameter whitelist: white-list of permitted HTML elements
/// - parameter outputSettings: document output settings; use to control pretty-printing and entity escape modes
/// - returns: safe HTML (body fragment)
/// - seealso: ``Cleaner/clean(_:)``
public func clean(
  _ bodyHtml: String, _ baseUri: String, _ whitelist: Whitelist, _ outputSettings: OutputSettings
) throws -> String? {
  let dirty: Document = try SwiftSoup.parseBodyFragment(bodyHtml, baseUri)
  let cleaner: Cleaner = Cleaner(whitelist)
  let clean: Document = try cleaner.clean(dirty)
  clean.outputSettings(outputSettings)
  let html = try clean.body()?.html()
  return normalizeTextOnlyNbsp(html, whitelist: whitelist)
}

private func normalizeTextOnlyNbsp(_ html: String?, whitelist: Whitelist) -> String? {
  guard whitelist.isTextOnly(), let html else {
    return html
  }

  var normalized = html.replacingOccurrences(of: "&nbsp;", with: " ")
  normalized = normalized.replacingOccurrences(
    of: "&#(?:160|x[aA]0);",
    with: " ",
    options: .regularExpression
  )
  return normalized
}

/// Test if the input HTML has only tags and attributes allowed by the Whitelist. Useful for form validation. The input HTML should
/// still be run through the cleaner to set up enforced attributes, and to tidy the output.
/// - parameter bodyHtml: HTML to test
/// - parameter whitelist: whitelist to test against
/// - returns: true if no tags or attributes were removed; false otherwise
/// - seealso: ``clean(_:_:)``
public func isValid(_ bodyHtml: String, _ whitelist: Whitelist) throws -> Bool {
  let dirty = try parseBodyFragment(bodyHtml, "")
  let cleaner = Cleaner(whitelist)
  return try cleaner.isValid(dirty)
}
