/*
 * 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.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackInputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.trix.cuery.filter.AcceptFilter;
import org.trix.cuery.filter.AttributeFilter;
import org.trix.cuery.filter.ChildFilter;
import org.trix.cuery.filter.ClassFilter;
import org.trix.cuery.filter.DescendantFilter;
import org.trix.cuery.filter.DirectAdjacentFilter;
import org.trix.cuery.filter.ElementFilter;
import org.trix.cuery.filter.Filter;
import org.trix.cuery.filter.HyphenTokensFilter;
import org.trix.cuery.filter.IDFilter;
import org.trix.cuery.filter.MultipleFilter;
import org.trix.cuery.filter.PseudoClassFilter;
import org.trix.cuery.filter.TokensFilter;
import org.trix.cuery.value.CSSValue;
import org.trix.cuery.value.EMLength;

import org.w3c.css.sac.AttributeCondition;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.CombinatorCondition;
import org.w3c.css.sac.Condition;
import org.w3c.css.sac.ConditionalSelector;
import org.w3c.css.sac.DescendantSelector;
import org.w3c.css.sac.ElementSelector;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LangCondition;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.Selector;
import org.w3c.css.sac.SiblingSelector;

/**
 * DOCUMENT.
 * 
 * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
 * @version $ Id: CSSUtil.java,v 1.14 2005/12/08 00:19:42 Teletha Exp $
 */
public final class CSSUtil {

    /** The system seperator. */
    private static final String SEPARATOR = System.getProperty("line.separator");

    /** The default encoding of this platform. */
    private static final String DEFAULT_ENCODING = System.getProperty("file.encoding");

    /** The buffer size. It shuld be more than enough. */
    private static final int ENCODING_BUFFER_SIZE = 100;

    /** The regex for encoding pattern. */
    private static final Pattern ENCODING_PATTERN = Pattern.compile("^@charset\\s[\"']([^\\s]+)[\"']");

    /**
     * Avoid creating CSSUtil instance.
     */
    private CSSUtil() {
    }

    /**
     * Create InputSource instance from a stylesheet uri. The encoding is automatically setted.
     * 
     * @param uri A stylesheet uri.
     * @return A created InputSource instance.
     * @throws IOException If the stylesheet has an IO error.
     */
    public static InputSource getSource(String uri) throws IOException {
        // check null
        if (uri == null || uri.length() == 0) {
            throw new IOException("This input uri is null.");
        }

        // check file protocol
        if (uri.startsWith("file:")) {
            uri = uri.substring(5);
        }

        try {
            // parse as URL
            URL url = new URL(uri);
            return getSource(url.openStream(), uri);
        } catch (MalformedURLException e) {
            // try to resolve as file
            return getSource(new FileInputStream(uri), uri);
        } catch (IOException e) {
            throw new IOException("This input uri can't open stream. '" + uri + "'");
        }
    }

    /**
     * Create InputSource instance from a stylesheet file. The encoding is automatically setted.
     * 
     * @param file A stylesheet file.
     * @return A created InputSource instance.
     * @throws IOException If the stylesheet has an IO error.
     */
    public static InputSource getSource(File file) throws IOException {
        // check null
        if (file == null) {
            throw new IOException("This input is null file.");
        }

        // check file protocol
        String uri = file.getPath();

        if (uri.startsWith("file:")) {
            uri = uri.substring(5);
        }
        return getSource(new FileInputStream(file), uri);
    }

    /**
     * Create InputSource instance from a stylesheet input. The encoding is automatically setted.
     * 
     * @param input A stylesheet.
     * @return A created InputSource instance.
     * @throws IOException If the stylesheet has an IO error.
     */
    public static InputSource getSource(InputStream input) throws IOException {
        return getSource(input, null);
    }

    /**
     * Create InputSource instance from a stylesheet input. The encoding and uri are automatically
     * setted.
     * 
     * @param input A stylesheet.
     * @param uri A uri.
     * @return A created InputSource instance.
     * @throws IOException If the stylesheet has an IO error.
     */
    public static InputSource getSource(InputStream input, String uri) throws IOException {
        // check null
        if (input == null) {
            throw new IOException("This input is null input stream.");
        }

        PushbackInputStream pushback = null;

        if (input instanceof PushbackInputStream) {
            pushback = (PushbackInputStream) input;
        } else {
            pushback = new PushbackInputStream(input, ENCODING_BUFFER_SIZE);
        }

        // Read some bytes and push them back
        byte[] buffer = new byte[ENCODING_BUFFER_SIZE];
        int length = pushback.read(buffer, 0, buffer.length);
        pushback.unread(buffer, 0, length);

        // Interpret them as an ASCII string
        Reader reader;
        InputSource source = new InputSource();
        Matcher matcher = ENCODING_PATTERN.matcher(new String(buffer, 0, length, "ASCII"));

        // matching
        if (matcher.lookingAt()) {
            reader = new InputStreamReader(pushback, matcher.group(1));
            source.setEncoding(matcher.group(1));
        } else {
            reader = new InputStreamReader(pushback, DEFAULT_ENCODING);
            source.setEncoding(DEFAULT_ENCODING);
        }

        // convert input source and set encoding
        source.setCharacterStream(new BufferedReader(reader));
        source.setURI(uri);
        return source;
    }

