/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.xml;

import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.nio.file.Path;
import java.util.function.Predicate;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;

import net.ranides.assira.generic.Wrapper;
import net.ranides.assira.generic.Wrappers;
import net.ranides.assira.generic.Wrappers.AbstractWrapper;
import net.ranides.assira.io.ResourceUtils;
import net.ranides.assira.xml.impl.W3ElementFactory;
import net.ranides.assira.xml.impl.W3Element;
import org.intellij.lang.annotations.Language;
import org.w3c.dom.Node;

/**
 * XMLElement offer interface similar to JQuery
 *
 * You can use static factory methods to wrap "org.w3c.dom" nodes or load documents.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@SuppressWarnings({
    "PMD.TooManyDifferentMethods",
    "PMD.TooManyPublicMethods",
    "PMD.MethodNameRules"
})
public interface XMLElement extends AbstractWrapper<String> {

    /**
     * Wraps "org.w3c.dom" node
     *
     * @param node node
     * @return XMLElement
     */
    static XMLElement fromNode(Node node) {
        return W3Element.wrap(node);
    }

    /**
     * Parses XML markup from "doc" string.
     *
     * @param doc doc
     * @return XMLElement
     */
    static XMLElement fromString(@Language("XML") String doc) {
        return W3ElementFactory.fromString(new StringReader(doc));
    }

    /**
     * Parses XML document from file
     *
     * @param doc doc
     * @return XMLElement
     */
    static XMLElement fromFile(File doc) {
        return W3ElementFactory.fromFile(doc);
    }

    /**
     * Parses XML document from file
     *
     * @param doc doc
     * @return XMLElement
     */
    static XMLElement fromPath(Path doc) {
        return W3ElementFactory.fromPath(doc);
    }

    /**
     * Parses XML document from file
     *
     * @param doc doc
     * @return XMLElement
     */
    static XMLElement fromURI(URI doc) {
        return W3ElementFactory.fromURI(doc);
    }

    /**
     * Parses XML document from reader
     *
     * @param doc doc
     * @return XMLElement
     */
    static XMLElement fromReader(Reader doc) {
        return W3ElementFactory.fromString(doc);
    }

    /**
     * Parses XML document from stream
     *
     * @param doc doc
     * @return XMLElement
     */
    static XMLElement fromStream(InputStream doc) {
        return W3ElementFactory.fromStream(doc);
    }

    /**
     * Parses XML document from resource file
     *
     * @param doc doc
     * @return XMLElement
     */
    static XMLElement fromResource(String doc) {
        return W3ElementFactory.fromStream(ResourceUtils.stream(doc));
    }

    /**
     * Returns document which owns this element
     *
     * This method does not return root node of XML document.
     * To obtain root element, please use {@link #root}
     *
     * @return XMLDocument
     */
    XMLDocument document();

    /**
     * Returns root parent of this element
     * If this element is root, returns it.
     *
     * @return XMLElement
     */
    XMLElement root();

    /**
     * Returns type of this element.
     * For example XMLElement can represent attribute, tag, or text fragment.
     *
     * @return XMLType
     */
    XMLType nodetype();

    /**
     * Interoperability method: returns "org.w3c.dom.Node"
     *
     * @return this
     */
    Node node();

    /**
     * Returns name of this element.
     *
     * Please note that different node types return different results:
     * 		ELEMENT         tag name
     * 		ATTRIBUTE       attribute name
     *
     * 		ENTITY          name of entity
     * 		REFERENCE       name of referenced entity
     * 		PROCESSING      processing instruction target
     * 		DOCTYPE         doctype name
     * 		NOTATION        notation name
     *
     * 		DOCUMENT        "#document"
     * 		FRAGMENT        "#document-fragment"
     * 		TEXT            "#text"
     * 		CDATA           "#cdata-section"
     * 		COMMENT         "#comment"
     *
     * @return String
     */
    String name();

    /**
     * Returns XPATH to this element
     *
     * @return String
     */
    String xpath();

    /**
     * Returns content of this element
     *
     * Please note that different node types return different results:
     *		ELEMENT         null!
     *
     * 		ATTRIBUTE       attribute value
     * 		TEXT            content of text node
     * 		COMMENT         content of the comment
     * 		CDATA           content of CDATA section
     * 	    PROCESSING      content of processing instruction
     *
     * 		ENTITY          null
     * 		REFERENCE       null
     * 		DOCTYPE         null
     * 		NOTATION        null
     * 		DOCUMENT        null
     * 		FRAGMENT        null
     *
     * @return String
     */
    String content();

    /**
     * Returns text content inside this element.
     *
     * If this node returns non-null content, then it is returned.
     * Otherwise, it returns concatenation of "text" from all child nodes.
     * It returns empty string, if it has no children.
     *
     * Exceptions:
     *      DOCUMENT and NOTATION will return null!
     *
     * @return String or null
     */
    String text();

    /**
     * Returns all attributes of this elements, as XMLElements collection.
     *
     * XMLElements is universal collection.
     * You can use it to process list of attributes, tag elements or comment sections.
     *
     * @return XMLElements
     */
    XMLElements attrs();

    /**
     * Returns first attribute with specified name
     *
     * Throws NoSuchElementException if there is no attribute.
     *
     * @param name name
     * @return XMLElement
     */
    XMLElement attr(String name);

    /**
     * Compiles provided expression to XMLSelector and returns elements matched by it.
     *
     * Generally, this method searches recursively for any matching element.
     * If you want to select only direct children matching provided expression, lets use {@link #children(String)}
     *
     * Returns empty collection if there is no matching elements.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements find(String selector);

    /**
     * Returns elements matched by provided XMLSelector.
     *
     * Generally, this method searches recursively for any matching element.
     * If you want to select only direct children matching provided expression, lets use {@link #children(Predicate)}
     *
     * Returns empty collection if there is no matching elements.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements find(XMLSelector selector);

    /**
     * Returns list of direct children
     *
     * Returns empty collection element does not have children.
     *
     * @return XMLElements
     */
    XMLElements children();

    /**
     * Returns list of direct children filtered by provided selector.
     *
     * This method does not search recursively.
     * If you want to search recursively for any matching element, lets use {@link #find(String)}
     *
     * Returns empty collection if there is no matching children.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements children(String selector);

    /**
     * Returns list of direct children filtered by provided predicate.
     *
     * This method does not search recursively.
     * If you want to search recursively for any matching element,
     * and your selector is XMLSelector, you can use {@link #find(XMLSelector)}.
     *
     * There is no way to recursively scan by predicate.
     *
     * Returns empty collection if there is no matching children.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements children(Predicate<XMLContext> selector);

    /**
     * Returns true, if this element has any children
     *
     * @return boolean
     */
    boolean has();

    /**
     * Returns true, if this element has any direct child matching selector
     *
     * There is no method to make recursive check, but you can call {@code find(selector).has()}.
     *
     * @param selector selector
     * @return boolean
     */
    boolean has(String selector);

    /**
     * Returns true, if this element has any direct child matching predicate
     *
     * There is no method to make recursive check, but you can call {@code find(selector).has()}.
     *
     * @param selector selector
     * @return boolean
     */
    boolean has(Predicate<XMLContext> selector);

    /**
     * Returns next sibling of element
     * Returns null if there is no next sibling.
     *
     * @return XMLElement?
     */
    XMLElement next();

    /**
     * Returns all next siblings matched by provided selector.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements next(String selector);

    /**
     * Returns all next siblings accepted by provided predicate.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements next(Predicate<XMLContext> selector);

    /**
     * Returns previous sibling of element
     * Returns null if there is no previous sibling.
     *
     * @return XMLElement?
     */
    XMLElement prev();

    /**
     * Returns all previous siblings matched by provided selector.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements prev(String selector);

    /**
     * Returns all previous siblings accepted by provided predicate.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements prev(Predicate<XMLContext> selector);

    /**
     * Returns direct parent of this element
     * Returns null if there is no parent element.
     *
     * @return XMLElement?
     */
    XMLElement parent();

    /**
     * Returns parent chain for this element
     *
     * Elements are ordered from direct parent to root parent.
     *
     * @return XMLElements
     */
    XMLElements parents();

    /**
     * Returns those elements from parents chain which match selector.
     *
     * Elements are ordered from direct parent to root parent.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements parents(String selector);

    /**
     * Returns those elements from parents chain which are accepted by predicate.
     *
     * Elements are ordered from direct parent to root parent.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements parents(Predicate<XMLContext> selector);

    /**
     * Returns all siblings of element.
     *
     * @return XMLElements
     */
    XMLElements siblings();

    /**
     * Returns all siblings matched by provided selector.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements siblings(String selector);

    /**
     * Returns all siblings accepted by provided predicate.
     *
     * @param selector selector
     * @return XMLElements
     */
    XMLElements siblings(Predicate<XMLContext> selector);


    /**
     * Normalization merges all text nodes and removes empty text nodes.
     *
     * @return this
     */
    XMLElement normalize();

    /**
     * Removes all child nodes.
     *
     * @return this
     */
    XMLElement removeAll();

    /**
     * Inserts provided element before this one. That means: "element" will be previous sibling.
     *
     * @param element element
     * @return this
     */
    XMLElement before(XMLElement element);

    /**
     * Inserts provided element after this one. That means: "element" will be next sibling.
     *
     * @param element element
     * @return this
     */
    XMLElement after(XMLElement element);

    /**
     * Inserts provided element into this one, before any children nodes. That means, as first child.
     *
     * @param element element
     * @return this
     */
    XMLElement prepend(XMLElement element);

    /**
     * Inserts provided element into this one, after any children nodes. That means, as last child.
     *
     * @param element element
     * @return this
     */
    XMLElement append(XMLElement element);

    /**
     * Replaces this element by provided one.
     * That means: this element will be removed from document, and provided one will be inserted instead.
     *
     * Returns this element in detached state.
     *
     * @param element element
     * @return this
     */
    XMLElement replace(XMLElement element);

    /**
     * Changes name of this element.
     * Operation is supported only for attributes and tags.
     *
     * Throws exception if operation is unsupported.
     *
     * @param tagname tagname
     * @return this
     */
    XMLElement rename(String tagname);

    /**
     * Removes this element from document.
     *
     * Returns this element in detached state.
     *
     * @return this
     */
    XMLElement remove();

    /**
     * Wraps this element by provided tag.
     * That means: first it will be replaced by new "tagname" and then will be inserted into this "tagname"
     *
     * Returns newly created wrapper element.
     *
     * @param tagname tagname
     * @return XMLElement
     */
    XMLElement wrap(String tagname);

    /**
     * Removes from document enclosing tag with all children and puts into its place only this element.
     *
     * @return this
     */
    XMLElement unwrap();

    /**
     * Changes content of this node.
     *
     * If this node is ELEMENT, it replaces its children by provided element.
     * Otherwise, it reads text content from provided element and assigns it.
     *
     * @param value value
     * @return this
     */
    XMLElement content(XMLElement value);

    /**
     * Changes content of this node.
     *
     * If this node is ELEMENT, parses provided markup into XML nodes and replaces current children by those new nodes.
     * Otherwise, it just assigns provided text value to element.
     *
     * @param value value
     * @return this
     */
    XMLElement content(String value);

    /**
     * Changes text content of this node.
     *
     * If this node is ELEMENT, it removes all children and puts new TEXT element into it.
     * Otherwise, it just assigns provided text to element.
     *
     * @param value value
     * @return this
     */
    XMLElement text(String value);

    /**
     * Executes provided transformer against document subtree represented by this element and
     * creates new output document. Returns new tree generated by transformer
     *
     * @param transformer transformer
     * @return XMLElement
     * @throws XMLException on error
     */
    XMLElement apply(Transformer transformer) throws XMLException;

    /**
     * Executes provided transformer against document subtree represented by this element and
     * creates new output document. Returns new tree generated by transformer
     *
     * Assumes that provided argument is correctly loaded XSLT document and uses it to create XML transformation.
     *
     * @param transformer transformer
     * @return XMLElement
     * @throws XMLException on error
     */
    XMLElement apply(XMLElement transformer) throws XMLException;

    /**
     * Executes provided transformer against document subtree represented by this element and
     * stores generated result into "output" object.
     *
     * @param transformer transformer
     * @param output output
     * @param <R> R
     * @return output
     * @throws XMLException on error
     */
    <R extends Result> R apply(Transformer transformer, R output) throws XMLException;

    /**
     * Executes provided transformer against document subtree represented by this element and
     * stores generated result into "output" object.
     *
     * Assumes that provided argument is correctly loaded XSLT document and uses it to create XML transformation.
     *
     * @param transformer transformer
     * @param output output
     * @param <R> R
     * @return output
     * @throws XMLException on error
     */
    <R extends Result> R apply(XMLElement transformer, R output) throws XMLException;

    /**
     * Creates new XMLWriter associated with this element.
     * Writer allows you to configure XML generator and serialize this element.
     *
     * For example, you can enable pretty printing for returned writer.
     *
     * @return XMLWriter
     */
    XMLWriter writer();

}
