/*
 * Copyright 2004-2005 The Trix Development Team.
 *
 * 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 org.trix.cuery;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.trix.cuery.css.AbstractCSSConsumer;
import org.trix.cuery.css.CSSPipe;
import org.trix.cuery.filter.AcceptFilter;
import org.trix.cuery.filter.Filter;
import org.trix.cuery.parser.CueryParser;
import org.trix.cuery.property.CascadableProperty;
import org.trix.cuery.property.Property;
import org.trix.cuery.property.PropertyDescription;
import org.trix.cuery.property.SimpleProperty;
import org.trix.cuery.sac.LocatorImpl;
import org.trix.cuery.util.CSSUtil;
import org.trix.cuery.util.DOMUtil;
import org.trix.cuery.util.I18nUtil;
import org.trix.cuery.util.URIUtil;
import org.trix.cuery.value.CSSValue;

import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.Locator;
import org.w3c.css.sac.SACMediaList;
import org.w3c.css.sac.SelectorList;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.stylesheets.StyleSheet;
import org.w3c.dom.stylesheets.StyleSheetList;

import org.xml.sax.SAXException;

/**
 * <p>
 * This class weaves information on the property sepecified with CSS for DOM document.
 * </p>
 * <p>
 * Once a user agent has parsed a document and constructed a document tree, it must assign, for
 * every element in the tree, a value to every property that applies to the target media type. The
 * final value of a property is the result of a three-step calculation: the value is determined
 * through specification (the "specified value"), then resolved into an absolute value if necessary
 * (the "computed value"), and finally transformed according to the limitations of the local
 * environment (the "actual value").
 * </p>
 * <p>
 * CSSWeaver calculates not the actual value but the computed value. So you should decide the actual
 * value even for the runtime environment.
 * </p>
 * 
 * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
 * @version $ Id: CSSWeaver.java,v 1.13 2006/03/24 16:26:08 Teletha Exp $
 */
public class CSSWeaver {

    /** The css parser. */
    private static CueryParser parser = new CueryParser();

    /** The stylesheet list. */
    private List agents = new ArrayList();

    /** The stylesheet list. */
    private List authors = new ArrayList();

    /** The stylesheet list. */
    private List users = new ArrayList();

    /** The uri resolver. */
    private URIResolver resolver;

    /** The flag to aware xml-stylesheet pi. */
    private boolean awareXMLStylesheet = false;

    /** The flag to aware DOM node mutation. */
    private boolean awareDOMMutarion = false;

    /**
     * Add styles as an agent's stylesheet.
     * 
     * @param source A stylesheet source.
     * @throws IOException If this uri is invalid.
     */
    public void addAgentStylesheet(InputSource source) throws IOException {
        addAgentStylesheet(source, null);
    }

    /**
     * Add styles as an agent's stylesheet with the css pipe for events hooking.
     * 
     * @param source A stylesheet source.
     * @param pipe A css pipe to hook.
     * @throws IOException If this uri is invalid.
     */
    public void addAgentStylesheet(InputSource source, CSSPipe pipe) throws IOException {
        // assert null
        if (source == null) {
            return;
        }

        StylesheetInfo info = new StylesheetInfo();
        info.pipe = pipe;
        info.source = source;

        agents.add(info);
    }

    /**
     * Add styles as an author's stylesheet.
     * 
     * @param source A stylesheet source.
     * @throws IOException If this uri is invalid.
     */
    public void addAuthorStylesheet(InputSource source) throws IOException {
        addAuthorStylesheet(source, null);
    }

    /**
     * Add styles as an author's stylesheet with the css pipe for events hooking.
     * 
     * @param source A stylesheet source.
     * @param pipe A css pipe to hook.
     * @throws IOException If this uri is invalid.
     */
    public void addAuthorStylesheet(InputSource source, CSSPipe pipe) throws IOException {
        // assert null
        if (source == null) {
            return;
        }

        StylesheetInfo info = new StylesheetInfo();
        info.pipe = pipe;
        info.source = source;

        authors.add(info);
    }

    /**
     * Add styles as an user's stylesheet.
     * 
     * @param source A stylesheet source.
     * @throws IOException If this uri is invalid.
     */
    public void addUserStylesheet(InputSource source) throws IOException {
        addUserStylesheet(source, null);
    }

