package com.metaweb.lessen.tokenizers;


import com.metaweb.lessen.tokens.NumericToken;
import com.metaweb.lessen.tokens.NumericWithUnitToken;
import com.metaweb.lessen.tokens.StringValueToken;
import com.metaweb.lessen.tokens.Token;
import com.metaweb.lessen.tokens.UriToken;
import com.metaweb.lessen.tokens.Token.Type;


abstract public class TokenizerBase implements Tokenizer {
    protected Token _token;
    
    abstract protected boolean hasMoreChar();
    
    abstract protected boolean hasMoreChar(int offset);
    
    abstract protected char getCurrentChar();
    
    abstract protected char getCharRelative(int offset);
    
    abstract protected int getCurrentOffset();
    
    abstract protected void advance();
    
    abstract protected void advance(int by, boolean flush);
    
    protected void advance(int by) {
        advance(by, true);
    }
    
    abstract protected String getText(int from, int to);
    
    abstract protected void flush(int upToIndex);
    
    @Override
    public Token getToken() {
        return _token;
    }
    
    @Override
    public void next() {
        _token = null;
        if (hasMoreChar()) {
            int tokenStart = getCurrentOffset();
            char c = getCurrentChar();
            
            if (isWhitespace(c)) {
                int distance = lookOverWhitespace(tokenStart);
                int tokenEnd = tokenStart + distance;
                
                _token = new Token(
                    Type.Whitespace, 
                    tokenStart, 
                    tokenEnd, 
                    getText(tokenStart, tokenEnd)
                );
                
                advance(distance);
                return;
                
            } else if (Character.isDigit(c)) {
                parseNumber(tokenStart, 1);
                return;
                
            } else if ("{}()[]:;,".indexOf(c) >= 0) {
                int tokenEnd = tokenStart + 1;
                
                _token = new Token(
                    Type.Delimiter,
                    tokenStart,
                    tokenEnd,
                    getText(tokenStart, tokenEnd)
                );
                
                advance();
                return;
            } else if (c == '~' || c == '|') {
                if (hasMoreChar(1) && getCharRelative(1) == '=') {
                    advance();
                }
                // fall through
            } else if (c == '/') {
                if (hasMoreChar(1)) {
                    char c2 = getCharRelative(1);
                    if (c2 == '/') {
                        advance(2, false);
                        
                        int commentStart = getCurrentOffset();
                        while (hasMoreChar()) {
                            c2 = getCurrentChar();
                            if (c2 == '\r' || c2 == '\n' || c2 == '\f') {
                                break;
                            } else {
                                advance();
                            }
                        }
                        
                        int tokenEnd = getCurrentOffset();
                        
                        _token = new StringValueToken(
                            Type.Comment,
                            tokenStart,
                            tokenEnd,
                            getText(tokenStart, tokenEnd),
                            getText(commentStart, tokenEnd)
                        );
                        
                        return;
                    } else if (c2 == '*') {
                        advance(2, false);
                        
                        int commentStart = getCurrentOffset();
                        int commentEnd = commentStart;
                        
                        while (hasMoreChar()) {
                            c2 = getCurrentChar();
                            if (c2 == '*' && hasMoreChar(1) && getCharRelative(1) == '/') {
                                advance(2, false);
                                break;
                            } else {
                                commentEnd++;
                                advance();
                            }
                        }
                        
                        int tokenEnd = getCurrentOffset();
                        
                        _token = new StringValueToken(
                            Type.Comment,
                            tokenStart,
                            tokenEnd,
                            getText(tokenStart, tokenEnd),
                            getText(commentStart, commentEnd)
                        );
                        
                        flush(tokenEnd);
                        
                        return;
                    }
                    // fall through
                }
                // fall through
                
            } else if (c == '@') {
                int distance = lookOverIdentifier(1);
                if (distance > 0) {
                    int tokenEnd = tokenStart + 1 + distance;
                    
                    _token = new Token(
                        Type.AtIdentifier, 
                        tokenStart, 
                        tokenEnd,
                        getText(tokenStart, tokenEnd)
                    );
                    
                    advance(1 + distance);
                    return;
                }
                // fall through
                
            } else if (c == '#') {
                int distance = lookOverName(1);
                if (distance > 0) {
                    int tokenEnd = tokenStart + 1 + distance;
                    
                    _token = new Token(
                        Type.HashName,
                        tokenStart,
                        tokenEnd,
                        getText(tokenStart, tokenEnd)
                    );
                    
                    advance(1 + distance);
                    return;
                }
                // fall through
                
            } else if (c == '<' && 
                hasMoreChar(3) &&
                getCharRelative(1) == '!' &&
                getCharRelative(2) == '-' &&
                getCharRelative(3) == '-') {
                
                int tokenEnd = tokenStart + 4;
                
                _token = new Token(
                    Type.CDataOpen, 
                    tokenStart, 
                    tokenEnd,
                    getText(tokenStart, tokenEnd)
                );
                
                advance(4);
                return;
                
            } else if (c == '-') {
                if (hasMoreChar(2) && 
                        getCharRelative(1) == '-' && 
                        getCharRelative(2) == '>') {
                    
                    int tokenEnd = tokenStart + 3;
                    
                    _token = new Token(
                        Type.CDataClose, 
                        tokenStart, 
                        tokenEnd,
                        getText(tokenStart, tokenEnd)
                    );
                    
                    advance(3);
                    return;
                    
                } else if (hasMoreChar(1)) {
                    if (Character.isDigit(getCharRelative(1))) {
                        advance(); // swallow -
                        
                        parseNumber(tokenStart, -1);
                        return;
                        
                    } else {
                        int distance = lookOverName(1);
                        if (distance > 0) {
                            int tokenEnd = tokenStart + 1 + distance;
                            
                            _token = new Token(
                                Type.Identifier, 
                                tokenStart, 
                                tokenEnd,
                                getText(tokenStart, tokenEnd)
                            );
                            
                            advance(1 + distance);
                            return;
                        }
                        // fall through
                    }
                }
                // fall through
                
            } else if (isNameStartChar(c)) {
                if (c == 'u' && hasMoreChar(1) && getCharRelative(1) == '+') {
                    int unicodeCodeDistance = lookOverUnicodeCode(2);
                    if (unicodeCodeDistance > 0) {
                        int tokenEnd = tokenStart + 2 + unicodeCodeDistance;
                        
                        advance(2 + unicodeCodeDistance, false);
                        
                        if (hasMoreChar() && getCurrentChar() == '-') {
                            int unicodeCodeDistance2 = lookOverUnicodeCode(1);
                            if (unicodeCodeDistance2 > 0) {
                                tokenEnd += 1 + unicodeCodeDistance2;
                                
                                advance(1 + unicodeCodeDistance2, false);
                            }
                        }
                        
                        _token = new Token(
                            Type.UnicodeRange, 
                            tokenStart, 
                            tokenEnd,
                            getText(tokenStart, tokenEnd)
                        );
                        
                        flush(tokenEnd);
                        return;
                    }
                }
                
                int distance = lookOverName(0);
                int tokenEnd = tokenStart + distance;
                
                String name = getText(tokenStart, tokenEnd);
                
                if (hasMoreChar(distance) && 
                    getCharRelative(distance) == '(') {
                    
                    advance(distance + 1, false);
                    
                    if ("uri".equals(name) || "url".equals(name)) {
                        parseRestOfURI(tokenStart, name);
                        return;
                    } else {
                        tokenEnd++;
                        
                        _token = new Token(
                            Type.Function,
                            tokenStart, 
                            tokenEnd,
                            getText(tokenStart, tokenEnd)
                        );
                        return;
                    }
                }
                
                _token = new Token(
                    Type.Identifier, 
                    tokenStart, 
                    tokenEnd,
                    getText(tokenStart, tokenEnd)
                );
                
                advance(distance);
                return;
                
            } else if (c == '"' || c == '\'') {
                advance();
                
                String value = parseString(c);
                
                int tokenEnd = getCurrentOffset();
                
                _token = new StringValueToken(
                    Type.String,
                    tokenStart,
                    tokenEnd,
                    getText(tokenStart, tokenEnd),
                    value
                );
                
                return;
                
            } else if (c == '$' && hasMoreChar(1)) {
                char c2 = getCharRelative(1);
                if (c2 == '{') {
                    int nameStart = tokenStart + 2;
                    int nameEnd = nameStart;
                    
                    int distance = 2;
                    while (hasMoreChar(distance)) {
                        c2 = getCharRelative(distance);
                        if (c2 == '}') {
                            distance++;
                            break;
                        } else {
                            nameEnd++;
                            distance++;
                        }
                    }
                    
                    int tokenEnd = tokenStart + distance;
                    
                    _token = new StringValueToken(
                        Type.Variable,
                        tokenStart,
                        tokenEnd,
                        getText(tokenStart, tokenEnd),
                        getText(nameStart, nameEnd)
                    );
                    
                    advance(distance);
                    
                    return;
                } else if (Character.isLetter(c2)) {
                    int nameStart = tokenStart + 1;
                    
                    int distance = 1;
                    while (hasMoreChar(distance)) {
                        c2 = getCharRelative(distance);
                        if (Character.isLetter(c2)) {
                            distance++;
                        } else {
                            break;
                        }
                    }
                    
                    int tokenEnd = tokenStart + distance;
                    
                    _token = new StringValueToken(
                        Type.Variable,
                        tokenStart,
                        tokenEnd,
                        getText(tokenStart, tokenEnd),
                        getText(nameStart, tokenEnd)
                    );
                    
                    advance(distance);
                    
                    return;
                }
            }
            
            advance();
            
            int tokenEnd = getCurrentOffset();
            
            _token = new Token(
                Type.Operator,
                tokenStart,
                tokenEnd,
                getText(tokenStart, tokenEnd)
            );
        }
    }
    
