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

import java.util.AbstractList;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.function.Function;
import java.util.function.Predicate;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import net.ranides.assira.collection.iterators.ForwardIterator;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.query.CQueryBuilder;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.text.StringUtils;
import net.ranides.assira.xml.*;
import org.w3c.dom.DOMException;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class W3Element extends CElement implements XMLElement {
    
    private static final IClass<?> TYPE = IClass.typeinfo(String.class);
    
    private static final XMLSelector NORMALIZE = XMLSelector.compile("? //text()[normalize-space(.) = '']");
    
    protected W3Element(Node node) {
        super(node);
    }
    
    public static W3Element wrap(Node node) {
        return new W3Element(node);
    }
    
    @Override
    public void set(String value) {
        content(value);
    }

    @Override
    public String get() {
        return content();
    }

    @Override
    public IClass<?> type() {
        return TYPE;
    }

    @Override
    public XMLType nodetype() {
        return XMLType.valueOf(node.getNodeType());
    }

    @Override
    public XMLDocument document() {
        return new W3Document($root());
    }

    @Override
    public XMLElement root() {
        return new W3Element($root()).children(XMLType.ELEMENT).first();
    }
    
    private Document $root() {
        return (Document)ValueUtils.or(node.getOwnerDocument(), node);
    }
    
    @Override
    public Node node() {
        return node;
    }

    @Override
    public String name() {
        return node.getNodeName();
    }

    @Override
    public String xpath() {
        Deque<String> list = new ArrayDeque<>();
        W3Element item;
        if(node instanceof Attr) {
            list.addFirst("@" + name());
            item = $element(((Attr)node).getOwnerElement());
        } else {
            list.addFirst(iname());
            item = getParentNode();
        }
        Document root = document().node();
        for(; item.node!=root; item=item.getParentNode()) {
            list.addFirst(item.iname());
        }
        return "/"+StringUtils.join(list,"/");
    }
    
    private String iname() {
        int index = prev(name()).size();
        return 1 == index ? name() : name() + "[" + index + "]";
    }
    
    @Override
    public String content() {
        return node.getNodeValue();
    }

    @Override
    public String text() {
        return node.getTextContent();
    }

    @Override
    public XMLElements attrs() {
        NamedNodeMap nnmap = node.getAttributes();
        return null == nnmap ? CElements.EMPTY : new W3Attributes(node.getOwnerDocument(), nnmap);
    }

    @Override
    public XMLElement attr(String name) {
        return attrs().first(name);
    }

    @Override
    public XMLElements find(String selector) {
        return find(XMLSelector.compile(selector));
    }

    @Override
    public XMLElements find(XMLSelector selector) {
        return new CContext().select(this, selector);
    }

    @Override
    public XMLElements children() {
        return new CElements($stream(node.getChildNodes()));
    }

    @Override
    public XMLElements children(String selector) {
        return children(XMLSelector.compile(selector));
    }

    @Override
    public XMLElements children(Predicate<XMLContext> selector) {
        CContext c = new CContext();
        return new CElements($stream(node.getChildNodes()).filter(n -> c.test(n, selector)));
    }

    @Override
    public boolean has() {
        return children().matches();
    }

    @Override
    public boolean has(String selector) {
        return children(selector).matches();
    }

    @Override
    public boolean has(Predicate<XMLContext> selector) {
        return children(selector).matches();
    }
    
    @Override
    public XMLElement next() {
        return $element(node.getNextSibling());
    }

    @Override
    public XMLElements next(String selector) {
        return next(XMLSelector.compile(selector));
    }
    
    @Override
    public XMLElements next(Predicate<XMLContext> selector) {
        CContext c = new CContext();
        return new CElements($stream(node, Node::getNextSibling).filter(n -> c.test(n, selector)));
    }

    @Override
    public XMLElement prev() {
        return $element(node.getPreviousSibling());
    }

    @Override
    public XMLElements prev(String selector) {
        return prev(XMLSelector.compile(selector));
    }

    @Override
    public XMLElements prev(Predicate<XMLContext> selector) {
        CContext c = new CContext();
        return new CElements($stream(node, Node::getPreviousSibling).filter(n -> c.test(n, selector)));
    }

    @Override
    public XMLElement parent() {
        if(node instanceof Attr) {
            return $element(((Attr)node).getOwnerElement());
        } else {
            return $element(node.getParentNode());
        }
    }

    @Override
    public XMLElements parents() {
        return new CElements(CQueryBuilder.fromIterable(()->ForwardIterator.produce(parent(), XMLElement::parent)));
    }

    @Override
    public XMLElements parents(String selector) {
        return parents(XMLSelector.compile(selector));
    }

    @Override
    public XMLElements parents(Predicate<XMLContext> selector) {
        CContext c = new CContext();
        return new CElements($stream(parent(), XMLElement::parent).filter(n -> c.test(n, selector)));
    }

    @Override
    public XMLElements siblings() {
        return new CElements(new CQueryBuilder<Node>()
            .addIterable(() -> ForwardIterator.produce(node.getPreviousSibling(), Node::getPreviousSibling))
            .addIterable(() -> ForwardIterator.produce(node.getNextSibling(), Node::getNextSibling))
            .result()
            .map(W3Element::new)
        );
    }

    @Override
    public XMLElements siblings(String selector) {
        return siblings(XMLSelector.compile(selector));
    }

    @Override
    public XMLElements siblings(Predicate<XMLContext> selector) {
        return new CElements(new CQueryBuilder<XMLElement>()
            .addIterable(next(selector))
            .addIterable(prev(selector))
            .result()
        );
    }

    @Override
    public XMLElement before(XMLElement element) {
        node.getParentNode().insertBefore(adopt(element), node);
        return this;
    }

    @Override
    public XMLElement after(XMLElement element) {
        Node parent = node.getParentNode();
        Node next = node.getNextSibling();
        if(null != next) {
            parent.insertBefore(adopt(element), next);
        } else {
            parent.appendChild(adopt(element));
        }
        return this;
    }

    @Override
    public XMLElement prepend(XMLElement element) {
        Node first = node.getFirstChild();
        if(null != first) {
            node.insertBefore(adopt(element), first);
        } else {
            node.appendChild(adopt(element));
        }
        return this;
    }

    @Override
    public XMLElement append(XMLElement element) {
        node.appendChild(adopt(element));
        return this;
    }

    @Override
    public XMLElement replace(XMLElement element) {
        node.getParentNode().replaceChild(adopt(element), node);
        return this;
    }

    @Override
    public XMLElement rename(String tagname) {
        node.getOwnerDocument().renameNode(node, node.getNamespaceURI(), tagname);
        return this;
    }

    @Override
    public XMLElement remove() {
        node.getParentNode().removeChild(node);
        return this;
    }

    @Override
    public XMLElement wrap(String tagname) {
        Node wrapper = node.getOwnerDocument().createElement(tagname);
        node.getParentNode().replaceChild(wrapper, node);
        wrapper.appendChild(node);
        return new W3Element(wrapper);
    }

    @Override
    public XMLElement unwrap() {
        Node wrapper = node.getParentNode();
        Node parent = wrapper.getParentNode();
        
        wrapper.removeChild(node);
        parent.insertBefore(node, wrapper);
        if(!wrapper.hasChildNodes()) {
            parent.removeChild(wrapper);
        }
        return this;
    }

    @Override
    public XMLElement content(String value) {
        if(null != node.getNodeValue()) {
            node.setNodeValue(value);
        } else {
            clear();
            W3CQuery.append(this, value);
        }
        return this;
    }

    @Override
    public XMLElement content(XMLElement value) {
        if(null != node.getNodeValue()) {
            node.setNodeValue(value.content());
        } else {
            clear();
            append(value);
        }
        return this;
    }
    
    

    @Override
    public XMLElement text(String value) {
        node.setTextContent(value);
        return this;
    }

    @Override
    public XMLElement clear() {
        while(node.hasChildNodes()) {
            node.removeChild(node.getFirstChild());
        }
        return this;
    }

    
    
    @Override
    public XMLElement normalize() {
        node.normalize();
        
        find(NORMALIZE).remove();
        return this;
    }

    @Override
    public XMLElement apply(XMLElement transformer) throws XMLException {
        return apply($tr(transformer));
    }
    
    @Override
    public XMLElement apply(Transformer transformer) throws XMLException {
        try {
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            apply(transformer, new DOMResult(doc));
            return new W3Element(doc);
        } catch(ParserConfigurationException cause) {
            throw new XMLException(cause);
        }
    }
    
    @Override
    public <R extends Result> R apply(XMLElement transformer, R output) throws XMLException {
        return apply($tr(transformer), output);
    }
    

    @Override
    public <R extends Result> R apply(Transformer transformer, R output) throws XMLException {
        try {
            transformer.transform(new DOMSource(node), output);
            return output;
        } catch(TransformerException cause) {
            throw new XMLException(cause);
        }
    }

    @Override
    public XMLWriter writer() {
        return new W3Writer(this);
    }
    
    private Node adopt(XMLElement element) throws DOMException {
        return document().node().adoptNode(element.node().cloneNode(true));
    }

    private static Transformer $tr(XMLElement transformer) {
        try {
            return TransformerFactory.newInstance().newTransformer(new DOMSource(transformer.node()));
        } catch(TransformerFactoryConfigurationError | TransformerConfigurationException cause) {
            throw new XMLException(cause);
        }
    }
    
    static W3Element $element(Node node) {
        return null == node ? null : new W3Element(node);
    }
    
    static CQuery<XMLElement> $stream(NodeList nodes) {
        return $stream(new NodeCollection(nodes));
    }
    
    static CQuery<XMLElement> $stream(Collection<XMLElement> nodes) {
        return CQueryBuilder.fromCollection(nodes);
    }
    
    static CQuery<XMLElement> $stream(XMLElement current, Function<XMLElement,XMLElement> next) {
        return CQueryBuilder.fromIterable(() -> ForwardIterator.produce(current, next));
    }
    
    static CQuery<XMLElement> $stream(Node current, Function<Node,Node> next) {
        return CQueryBuilder.fromIterable(() -> ForwardIterator.produce(current, next)).map(W3Element::new);
    }
    
    private static final class NodeCollection extends AbstractList<XMLElement> {
        
        private final NodeList nodes;

        public NodeCollection(NodeList nodes) {
            this.nodes = nodes;
        }

        @Override
        public XMLElement get(int index) {
            Node node = nodes.item(index);
            if(null == node) {
                throw new IndexOutOfBoundsException();
            }
            return new W3Element(node);
        }

        @Override
        public int size() {
            return nodes.getLength();
        }

    }
    
}
