/*
 * $Id: AbstractSACParser.java,v 1.4 2008/03/26 02:17:24 sdanig Exp $
 *
 * CSS Parser Project
 *
 * Copyright (C) 2005-2008 David Schweinsberg.  All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * To contact the authors of the library:
 *
 * http://cssparser.sourceforge.net/
 * mailto:davidsch@users.sourceforge.net
 */
package com.steadystate.css.parser;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.ConditionFactory;
import org.w3c.css.sac.DocumentHandler;
import org.w3c.css.sac.ErrorHandler;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.Locator;
import org.w3c.css.sac.Parser;
import org.w3c.css.sac.SACMediaList;
import org.w3c.css.sac.Selector;
import org.w3c.css.sac.SelectorFactory;
import org.w3c.css.sac.SelectorList;

import com.steadystate.css.parser.selectors.ConditionFactoryImpl;
import com.steadystate.css.parser.selectors.SelectorFactoryImpl;
import com.steadystate.css.sac.DocumentHandlerExt;
import com.steadystate.css.sac.TestCSSParseException;

/**
 * Base implementation of {@link Parser}.
 * 
 * @author koch
 */
abstract class AbstractSACParser implements Parser
{
    private DocumentHandlerExt documentHandler = null;
    private ErrorHandler errorHandler = null;
    private InputSource source = null;
    private Locale locale = null;
    private SelectorFactory selectorFactory = null;
    private ConditionFactory conditionFactory = null;
    private ResourceBundle sacParserMessages;
    protected abstract Token getToken();

    protected DocumentHandlerExt getDocumentHandler()
    {
        if (this.documentHandler == null)
        {
            this.setDocumentHandler(new HandlerBase());
        }
        return this.documentHandler;
    }

    public void setDocumentHandler(DocumentHandler handler)
    {
        if (handler instanceof DocumentHandlerExt)
        {
            this.documentHandler = (DocumentHandlerExt) handler;
        }
    }

    protected ErrorHandler getErrorHandler()
    {
        if (this.errorHandler == null)
        {
            this.setErrorHandler(new HandlerBase());
        }
        return this.errorHandler;
    }

    public void setErrorHandler(ErrorHandler eh)
    {
        this.errorHandler = eh;
    }

    protected InputSource getInputSource()
    {
        return this.source;
    }

    public void setLocale(Locale locale)
    {
        if (this.locale != locale)
        {
            this.sacParserMessages = null;
        }
        this.locale = locale;
    }

    protected Locale getLocale()
    {
        if (this.locale == null)
        {
            this.setLocale(Locale.getDefault());
        }
        return this.locale;
    }
    
    protected SelectorFactory getSelectorFactory()
    {
        if (this.selectorFactory == null)
        {
            this.selectorFactory = new SelectorFactoryImpl();
        }
        return this.selectorFactory;
    }

    public void setSelectorFactory(SelectorFactory selectorFactory)
    {
        this.selectorFactory = selectorFactory;
    }
    
    protected ConditionFactory getConditionFactory()
    {
        if (this.conditionFactory == null)
        {
            this.conditionFactory = new ConditionFactoryImpl();
        }
        return this.conditionFactory;
    }

    public void setConditionFactory(ConditionFactory conditionFactory)
    {
        this.conditionFactory = conditionFactory;
    }

    protected ResourceBundle getSACParserMessages()
    {
        if (this.sacParserMessages == null)
        {
            this.sacParserMessages = ResourceBundle.getBundle(
                "com.steadystate.css.parser.SACParserMessages",
                this.getLocale());
        }
        return this.sacParserMessages;
    }

    public Locator getLocator()
    {
        return new LocatorImpl(this.getInputSource().getURI(),
            this.getToken() == null ? 0 : this.getToken().beginLine,
            this.getToken() == null ? 0 : this.getToken().beginColumn);//this.locator;
    }