    protected void parseNumber(int tokenStart, int signum) {
        int n = 0;
        double divide = 1;
        
        char c;
        while (hasMoreChar()) {
            c = getCurrentChar();
            if (Character.isDigit(c)) {
                n = n * 10 + (c - '0');
                advance();
            } else {
                break;
            }
        }
        
        if (hasMoreChar()) {
            c = getCurrentChar();
            if (c == '.') {
                advance();
                
                while (hasMoreChar()) {
                    c = getCurrentChar();
                    if (Character.isDigit(c)) {
                        n = n * 10 + (c - '0');
                        divide *= 10;
                        
                        advance();
                    } else {
                        break;
                    }
                }
            }
        }
        
        Number num = signum * (divide > 1 ? (n / divide) : n);
        
        if (hasMoreChar()) {
            c = getCurrentChar();
            if (c == '%') {
                advance();
                
                int tokenEnd = getCurrentOffset();
                
                _token = new NumericToken(
                    Type.Percentage, 
                    tokenStart, 
                    tokenEnd,
                    getText(tokenStart, tokenEnd), 
                    num
                );
                
                flush(tokenEnd);
                return;
            } else if (Character.isLetter(c)) {
                int unitStart = getCurrentOffset();
                
                advance();
                
                while (hasMoreChar() && Character.isLetter(getCurrentChar())) {
                    advance();
                }
                
                int tokenEnd = getCurrentOffset();
                
                _token = new NumericWithUnitToken(
                    Type.Dimension, 
                    tokenStart, 
                    tokenEnd, 
                    getText(tokenStart, tokenEnd), 
                    num, 
                    getText(unitStart, tokenEnd)
                );
                
                flush(tokenEnd);
                return;
            }
        }
        
        int tokenEnd = getCurrentOffset();
        
        _token = new NumericToken(
            Type.Number, 
            tokenStart, 
            tokenEnd,
            getText(tokenStart, tokenEnd), 
            num
        );
        
        flush(tokenEnd);
    }