    /**
     * Add styles as an user's stylesheet with the css pipe for events hooking.
     * 
     * @param source A stylesheet source.
     * @param pipe A css pipe to hook.
     * @throws IOException If this uri is invalid.
     */
    public void addUserStylesheet(InputSource source, CSSPipe pipe) throws IOException {
        // assert null
        if (source == null) {
            return;
        }

        StylesheetInfo info = new StylesheetInfo();
        info.pipe = pipe;
        info.source = source;

        users.add(info);
    }

    /**
     * Apply styles to the document file.
     * 
     * @param path A path to the xml document file.
     * @return A styled document.
     * @throws IOException If this document has I/O error.
     */
    public Document apply(String path) throws IOException {
        return apply(new File(path));
    }

    /**
     * Apply styles to the document file.
     * 
     * @param file A xml document file.
     * @return A styled document.
     * @throws IOException If this document has I/O error.
     */
    public Document apply(File file) throws IOException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);

        try {
            DocumentBuilder builder = factory.newDocumentBuilder();

            return apply(builder.parse(file));
        } catch (ParserConfigurationException e) {
            throw new IOException(e.getMessage());
        } catch (SAXException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Apply styles to the document.
     * 
     * @param document A target document.
     * @return A styled document.
     */
    public Document apply(Document document) {
        // assert null
        if (document == null) {
            throw new IllegalArgumentException("The input document is null.");
        }

        // parse all stylesheets
        PropertyProcessor processor = new PropertyProcessor();

        // agent stylesheet
        processor.origin = CSS.ORIGIN_AGENT;

        for (int i = 0; i < agents.size(); i++) {
            StylesheetInfo info = (StylesheetInfo) agents.get(i);

            if (info.pipe == null) {
                parser.setDocumentHandler(processor);
            } else {
                info.pipe.setConsumer(processor);
                parser.setDocumentHandler(info.pipe);
            }

            try {
                parser.parseStyleSheet(info.source);
            } catch (CSSException e) {
                System.out.println(e);
            } catch (IOException e) {
                System.out.println(e);
            }
        }

        // author stylesheet
        processor.origin = CSS.ORIGIN_AUTHOR;

        for (int i = 0; i < authors.size(); i++) {
            StylesheetInfo info = (StylesheetInfo) authors.get(i);

            if (info.pipe == null) {
                parser.setDocumentHandler(processor);
            } else {
                info.pipe.setConsumer(processor);
                parser.setDocumentHandler(info.pipe);
            }

            try {
                parser.parseStyleSheet(info.source);
            } catch (CSSException e) {
                System.out.println(e);
            } catch (IOException e) {
                System.out.println(e);
            }
        }

        // aware linked inline styles
        if (awareXMLStylesheet) {
            StyleSheetList stylesheets = DOMUtil.getStylesheets(document);
            parser.setDocumentHandler(processor);

            for (int i = 0; i < stylesheets.getLength(); i++) {
                StyleSheet stylesheet = stylesheets.item(i);

                // check mime-type
                if (!stylesheet.getType().equals("text/css")) {
                    continue;
                }

                try {
                    // resolve uri and parse it
                    InputSource source = getResolver().resolve(stylesheet.getHref(), document.getDocumentURI());
                    parser.parseStyleSheet(source);
                } catch (IOException e) {
                    System.out.println(e);
                    // do nothing
                } catch (CSSException e) {
                    System.out.println(e);
                    // do nothing
                }
            }
        }

        // author stylesheet
        processor.origin = CSS.ORIGIN_USER;

        for (int i = 0; i < users.size(); i++) {
            StylesheetInfo info = (StylesheetInfo) users.get(i);

            if (info.pipe == null) {
                parser.setDocumentHandler(processor);
            } else {
                info.pipe.setConsumer(processor);
                parser.setDocumentHandler(info.pipe);
            }

            try {
                parser.parseStyleSheet(info.source);
            } catch (CSSException e) {
                System.out.println(e);
            } catch (IOException e) {
                System.out.println(e);
            }
        }

        // create query for this document and weave registered styles
        CSSQuery query = new CSSQuery(document);
        processor.weave(query);

        // DOM Mutation
        if (awareDOMMutarion) {
            // check feature
            if (document.getImplementation().hasFeature("Events", "2.0")) {
                EventTarget target = (EventTarget) document;
                EventListener listener = processor;

                // register
                target.addEventListener("DOMAttrModified", listener, false);
                target.addEventListener("DOMNodeInserted", listener, true);
                target.addEventListener("DOMNodeRemoved", listener, false);
                target.addEventListener("DOMCharacterDataModified", listener, false);
                target.addEventListener(StyleContext.UI_PSEUDO_CLASS_ASSING_EVENT, listener, false);
                target.addEventListener(StyleContext.UI_PSEUDO_CLASS_DEPRIVE_EVENT, listener, false);
            } else { // error
                Locator locator = new LocatorImpl(document.getDocumentURI(), 0, 0);
                processor.error(new CSSParseException(I18nUtil.getText("weaver.domMutation"), locator));
            }
        }
        return document;
    }

    /**
     * Set a flag whether xml-stylesheet processing instruction is aware or not.
     * 
     * @param aware A flag.
     */
    public void awareXMLStylesheet(boolean aware) {
        this.awareXMLStylesheet = aware;
    }

    /**
     * Set a flag whether dom node mutation is aware or not. If this setting is true, whole document
     * is reparsed at each DOM node modifications. So you shouldn't use this method when the
     * processing speed is important.
     * 
     * @param aware A flag.
     */
    public void awareDOMMutation(boolean aware) {
        this.awareDOMMutarion = aware;
    }

    /**
     * Set URIResolver.
     * 
     * @param resolver A uri resolver.
     */
    public void setURIResolver(URIResolver resolver) {
        // assert null
        if (resolver == null) {
            return;
        }
        this.resolver = resolver;
    }

    /**
     * Return URIResolver.
     * 
     * @return A URIResolver.
     */
    private URIResolver getResolver() {
        // check null
        if (resolver == null) {
            resolver = new SimpleResolver();
        }
        return resolver;
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: PropertyProcessor.java,v 1.1 2005/11/18 09:18:17 Teletha Exp $
     */
    protected final class PropertyProcessor extends AbstractCSSConsumer implements EventListener {

        /** The style rule list. */
        private List descriptions = new ArrayList();

        /** The uri. */
        private String uri;

        /** The origin. */
        private int origin = CSS.ORIGIN_AUTHOR;

        /** The current position. */
        private int position;

        /** The current poperty. */
        private SimpleProperty property;

        /**
         * Create PropertyProcessor instance.
         */
        public PropertyProcessor() {
            this.position = -1;
        }

        /**
         * Create PropertyProcessor instance.
         * 
         * @param parent A parent.
         */
        public PropertyProcessor(PropertyProcessor parent) {
            this.position = parent.position;
        }

        /**
         * @see org.trix.cuery.css.AbstractCSSConsumer#property(java.lang.String,
         *      org.w3c.css.sac.LexicalUnit, boolean)
         */
        public void property(String name, LexicalUnit value, boolean important) throws CSSException {
            property.setProperty(name, (CSSValue) value, important);
        }

        /**
         * @see org.trix.cuery.css.AbstractCSSConsumer#startSelector(org.w3c.css.sac.SelectorList)
         */
        public void startSelector(SelectorList selectorList) throws CSSException {
            property = new SimpleProperty();
            position++;
        }

        /**
         * @see org.trix.cuery.css.AbstractCSSConsumer#endSelector(org.w3c.css.sac.SelectorList)
         */
        public void endSelector(SelectorList selectorList) throws CSSException {
            for (int i = 0; i < selectorList.getLength(); i++) {
                PropertyDescription description = new PropertyDescription((Filter) selectorList.item(i), origin, position, property);
                descriptions.add(description);
            }
        }

        /**
         * @see org.trix.cuery.css.AbstractCSSConsumer#importStyle(java.lang.String,
         *      org.w3c.css.sac.SACMediaList, java.lang.String)
         */
        public void importStyle(String uri, SACMediaList mediaList, String defaultNamespaceURI) throws CSSException {
            InputSource source = getResolver().resolve(uri, this.uri);

            try {
                CueryParser parser = new CueryParser();
                parser.setDocumentHandler(this);
                parser.parseStyleSheet(source);
            } catch (CSSException e) {
                System.out.println(e);
            } catch (IOException e) {
                System.out.println(e);
            }
        }

        /**
         * @see org.w3c.dom.events.EventListener#handleEvent(org.w3c.dom.events.Event)
         */
        public void handleEvent(Event event) {
            Damager damager = null;
            String type = event.getType();
            Node target = (Node) event.getTarget();

            // Decided suitable damager
            if (type.equals("DOMAttrModified")) {
                damager = CSSWeaver.AttributeModifyDamager.SINGLETON;
            } else if (type.equals("DOMNodeInserted")) {
                switch (target.getNodeType()) {
                case Node.ELEMENT_NODE:
                    damager = CSSWeaver.InsertDamager.SINGLETON;
                    break;

                case Node.TEXT_NODE:
                    damager = CSSWeaver.TextModifyDamager.SINGLETON;
                    break;

                default:
                    return;
                }
            } else if (type.equals("DOMNodeRemoved")) {
                switch (target.getNodeType()) {
                case Node.ELEMENT_NODE:
                    damager = CSSWeaver.RemoveDamager.SINGLETON;
                    break;

                case Node.TEXT_NODE:
                    damager = CSSWeaver.TextModifyDamager.SINGLETON;
                    break;

                default:
                    return;
                }
            } else if (type.equals("DOMCharacterDataModified")) {
                damager = CSSWeaver.RemoveDamager.SINGLETON;
            } else if (type.equals(StyleContext.UI_PSEUDO_CLASS_ASSING_EVENT)) {
                damager = CSSWeaver.PseudoClassModifyDamager.SINGLETON;
            } else if (type.equals(StyleContext.UI_PSEUDO_CLASS_DEPRIVE_EVENT)) {
                damager = CSSWeaver.PseudoClassModifyDamager.SINGLETON;
            } else {
                return;
            }

            // Tally dameded region in the document and update each elements.
            Iterator iterator = damager.getDamegeRegion(target).iterator();

            while (iterator.hasNext()) {
                update((Element) iterator.next());
            }
        }

        /**
         * Actual style weaver.
         * 
         * @param query A css query.
         */
        private void weave(CSSQuery query) {
            // check null
            if (query == null) {
                return;
            }

            for (int i = 0; i < descriptions.size(); i++) {
                PropertyDescription description = (PropertyDescription) descriptions.get(i);
                Set elements = query.select(description.getFilter());
                Iterator iterator = elements.iterator();

                // weave style
                while (iterator.hasNext()) {
                    Element element = (Element) iterator.next();
                    CascadableProperty cascadable = (CascadableProperty) element.getUserData(StyleContext.KEY_PROPERTY);

                    if (cascadable == null) {
                        cascadable = new CascadableProperty(element);
                        element.setUserData(StyleContext.KEY_PROPERTY, cascadable, null);
                    }
                    cascadable.addProperty(description);
                }
            }
        }

        /**
         * Update the style imfomation of the given element.
         * 
         * @param element A target element.
         */
        private void update(Element element) {
            Property previous = StyleContext.getProperty(element);

            CascadableProperty property = null;

            if (previous instanceof CascadableProperty) {
                property = (CascadableProperty) previous;
                property.clear();
            }

            for (int i = 0; i < descriptions.size(); i++) {
                PropertyDescription description = (PropertyDescription) descriptions.get(i);

                if (description.getFilter().accept(element)) {
                    if (property == null) {
                        property = new CascadableProperty(element);
                        element.setUserData(StyleContext.KEY_PROPERTY, property, null);
                    }

                    // add property
                    property.addProperty(description);
                }
            }

            // fire event
            StyleContext context = StyleContext.getContext(element);

            if (!context.hasListener()) {
                return; // this element has no style event listener
            }

            // no diff
            if (property == null) {
                return;
            }
            context.notifyListener(property);
        }
    }

    /**
     * A style information damager is a strategy used by CSSWeaver to determine the region of the
     * DOM elements which must be rebuilt because of a document change. A style information damager
     * is assumed to be specific for a particular DOM event type. A style information damager is
     * expected to return a damage region.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: Damager.java,v 1.0 2005/11/17 21:54:15 Teletha Exp $
     */
    private static interface Damager {

        /**
         * Returns the damage in the DOM document caused by the given node change. The damage is
         * restricted to the specified partition for which the style information damager is
         * responsible. The damage may also depend on whether the document change also caused
         * changes of the document's partitioning.
         * 
         * @param node A target element which is cause of a damage.
         * @return A set of elements which are damaged.
         */
        Set getDamegeRegion(Node node);
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: InsertDamager.java,v 1.0 2005/11/17 22:20:56 Teletha Exp $
     */
    private static final class InsertDamager implements Damager {

        /** The singleton. */
        private static final InsertDamager SINGLETON = new InsertDamager();

        /**
         * @see org.trix.cuery.CSSWeaver.Damager#getDamegeRegion(Node)
         */
        public Set getDamegeRegion(Node node) {
            Set container = new HashSet();

            // parent
            Element parent = DOMUtil.getParentElement(node);

            if (parent != null) {
                container.add(parent);

                // sibling
                NodeList list = parent.getChildNodes();

                for (int i = 0; i < list.getLength(); i++) {
                    Node child = list.item(i);

                    if (child.getNodeType() == Node.ELEMENT_NODE) {
                        container.add((Element) child);
                    }
                }
            } else {
                container.add(node);
            }

            // descendant
            container.addAll(DOMUtil.retrieveElements(node, AcceptFilter.SINGLETON, true));
            return container;
        }
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: RemoveDamager,v 1.0 2005/11/18 3:45:59 Teletha Exp $
     */
    private static final class RemoveDamager implements Damager {

        /** The singleton. */
        private static final RemoveDamager SINGLETON = new RemoveDamager();

        /**
         * @see org.trix.cuery.CSSWeaver.Damager#getDamegeRegion(Node)
         */
        public Set getDamegeRegion(Node node) {
            Set container = new HashSet();
            DOMUtil.setVanishing(node);

            // parent
            Element parent = DOMUtil.getParentElement(node);

            if (parent != null) {
                container.add(parent);

                // sibling
                NodeList list = parent.getChildNodes();

                for (int i = 0; i < list.getLength(); i++) {
                    Node child = list.item(i);

                    if (child.getNodeType() == Node.ELEMENT_NODE && child != node) {
                        container.add((Element) child);
                    }
                }
            }
            return container;
        }
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: TextModifyDamager.java,v 1.0 2005/11/18 8:33:04 Teletha Exp $
     */
    private static final class TextModifyDamager implements Damager {

        /** The singleton. */
        private static final TextModifyDamager SINGLETON = new TextModifyDamager();

        /**
         * @see org.trix.cuery.CSSWeaver.Damager#getDamegeRegion(Node)
         */
        public Set getDamegeRegion(Node node) {
            Node parent = DOMUtil.getParentElement(node);

            if (parent == null) {
                return Collections.EMPTY_SET;
            }

            Set container = new HashSet();

            while (parent != null) {
                if (parent.getNodeType() == Node.ELEMENT_NODE) {
                    container.add(parent);
                }
                parent = parent.getNextSibling();
            }
            return container;
        }
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: AttributeModifyDamager.java,v 1.0 2005/11/18 8:33:04 Teletha Exp $
     */
    private static final class AttributeModifyDamager implements Damager {

        /** The singleton. */
        private static final AttributeModifyDamager SINGLETON = new AttributeModifyDamager();

        /**
         * @see org.trix.cuery.CSSWeaver.Damager#getDamegeRegion(Node)
         */
        public Set getDamegeRegion(Node node) {
            Set container = new HashSet();
            container.addAll(DOMUtil.retrieveElements(node, AcceptFilter.SINGLETON, true));

            while (node != null) {
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    container.add(node);
                }
                node = node.getNextSibling();
            }
            return container;
        }
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: PseudoClassModifyDamager.java,v 1.0 2005/12/11 13:23:04 Teletha Exp $
     */
    private static final class PseudoClassModifyDamager implements Damager {

        /** The singleton. */
        private static final PseudoClassModifyDamager SINGLETON = new PseudoClassModifyDamager();

        /**
         * @see org.trix.cuery.CSSWeaver.Damager#getDamegeRegion(Node)
         */
        public Set getDamegeRegion(Node node) {
            Set container = new HashSet();
            container.addAll(DOMUtil.retrieveElements(node, AcceptFilter.SINGLETON, true));

            while (node != null) {
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    container.add(node);
                }
                node = node.getNextSibling();
            }
            return container;
        }
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: CSSWeaver.java,v 1.0 2005/12/29 12:28:42 Teletha Exp $
     */
    private static final class SimpleResolver implements URIResolver {

        /**
         * @see org.trix.cuery.URIResolver#resolve(java.lang.String, java.lang.String)
         */
        public InputSource resolve(String href, String base) throws CSSException {
            try {
                return CSSUtil.getSource(URIUtil.getNormalizedURI(base, href));
            } catch (IOException e) {
                throw new CSSException(e);
            }
        }
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: StylesheetInfo.java,v 1.0 2006/03/24 8:01:20 Teletha Exp $
     */
    private static final class StylesheetInfo {

        /** The css source. */
        private InputSource source;

        /** The css event stream pipe. */
        private CSSPipe pipe;
    }
}