    protected String add_escapes(String str)
    {
        StringBuilder retval = new StringBuilder();
        char ch;
        for (int i = 0; i < str.length(); i++)
        {
            switch (str.charAt(i))
            {
                case 0 :
                    continue;
                case '\b':
                    retval.append("\\b");
                    continue;
                case '\t':
                    retval.append("\\t");
                    continue;
                case '\n':
                    retval.append("\\n");
                    continue;
                case '\f':
                    retval.append("\\f");
                    continue;
                case '\r':
                    retval.append("\\r");
                    continue;
                case '\"':
                    retval.append("\\\"");
                    continue;
                case '\'':
                    retval.append("\\\'");
                    continue;
                case '\\':
                    retval.append("\\\\");
                    continue;
                default:
                    if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e)
                    {
                       String s = "0000" + Integer.toString(ch, 16);
                       retval.append("\\u"
                           + s.substring(s.length() - 4, s.length()));
                    }
                    else
                    {
                       retval.append(ch);
                    }
                    continue;
            }
        }
        return retval.toString();
    }

    protected CSSParseException toCSSParseException(String key, ParseException e)
    {
        String messagePattern1 =
            this.getSACParserMessages().getString("invalidExpectingOne");
        String messagePattern2 =
            this.getSACParserMessages().getString("invalidExpectingMore");
        int maxSize = 0;
        StringBuilder expected = new StringBuilder();
        for (int i = 0; i < e.expectedTokenSequences.length; i++)
        {
            if (maxSize < e.expectedTokenSequences[i].length)
            {
                maxSize = e.expectedTokenSequences[i].length;
            }
            for (int j = 0; j < e.expectedTokenSequences[i].length; j++)
            {
                expected.append(e.tokenImage[e.expectedTokenSequences[i][j]]);
            }
            //if (e.expectedTokenSequences[i][e.expectedTokenSequences[i].length - 1] != 0)
            if (i < e.expectedTokenSequences.length - 1)
            {
                expected.append(", ");
            }
        }
        StringBuilder invalid = new StringBuilder();
        Token tok = e.currentToken.next;
        for (int i = 0; i < maxSize; i++)
        {
            if (i != 0)
            {
                invalid.append(" ");
            }
            if (tok.kind == 0)
            {
                invalid.append(e.tokenImage[0]);
                break;
            }
            invalid.append(this.add_escapes(tok.image));
            tok = tok.next;
        }
        String s = null;
        try
        {
            s = this.getSACParserMessages().getString(key);
        }
        catch (MissingResourceException ex)
        {
            s = key;
        }
        StringBuffer message = new StringBuffer(s);
        message.append(' ');
        if (e.expectedTokenSequences.length == 1)
        {
            message.append(MessageFormat.format(
                messagePattern1, new Object[] {invalid, expected}));
        }
        else
        {
            message.append(MessageFormat.format(
                messagePattern2, new Object[] {invalid, expected}));
        }
        return new TestCSSParseException(message.toString(),
            this.getInputSource().getURI(), e.currentToken.next.beginLine,
            e.currentToken.next.beginColumn, this.getGrammarUri());
    }

    protected CSSParseException toCSSParseException(TokenMgrError e)
    {
        String messagePattern =
            this.getSACParserMessages().getString("tokenMgrError");
        return new TestCSSParseException(messagePattern,
            this.getInputSource().getURI(), 1, 1, this.getGrammarUri());
    }

    protected CSSParseException createSkipWarning(String key, CSSParseException e)
    {
        String s = null;
        try
        {
            s = this.getSACParserMessages().getString(key);
        }
        catch (MissingResourceException ex)
        {
            s = key;
        }
        return new TestCSSParseException(s, e.getURI(), e.getLineNumber(),
            e.getColumnNumber(), this.getGrammarUri());
    }

    public void parseStyleSheet(InputSource source)
        throws IOException
    {
        this.source = source;
        this.ReInit(getCharStream(source));
        try
        {
            this.styleSheet();
        }
        catch (ParseException e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException("invalidStyleSheet", e));
        }
        catch (TokenMgrError e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException(e));
        }
        catch (CSSParseException e)
        {
            this.getErrorHandler().error(e);
        }
    }
    
    public void parseStyleSheet(String uri) throws IOException
    {
        this.parseStyleSheet(new InputSource(uri));
    }

    public void parseStyleDeclaration(InputSource source)
        throws IOException
    {
        this.source = source;
        this.ReInit(getCharStream(source));
        try
        {
            this.styleDeclaration();
        }
        catch (ParseException e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException("invalidStyleDeclaration", e));
        }
    }
    
    public void parseRule(InputSource source) throws IOException
    {
        this.source = source;
        this.ReInit(getCharStream(source));
        try
        {
            this.styleSheetRuleSingle();
        }
        catch (ParseException e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException("invalidRule", e));
        }
    }
    
    public SelectorList parseSelectors(InputSource source)
        throws IOException
    {
        this.source = source;
        this.ReInit(getCharStream(source));
        SelectorList sl = null;
        try
        {
            sl = this.selectorList();
        }
        catch (ParseException e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException("invalidSelectorList", e));
        }
        return sl;
    }
    
    public LexicalUnit parsePropertyValue(InputSource source)
        throws IOException
    {
        this.source = source;
        this.ReInit(getCharStream(source));
        LexicalUnit lu = null;
        try
        {
            lu = expr();
        }
        catch (ParseException e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException("invalidExpr", e));
        }
        return lu;
    }
    
    public boolean parsePriority(InputSource source)
        throws IOException
    {
        this.source = source;
        this.ReInit(getCharStream(source));
        boolean b = false;
        try
        {
            b = prio();
        }
        catch (ParseException e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException("invalidPrio", e));
        }
        return b;
    }
    
    public SACMediaList parseMedia(InputSource source) throws IOException
    {
        this.source = source;
        this.ReInit(this.getCharStream(source));
        SACMediaListImpl ml = new SACMediaListImpl();
        try
        {
            this.mediaList(ml);
        }
        catch (ParseException e)
        {
            this.getErrorHandler().error(
                this.toCSSParseException("invalidMediaList", e));
        }
        catch (CSSParseException e)
        {
            this.getErrorHandler().error(e);
        }
        return ml;
    }
    
    private CharStream getCharStream(InputSource source)
        throws IOException
    {
        if (source.getCharacterStream() != null)
        {
            return new ASCII_CharStream(
                source.getCharacterStream(), 1, 1);
        }
        else if (source.getByteStream() != null)
        {
            return new ASCII_CharStream(new InputStreamReader(
                source.getByteStream()), 1, 1);
        }
        else if (source.getURI() != null)
        {
            return new ASCII_CharStream(new InputStreamReader(
                new URL(source.getURI()).openStream()), 1, 1);
        }
        return null;
    }

    public abstract String getParserVersion();
    protected abstract String getGrammarUri();
    protected abstract void ReInit(CharStream charStream);
    protected abstract void styleSheet() throws CSSParseException, ParseException;
    protected abstract void styleDeclaration() throws ParseException;
    protected abstract void styleSheetRuleSingle() throws ParseException;
    protected abstract SelectorList selectorList() throws ParseException;
    protected abstract LexicalUnit expr() throws ParseException;
    protected abstract boolean prio() throws ParseException;
    protected abstract void mediaList(SACMediaListImpl ml) throws ParseException;

    protected void handleStartDocument()
    {
        this.getDocumentHandler().startDocument(this.getInputSource());
    }

    protected void handleEndDocument()
    {
        this.getDocumentHandler().endDocument(this.getInputSource());
    }

    protected void handleIgnorableAtRule(String s)
    {
        this.getDocumentHandler().ignorableAtRule(s);
    }

    protected void handleCharset(String characterEncoding)
    {
        this.getDocumentHandler().charset(characterEncoding);
    }

    protected void handleImportStyle(String uri, SACMediaList media,
        String defaultNamespaceURI)
    {
        this.getDocumentHandler().importStyle(uri, media, defaultNamespaceURI);
    }

    protected void handleStartMedia(SACMediaList media)
    {
        this.getDocumentHandler().startMedia(media);
    }

    protected void handleMedium(String medium)
    {
    }

    protected void handleEndMedia(SACMediaList media)
    {
        this.getDocumentHandler().endMedia(media);
    }

    protected void handleStartPage(String name, String pseudo_page)
    {
        this.getDocumentHandler().startPage(name, pseudo_page);
    }

    protected void handleEndPage(String name, String pseudo_page)
    {
        this.getDocumentHandler().endPage(name, pseudo_page);
    }

    protected void handleStartFontFace()
    {
        this.getDocumentHandler().startFontFace();
    }

    protected void handleEndFontFace()
    {
        this.getDocumentHandler().endFontFace();
    }

    protected void handleSelector(Selector selector)
    {
    }

    protected void handleStartSelector(SelectorList selectors)
    {
        this.getDocumentHandler().startSelector(selectors);
    }

    protected void handleEndSelector(SelectorList selectors)
    {
        this.getDocumentHandler().endSelector(selectors);
    }

    protected void handleProperty(String name, LexicalUnit value,
        boolean important)
    {
        this.getDocumentHandler().property(name, value, important);
    }

    protected LexicalUnit functionInternal(LexicalUnit prev, Token t,
        LexicalUnit params)
    {
        if (t.image.equalsIgnoreCase("counter(")) {
            return LexicalUnitImpl.createCounter(prev, params);
         } else if (t.image.equalsIgnoreCase("counters(")) {
            return LexicalUnitImpl.createCounters(prev, params);
         } else if (t.image.equalsIgnoreCase("attr(")) {
            return LexicalUnitImpl.createAttr(prev, params);
         } else if (t.image.equalsIgnoreCase("rect(")) {
            return LexicalUnitImpl.createRect(prev, params);
         }
         return LexicalUnitImpl.createFunction(
             prev,
             t.image.substring(0, t.image.length() - 1),
             params);
    }

    protected LexicalUnit hexcolorInternal(LexicalUnit prev, Token t)
    {
        // Step past the hash at the beginning
        int i = 1;
        int r = 0;
        int g = 0;
        int b = 0;
        int len = t.image.length() - 1;
        String pattern = this.getSACParserMessages().getString("invalidColor");
        try
        {
            if (len == 3) {
                r = Integer.parseInt(t.image.substring(i + 0, i + 1), 16);
                g = Integer.parseInt(t.image.substring(i + 1, i + 2), 16);
                b = Integer.parseInt(t.image.substring(i + 2, i + 3), 16);
                r = (r << 4) | r;
                g = (g << 4) | g;
                b = (b << 4) | b;
            } else if (len == 6) {
                r = Integer.parseInt(t.image.substring(i + 0, i + 2), 16);
                g = Integer.parseInt(t.image.substring(i + 2, i + 4), 16);
                b = Integer.parseInt(t.image.substring(i + 4, i + 6), 16);
            } else {
                throw new TestCSSParseException(MessageFormat.format(
                    pattern, new Object[] {t}),
                    this.getInputSource().getURI(), t.beginLine,
                    t.beginColumn, this.getGrammarUri());
            }

            // Turn into an "rgb()"
            LexicalUnit lr = LexicalUnitImpl.createNumber(null, r);
            LexicalUnit lc1 = LexicalUnitImpl.createComma(lr);
            LexicalUnit lg = LexicalUnitImpl.createNumber(lc1, g);
            LexicalUnit lc2 = LexicalUnitImpl.createComma(lg);
            LexicalUnit lb = LexicalUnitImpl.createNumber(lc2, b);

            return LexicalUnitImpl.createRgbColor(prev, lr);
        }
        catch (NumberFormatException ex)
        {
            throw new TestCSSParseException(MessageFormat.format(
                pattern, new Object[] {t}),
                this.getInputSource().getURI(), t.beginLine,
                t.beginColumn, ex, this.getGrammarUri());
        }
    }

    int intValue(char op, String s)
    {
        return ((op == '-') ? -1 : 1) * Integer.parseInt(s);
    }

    float floatValue(char op, String s) {
        return ((op == '-') ? -1 : 1) * Float.parseFloat(s);
    }

    int getLastNumPos(String s) {
        int i;
        for (i = 0; i < s.length(); i++) {
            if (Character.isLetter(s.charAt(i))) {
                break;
            }
        }
        return i - 1;
    }

    /**
     * Unescapes escaped characters in the specified string, according to the
     * <a href="http://www.w3.org/TR/CSS21/syndata.html#escaped-characters">CSS specification</a>.
     * 
     * This could be done directly in the parser, but portions of the lexer would have to be moved
     * to the parser, meaning that the grammar would no longer match the standard grammar specified
     * by the W3C. This would make the parser and lexer much less maintainable.
     */
    String unescape(String s) {
        int len = s.length();
        StringBuffer buf = new StringBuffer(len);
        int index = 0;

        while (index < len) {
            char c = s.charAt(index);
            if (c == '\\') {
                if (++index < len) {
                    c = s.charAt(index);
                    switch (c) {
                    case '0': case '1': case '2': case '3': case '4':
                    case '5': case '6': case '7': case '8': case '9':
                    case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
                    case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
                        int numValue = Character.digit(c, 16);
                        int count = 0;
                        int p = 16;

                        while (index + 1 < len && count < 6) {
                            c = s.charAt(index+1);

                            if (Character.digit(c, 16) != -1) {
                                numValue = (numValue * 16) + Character.digit(c, 16);
                                p *= 16;
                                index++;
                            } else {
                                if (Character.isWhitespace(c)) {
                                    // skip the latest white space
                                    index++;
                                }
                                break;
                            }
                        }
                        buf.append((char) numValue);
                        break;
                        case '\n':
                        case '\f':
                        break;
                        case '\r':
                        if (index + 1 < len) {
                            if (s.charAt(index + 1) == '\n') {
                                index ++;
                            }
                        }
                        break;
                    default:
                        buf.append(c);
                    }
                } else {
                    throw new TestCSSParseException("invalid string " + s,
                        getLocator(), this.getGrammarUri());
                }
            } else {
                buf.append(c);
            }
            index++;
        }

        return buf.toString();
    }

}