    protected void parseRestOfURI(int tokenStart, String prefix) {
        swallowWhitespace(false);
        
        String value = "";
        if (hasMoreChar()) {
            char c = getCurrentChar();
            if (c == '"' || c == '\'') {
                advance();
                
                value = parseString(c);
            } else {
                int valueStart = getCurrentOffset();
                advance();
                
                while (hasMoreChar()) {
                    c = getCurrentChar();
                    if (isWhitespace(c) || c == ')') {
                        break;
                    } else {
                        advance();
                    }
                }
                
                value = getText(valueStart, getCurrentOffset());
            }
        }
        
        swallowWhitespace(false);
        
        if (hasMoreChar()) {
            char c = getCurrentChar();
            if (c == ')') {
                advance();
            }
        }
        
        int tokenEnd = getCurrentOffset();
        _token = new UriToken(
            tokenStart,
            tokenEnd,
            getText(tokenStart, tokenEnd),
            prefix,
            value
        );
    }
    
    protected String parseString(char delimiter) {
        int start = getCurrentOffset();
        int end = start;
        
        while (hasMoreChar()) {
            char c = getCurrentChar();
            if (c == delimiter) {
                end = getCurrentOffset();
                advance();
                break;
            } else if (c == '\r' || c == '\n' || c == '\f') {
                end = getCurrentOffset();
                break;
            } else if (c == '\\' && hasMoreChar(1)) {
                advance();
            }
            
            advance();
        }
        
        return getText(start, end);
    }
    