    /**
     * Create filter form a condition.
     * 
     * @param condition A condition.
     * @return A filter.
     */
    public static Filter convert(Condition condition) {
        // check
        if (condition instanceof Filter) {
            return (Filter) condition;
        }

        switch (condition.getConditionType()) {
        case Condition.SAC_AND_CONDITION:
            CombinatorCondition combo = (CombinatorCondition) condition;
            return new MultipleFilter(convert(combo.getFirstCondition()), convert(combo.getSecondCondition()));

        case Condition.SAC_ATTRIBUTE_CONDITION:
            AttributeCondition attr = (AttributeCondition) condition;
            return new AttributeFilter(attr.getLocalName(), attr.getValue());

        case Condition.SAC_CLASS_CONDITION:
            AttributeCondition clazz = (AttributeCondition) condition;
            return new ClassFilter(clazz.getValue());

        case Condition.SAC_ID_CONDITION:
            AttributeCondition id = (AttributeCondition) condition;
            return new IDFilter(id.getValue());

        case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
            AttributeCondition hyphen = (AttributeCondition) condition;
            return new HyphenTokensFilter(hyphen.getLocalName(), hyphen.getValue());

        case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION:
            AttributeCondition one = (AttributeCondition) condition;
            return new TokensFilter(one.getLocalName(), one.getValue());

        case Condition.SAC_LANG_CONDITION:
            LangCondition lang = (LangCondition) condition;
            return new HyphenTokensFilter("xml:lang", lang.getLang());

        case Condition.SAC_PSEUDO_CLASS_CONDITION:
            AttributeCondition pseudo = (AttributeCondition) condition;
            return new PseudoClassFilter(pseudo.getValue());

        default:
            return AcceptFilter.SINGLETON;
        }
    }

    /**
     * Create filter form a selector.
     * 
     * @param selector A simple selector.
     * @return A filter.
     */
    public static Filter convert(Selector selector) {
        // check
        if (selector instanceof Filter) {
            return (Filter) selector;
        }

        switch (selector.getSelectorType()) {
        case Selector.SAC_CONDITIONAL_SELECTOR:
            ConditionalSelector conditional = (ConditionalSelector) selector;
            return new MultipleFilter(convert(conditional.getSimpleSelector()), convert(conditional.getCondition()));

        case Selector.SAC_ELEMENT_NODE_SELECTOR:
            ElementSelector element = (ElementSelector) selector;
            return new ElementFilter(null, element.getNamespaceURI(), element.getLocalName());

        case Selector.SAC_CHILD_SELECTOR:
            DescendantSelector child = (DescendantSelector) selector;
            return new ChildFilter(child.getAncestorSelector(), child.getSimpleSelector());

        case Selector.SAC_DESCENDANT_SELECTOR:
            DescendantSelector descendant = (DescendantSelector) selector;
            return new DescendantFilter(descendant.getAncestorSelector(), descendant.getSimpleSelector());

        case Selector.SAC_DIRECT_ADJACENT_SELECTOR:
            SiblingSelector sibling = (SiblingSelector) selector;
            return new DirectAdjacentFilter(sibling.getNodeType(), sibling.getSelector(), sibling.getSiblingSelector());

        default:
            return AcceptFilter.SINGLETON;
        }
    }

    /**
     * Create CSSValue from LexicalUnit.
     * 
     * @param unit A lexical unit.
     * @return A css value.
     */
    public static CSSValue convert(LexicalUnit unit) {
        // check null
        if (unit == null) {
            return null;
        }

        // check
        if (unit instanceof CSSValue) {
            return (CSSValue) unit;
        }

        switch (unit.getLexicalUnitType()) {
        case LexicalUnit.SAC_EM:
            return new EMLength(unit.getStringValue(), convert(unit.getPreviousLexicalUnit()));

        default:
            return null;
        }
    }

    /**
     * Display a float value without .0 if necessary
     * 
     * @param value A float value.
     * @return A formatted float value.
     */
    public static String displayFloat(float value) {
        int intValue = (int) value;

        if (value == (float) intValue) {
            return Integer.toString(intValue, 10);
        } else {
            return Float.toString(value);
        }
    }

    /**
     * Print error message.
     * 
     * @param exception A css parse exception.
     * @return A human readable message.
     */
    public static String getErrorMessage(CSSParseException exception) {
        StringBuffer buffer = new StringBuffer("CSS parsing error is occured in '");
        buffer.append(exception.getURI());
        buffer.append("' at line ");
        buffer.append(exception.getLineNumber());
        buffer.append(", column ");
        buffer.append(exception.getColumnNumber());
        buffer.append(".");
        buffer.append(SEPARATOR);
        buffer.append("Message : ");
        buffer.append(exception.getMessage());

        if (exception.getException() != null) {
            buffer.append(SEPARATOR);
            buffer.append("Caused by : ");
            buffer.append(exception.getException().getMessage());
        }
        return buffer.toString();
    }
}
