/*
 * 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.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.CSSConsumer;
import org.trix.cuery.css.CSSPipe;
import org.trix.cuery.parser.CueryParser;
import org.trix.cuery.style.CascadableStyle;
import org.trix.cuery.style.SimpleStyle;
import org.trix.cuery.style.Style;
import org.trix.cuery.util.CSSUtil;
import org.trix.cuery.util.DOMUtil;
import org.trix.cuery.util.URIUtil;
import org.trix.cuery.value.Value;

import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.SelectorList;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.stylesheets.StyleSheet;
import org.w3c.dom.stylesheets.StyleSheetList;

import org.xml.sax.SAXException;

/**
 * DOCUMENT.
 * 
 * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
 * @version $ Id: CSSWeaver.java,v 1.04 2005/08/15 9:45:08 Teletha Exp $
 */
public class CSSWeaver {

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

    /** The style processor. */
    private StyleProcessor processor = new StyleProcessor();

    /** The flag to aware xml-stylesheet pi. */
    private boolean aware = 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 {
        parseStylesheet(source, processor, CascadableStyle.ORIGIN_AGENT);
    }

    /**
     * Add styles as an agent's stylesheet.
     * 
     * @param source A stylesheet source.
     * @param pipe A css pipe to huck.
     * @throws IOException If this uri is invalid.
     */
    public void addAgentStylesheet(InputSource source, CSSPipe pipe) throws IOException {
        pipe.setConsumer(processor);
        parseStylesheet(source, pipe, CascadableStyle.ORIGIN_AGENT);
    }

    /**
     * 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 {
        parseStylesheet(source, processor, CascadableStyle.ORIGIN_AUTHOR);
    }

    /**
     * Add styles as an author's stylesheet.
     * 
     * @param source A stylesheet source.
     * @param pipe A css pipe to huck.
     * @throws IOException If this uri is invalid.
     */
    public void addAuthorStylesheet(InputSource source, CSSPipe pipe) throws IOException {
        pipe.setConsumer(processor);
        parseStylesheet(source, pipe, CascadableStyle.ORIGIN_AUTHOR);
    }

    /**
     * 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 {
        parseStylesheet(source, processor, CascadableStyle.ORIGIN_USER);
    }

    /**
     * Add styles as an user's stylesheet.
     * 
     * @param source A stylesheet source.
     * @param pipe A css pipe to huck.
     * @throws IOException If this uri is invalid.
     */
    public void addUserStylesheet(InputSource source, CSSPipe pipe) throws IOException {
        pipe.setConsumer(processor);
        parseStylesheet(source, pipe, CascadableStyle.ORIGIN_USER);
    }

    /**
     * Helper method to parse stylesheet.
     * 
     * @param stylesheet A target stylesheet to parse.
     * @param consumer A css consumer.
     * @param origin A stylesheet origin.
     * @throws IOException If this stylesheet is invalid.
     */
    private synchronized void parseStylesheet(InputSource stylesheet, CSSConsumer consumer, int origin)
            throws IOException {
        processor.origin = origin;
        parser.setDocumentHandler(consumer);
        parser.parseStyleSheet(stylesheet);
    }

    /**
     * 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) {
        // create query for this document
        CSSQuery query = new CSSQuery(document);

        // weave registered styles
        processor.weave(query);

        // aware linked inline styles
        if (aware) {
            StyleProcessor inlineProcessor = new StyleProcessor(processor);
            StyleSheetList stylesheets = DOMUtil.getStylesheets(document);

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

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

                try {
                    // resolve uri and parse it
                    String uri = URIUtil.getNormalizedURI(document.getDocumentURI(), stylesheet.getHref());
                    parseStylesheet(CSSUtil.getSource(uri), inlineProcessor, CascadableStyle.ORIGIN_AUTHOR);
                } catch (IOException e) {
                    System.out.println(e);
                    // do nothing
                }
            }

            // weave linked inline styles
            inlineProcessor.weave(query);
        }
        return document;
    }

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

    /**
     * Helper method to get a computed style from a element.
     * 
     * @param target A target element.
     * @return A computed style.
     */
    public static final Style getStyle(Element target) {
        return (Style) target.getUserData(Style.KEY);
    }

    /**
     * DOCUMENT.
     * 
     * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
     * @version $ Id: StyleProcessor.java,v 1.02 2005/08/15 9:40:17 Teletha Exp $
     */
    private final class StyleProcessor extends AbstractCSSConsumer {

        /** The style rule list. */
        private List<Object[]> container = new ArrayList<Object[]>();

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

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

        /** The current style. */
        private SimpleStyle style;

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

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

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

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

        /**
         * @see org.trix.cuery.css.AbstractCSSConsumer#endSelector(org.w3c.css.sac.SelectorList)
         */
        @Override
        public void endSelector(SelectorList selectorList) throws CSSException {
            container.add(new Object[] {selectorList, style, position, origin});
        }

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

            for (Object[] objects : container) {
                SelectorList list = (SelectorList) objects[0];
                Style style = (Style) objects[1];
                int position = (Integer) objects[2];
                int origin = (Integer) objects[3];

                for (int i = 0; i < list.getLength(); i++) {
                    Set<Element> elements = query.select(list.item(i));

                    // weave style
                    for (Element element : elements) {
                        CascadableStyle cascadable = (CascadableStyle) element.getUserData(Style.KEY);

                        if (cascadable == null) {
                            cascadable = new CascadableStyle();
                        }
                        cascadable.addStyle(style, origin, CSSUtil.converte(list.item(i)).getSpecificity(), position);
                        element.setUserData(Style.KEY, cascadable, null);
                    }
                }
            }
        }
    }
}
