/*
 * @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.List;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import net.ranides.assira.xml.XMLContext;
import net.ranides.assira.xml.XMLElement;
import net.ranides.assira.xml.XMLElements;
import net.ranides.assira.xml.XMLException;
import net.ranides.assira.xml.XMLSelector;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import se.fishtank.css.selectors.Selectors;
import se.fishtank.css.selectors.matching.SelectorMatcher;
import se.fishtank.css.selectors.parser.ParserException;
import se.fishtank.css.selectors.selector.Selector;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
public final class W3Selectors {
    
    private W3Selectors() {
        /* utility class */
    }

    public static XMLSelector compile(String selector) {
        if(isxpath(selector)) {
            return xpath(selector.substring(2));
        }
        if(isname(selector)) {
            return new Name(selector);
        }
        return css(selector);
    }
    
    public static XMLSelector css(String selector) {
        return new CSS(selector);
    }
    
    public static XMLSelector xpath(String selector) {
        return new XPath(selector);
    }
    
    private static boolean isxpath(String selector) {
        return selector.startsWith("? ");
    }
    
    private static boolean isname(String selector) {
        if(selector.trim().equals("*")) {
            return true;
        }
        for(int i=0,n=selector.length(); i<n; i++) {
            if( !isname(selector.charAt(i))) {
                return false;
            }
        }
        return true;
    }
    
    private static boolean isname(char cn) {
        return (cn=='-') || (cn>='a' && cn<='z') || (cn>='A' && cn<='Z');
    }
    
    static final class Name implements XMLSelector {
        
        private final String tag;

        public Name(String name) {
            this.tag = name;
        }

        public String name() {
            return tag;
        }
                
        @Override
        public XMLElements apply(XMLContext c) {
            Node node = c.node().node();
            if(node instanceof Element) {
                Element element = (Element)node;
                return new CElements(W3Element.$stream(element.getElementsByTagName(tag)));
            }
            if(node instanceof Document) {
                Document document = (Document)node;
                return new CElements(W3Element.$stream(document.getElementsByTagName(tag)));
            }
            return XMLElement.$false;
        }

        @Override
        public boolean test(XMLContext c) {
            return tag.equals(c.node().name());
        }
    
    }
    
    static final class CSS implements XMLSelector {
        
        private static final SelectorMatcher<W3Element> MATCHER = new SelectorMatcher<>();
        
        private final List<Selector> scanner;
        
        public CSS(String selector) {
            try {
                scanner = Selectors.parse(selector);
            } catch(ParserException cause) {
                throw new XMLException("Invalid CSS expression", cause);
            }
        }

        @Override
        public XMLElements apply(XMLContext c) {
            return new CElements(new Selectors<>((W3Element)c.node()).querySelectorAll(scanner));
        }

        @Override
        public boolean test(XMLContext c) {
            return MATCHER.matchesSelectors(scanner, (W3Element)c.node());
        }
        
    }
    
    static final class XPath implements XMLSelector {
        
        private final XPathExpression xpe;

        public XPath(String query) {
            try {
                xpe = XPathFactory.newInstance().newXPath().compile(query);
            } catch(XPathExpressionException cause) {
                throw new XMLException(cause);
            }
        }

        @Override
        public XMLElements apply(XMLContext context) {
            try {
                NodeList nodes = (NodeList)xpe.evaluate(context.node().node(), XPathConstants.NODESET);
                return new CElements(W3Element.$stream(nodes));
            } catch (XPathExpressionException cause) {
                throw new XMLException(cause);
            }
        }

        @Override
        public boolean test(XMLContext context) {
            try {
                NodeList nodes = (NodeList)xpe.evaluate(context.node().node(), XPathConstants.NODESET);
                return null != nodes && nodes.getLength()>0;
            } catch (XPathExpressionException cause) {
                throw new XMLException(cause);
            }
        }
        
    }
   
}