    protected void swallowWhitespace(boolean flush) {
        advance(lookOverWhitespace(0), flush);
    }
    
    protected int lookOverWhitespace(int index) {
        int distance = 0;
        while (hasMoreChar(distance) && Character.isWhitespace(getCharRelative(distance))) {
            distance++;
        }
        return distance;
    }
    
    protected int lookOverIdentifier(int offset) {
        int distance = 0;
        if (hasMoreChar(offset + distance)) {
            int distance2 = lookOverNameStartChar(offset + distance);
            if (distance2 > 0) {
                distance += distance2;
                distance += lookOverName(offset + distance);
            }
        }
        return distance;
    }
    
    protected int lookOverName(int offset) {
        int distance = 0;
        while (hasMoreChar(offset + distance)) {
            int distance2 = lookOverNameChar(offset + distance);
            if (distance2 > 0) {
                distance += distance2;
            } else {
                break;
            }
        }
        return distance;
    }
    
    protected int lookOverNameStartChar(int offset) {
        int distance = 0;
        if (hasMoreChar(offset + distance)) {
            char c = getCharRelative(offset + distance);
            if (isNameStartChar(c)) {
                distance++;
            } else if (c == '\\' && hasMoreChar(offset + distance + 1)) {
                char c2 = getCharRelative(offset + distance + 1);
                if (isHexDigit(c2)) {
                    int distance2 = lookOverUnicodeCode(offset + distance + 1);
                    if (distance2 > 0) {
                        distance += distance2;
                    }
                } else if (c2 != '\r' && c2 != '\n' && c2 != '\f') {
                    distance += 2;
                }
            }
        }
        return distance;
    }
    
    protected int lookOverNameChar(int offset) {
        int distance = 0;
        if (hasMoreChar(offset + distance)) {
            char c = getCharRelative(offset + distance);
            if (isNameChar(c)) {
                distance++;
            } else if (c == '\\' && hasMoreChar(offset + distance + 1)) {
                char c2 = getCharRelative(offset + distance + 1);
                if (isHexDigit(c2)) {
                    int distance2 = lookOverUnicodeCode(offset + distance + 1);
                    if (distance2 > 0) {
                        distance += distance2;
                    }
                } else if (c2 != '\r' && c2 != '\n' && c2 != '\f') {
                    distance += 2;
                }
            }
        }
        return distance;
    }
    
    protected int lookOverUnicodeChar(int offset) {
        int distance = 0;
        if (hasMoreChar(offset + distance)) {
            char c = getCharRelative(offset + distance);
            if (c == '\\' && hasMoreChar(offset + distance + 1)) {
                char c2 = getCharRelative(offset + distance + 1);
                if (isHexDigit(c2)) {
                    int distance2 = lookOverUnicodeCode(offset + distance + 1);
                    if (distance2 > 0) {
                        distance += distance2;
                    }
                }
            }
        }
        return distance;
    }
    
    protected int lookOverUnicodeCode(int offset) {
        int distance = 0;
        while (distance < 6 && hasMoreChar(offset + distance) && isHexDigit(getCharRelative(offset + distance))) {
            distance++;
        }
        return distance;
    }
    
    static protected boolean isWhitespace(char c) {
        return Character.isWhitespace(c);
    }
    
    static protected boolean isHexDigit(char c) {
        return Character.isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
    }
    
    static protected boolean isNameChar(char c) {
        return c == '_' || c == '-' || Character.isDigit(c) || Character.isLetter(c) || c > 177;
    }
    
    static protected boolean isNameStartChar(char c) {
        return c == '_' || Character.isLetter(c) || c > 177;
    }
}
