/*******************************************************************************
 * Copyright 2012-present Pixate, Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package com.pixate.freestyle.styling.adapters;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

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

import com.pixate.freestyle.annotations.PXDocElement;
import com.pixate.freestyle.styling.PXDeclaration;
import com.pixate.freestyle.styling.PXRuleSet;
import com.pixate.freestyle.styling.stylers.PXStyler;
import com.pixate.freestyle.styling.stylers.PXStylerContext;

@PXDocElement(hide=true)
public class PXDOMStyleAdapter extends PXStyleAdapter {
    private static PXDOMStyleAdapter sInstance;

    private PXDOMStyleAdapter() {
    }

    @Override
    protected List<PXStyler> createStylers() {
        return Collections.emptyList();

    }

    @Override
    public boolean updateStyle(List<PXRuleSet> ruleSets, List<PXStylerContext> contexts) {
        for (int i = 0; i < ruleSets.size(); i++) {
            PXStylerContext context = contexts.get(i);
            PXRuleSet ruleSet = ruleSets.get(i);
            Node node = (Node) context.getStyleable();
            Document ownerDocument = node.getOwnerDocument();
            NamedNodeMap attributes = node.getAttributes();
            for (PXDeclaration declaration : ruleSet.getDeclarations()) {
                String name = declaration.getName();
                String value = declaration.getStringValue();

                // Set the node's attribute
                Node attNode = ownerDocument.createAttribute(name);
                attNode.setNodeValue(value);
                attributes.setNamedItem(attNode);
            }
        }
        return true;
    }

    @Override
    public String getElementName(Object object) {
        return ((Node) object).getNodeName();
    }

    @Override
    public String getStyleId(Object object) {
        return getAttributeValue(object, "id");
    }

    @Override
    public String getStyleClass(Object object) {
        return getAttributeValue(object, "class");
    }

    @Override
    public int getIndexInParent(Object styleable) {
        Node node = (Node) styleable;
        Node parentNode = node.getParentNode();
        if (parentNode != null) {
            NodeList siblings = parentNode.getChildNodes();
            // find the node's index
            for (int i = 0; i < siblings.getLength(); i++) {
                Node sibling = siblings.item(i);
                if (sibling == node) {
                    return i;
                }
            }
        }
        return -1;
    }

    @Override
    public String getElementNamespace(Object styleable) {
        return ((Node) styleable).getNamespaceURI();
    }

    @Override
    public String getAttributeValue(Object styleable, String attribute) {
        Node node = (Node) styleable;
        NamedNodeMap attributes = node.getAttributes();
        String result = null;
        if (attributes != null) {
            Node idAttr = attributes.getNamedItem(attribute);
            if (idAttr != null) {
                result = idAttr.getNodeValue();
            }
        }
        return result;
    }

    @Override
    public String getAttributeValue(Object styleable, String attributeName, String namespaceURI) {
        Node node = (Node) styleable;
        if (node.hasAttributes()) {
            NamedNodeMap attributes = node.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attr = attributes.item(i);
                if (attr.getLocalName().equals(attributeName)) {
                    // check for any namespace prefix (TODO - we check for the
                    // attribute prefix and for the node's namespace URI here.
                    // There is a chance that only one way is needed, but for
                    // now we keep both)
                    if (namespaceURI == null) {
                        return attr.getNamespaceURI() == null ? attr.getNodeValue() : null;
                    }
                    if ("*".equals(namespaceURI) || namespaceURI.equals(attr.getNamespaceURI())) {
                        return attr.getNodeValue();
                    }
                }
            }
        }
        return null;
    }

    @Override
    public Object getSiblingAt(Object styleable, int offset) {
        return getSiblingAt((Node) styleable, offset, true);
    }

    /**
     * Returns the element children of the given node (Note: only the
     * ELEMENT_NODEs will be returned)
     * 
     * @param node
     * @return
     */
    @Override
    public List<Object> getElementChildren(Object styleable) {
        return getElementChildren((Node) styleable, true);
    }

    @Override
    public int getChildCount(Object styleable) {
        List<Object> children = getElementChildren(styleable);
        return children == null ? 0 : children.size();
    }

    @Override
    public Object getParent(Object styleable) {
        return ((Node) styleable).getParentNode();
    }

    @Override
    public boolean isSupportingStylers() {
        return false;
    }

    // Private

    private Object getSiblingAt(Node node, int offset, boolean skipNonElement) {
        Object result = null;
        int indexInParent = getIndexInParent(node);
        if (indexInParent != -1) {
            int siblingIndex = indexInParent + offset;
            NodeList siblings = node.getParentNode().getChildNodes();
            // Get the sibling at the offset. In case it's out of
            // bounds, return null.
            while (result == null) {
                if (siblingIndex >= 0 && siblingIndex < siblings.getLength()) {
                    Node sibling = siblings.item(siblingIndex);
                    if (skipNonElement && sibling.getNodeType() != Document.ELEMENT_NODE) {
                        // skip the non-element node
                        siblingIndex += (offset < 0) ? -1 : 1;
                    } else {
                        result = sibling;
                        break;
                    }
                } else {
                    // out of bounds
                    break;
                }
            }
        }
        return result;
    }

    /**
     * Returns the children of the given node. In case 'onlyElements' is passed,
     * only the ELEMENT_NODEs will be returned.
     * 
     * @param node
     * @param onlyElements
     * @return
     */
    private List<Object> getElementChildren(Node node, boolean onlyElements) {
        NodeList childNodes = node.getChildNodes();
        if (childNodes == null) {
            return Collections.emptyList();
        }
        List<Object> children = new ArrayList<Object>(childNodes.getLength());
        for (int i = 0; i < childNodes.getLength(); i++) {
            short nodeType = childNodes.item(i).getNodeType();
            if (!onlyElements
                    || (onlyElements && nodeType != Document.COMMENT_NODE && nodeType != Document.PROCESSING_INSTRUCTION_NODE)) {
                children.add(childNodes.item(i));
            }
        }
        return children;
    }

    // Statics

    public static PXDOMStyleAdapter getInstance() {
        synchronized (PXDOMStyleAdapter.class) {
            if (sInstance == null) {
                sInstance = new PXDOMStyleAdapter();
            }
        }

        return sInstance;
    }

}