package org.gvsig.expressionevaluator.spi;

import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.gvsig.expressionevaluator.ExpressionRuntimeException;
import org.gvsig.expressionevaluator.ExpressionSyntaxException;
import org.gvsig.expressionevaluator.I18N;
import org.gvsig.expressionevaluator.LexicalAnalyzer;
import org.gvsig.tools.lang.Cloneable;
import org.gvsig.tools.text.DMSNumberFormat;

public abstract class AbstractLexicalAnalyzer implements LexicalAnalyzer {

    public static class RowCol {
        private final int row;
        private final int col;
        
        public RowCol(int row, int col) {
            this.row = row;
            this.col = col;
        }
    }

    protected class DefaultToken implements Token {

        private int type;
        private String literal;
        private Object value;

        public DefaultToken() {
        }

        @Override
        public Token clone() throws CloneNotSupportedException {
            // We will assume that the properties of the class are immutable, so 
            // it would suffice to call the super class.
            DefaultToken other = (DefaultToken) super.clone();
            return other;
        }

        @Override
        public void set(int type, String literal) {
            this.set(type, literal, literal);
        }

        @Override
        public void set(int type, String literal, Object value) {
            this.literal = literal;
            this.type = type;
            this.value = value;
        }

        @Override
        public int getType() {
            return type;
        }

        @Override
        public Object getValue() {
            return value;
        }

        @Override
        public String getLiteral() {
            return literal;
        }

        @Override
        public void setLiteral(String literal) {
            this.literal = literal;
        }

        @Override
        public boolean is(String... values) {
            for (String theValue : values) {
                if( StringUtils.isBlank(literal) ) {
                    if( StringUtils.isBlank(theValue) ) {
                        return true;
                    }
                    continue;
                }
                if( StringUtils.isBlank(theValue) ) {
                    continue;
                }
                if( theValue.trim().equalsIgnoreCase(this.literal.trim()) ) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String toString() {
            return String.format("{%d,%s,%s}", this.type, Objects.toString(this.literal), Objects.toString(value));
        }
        
    }

    protected class Buffer implements Cloneable {

        StringBuilder builder;

        public Buffer() {
            this.builder = new StringBuilder();
        }

        @Override
        public Buffer clone() throws CloneNotSupportedException {
            Buffer other = (Buffer) super.clone();
            other.builder = new StringBuilder(builder);
            return other;
        }

        public void clear() {
            builder.delete(0, builder.length());
        }

        public void add(char ch) {
            builder.append(ch);
        }

        public int length() {
            return this.builder.length();
        }

        @Override
        public String toString() {
            return this.builder.toString();
        }
    }

    protected static final char EOF = 0;

    private NumberFormat nf;
    private ParsePosition nfPos;
    private Stack<Integer> states;
    private String source;
    private int position;
    private int maxposition;
    private int lineno;
    private int column;

    protected Buffer buffer;
    protected Token token;
    protected Map<String, Integer> tokens;
    protected boolean useBracketsForIdentifiers;
            
    public AbstractLexicalAnalyzer(String source) {
        this.useBracketsForIdentifiers = false;
        this.position = 0;
        this.maxposition = 0;
        this.source = source;
        this.states = new Stack<>();
        this.buffer = new Buffer();
        this.token = this.createToken();

        this.nf = NumberFormat.getInstance(Locale.UK);
        this.nf.setGroupingUsed(false);
        
        this.nfPos = new ParsePosition(0);

        this.tokens = new HashMap<>();
    }

    public AbstractLexicalAnalyzer() {
        this(null);
    }

    protected Token createToken() {
        return new DefaultToken();
    }
    
    @Override
    public LexicalAnalyzer clone() throws CloneNotSupportedException {
        AbstractLexicalAnalyzer other = (AbstractLexicalAnalyzer) super.clone();
        other.nf = NumberFormat.getInstance(Locale.UK);
        other.nfPos = new ParsePosition(0);
        other.buffer = buffer.clone();
        other.token = token.clone();
        other.states = new Stack<>();
        other.states.addAll(states);
        other.tokens = new HashMap<>(tokens);
        return other;
    }

    @Override
    public void setSource(String source) {
        this.source = source;
        this.position = 0;
        this.maxposition = 0;
    }

    @Override
    public String getSource() {
        return this.source;
    }

    @Override
    public Token next() {
        return getToken();
    }

    @Override
    public Token look() {
        save_state();
        try {
            return getToken();
        } finally {
            restore_state();
        }
    }

    abstract protected Token getToken();

    @Override
    public void save_state() {
        this.states.push(position);
    }

    @Override
    public void restore_state() {
        position = this.states.pop();
    }

    @Override
    public void drop_state() {
        this.states.pop();
    }

    @Override
    public int getPosition() {
        return position;
    }

    @Override
    public int getMaxPosition() {
        return this.maxposition;
    }
    
    private RowCol calcualteRowAndColumn(int position) {
        final String s = this.source;
        int max = s.length();
        if( max > position ) {
            max = position;
        }
        int line = 1;
        int col = 0;
        for (int i = 0; i < max ; i++) {
            if( s.charAt(i)=='\n' ) {
                line++;
                col = 0;
            } 
            col++;
        }
        return new RowCol(line, col);
    }

    @Override
    public int getLine() {
        RowCol rowcol = this.calcualteRowAndColumn(this.position);
        this.lineno = rowcol.row;
        this.column = rowcol.col;
        return lineno;
    }

    @Override
    public int getMaxLine() {
        RowCol rowcol = this.calcualteRowAndColumn(this.maxposition);
        return rowcol.row;
    }

    @Override
    public int getColumn() {
        RowCol rowcol = this.calcualteRowAndColumn(this.position);
        this.lineno = rowcol.row;
        this.column = rowcol.col;
        return column;
    }

    @Override
    public int getMaxColumn() {
        RowCol rowcol = this.calcualteRowAndColumn(this.maxposition);
        return rowcol.col;
    }

    @Override
    public boolean isEOF() {
        return this.position >= this.source.length();
    }

    protected void skipblanks() {
        if (isEOF()) {
            return;
        }
        char ch = getch();
        while (ch != EOF && Character.isWhitespace(ch)) {
            ch = getch();
        }
        ungetch();
    }

    protected char lookch() {
        if (this.position >= this.source.length()) {
            return EOF;
        }
        return this.source.charAt(this.position);
    }

    protected char getch() {
        if (this.position >= this.source.length()) {
            return EOF;
        }
        this.column++;
        char ch = this.source.charAt(this.position++);
        if( this.position>this.maxposition ) {
            this.maxposition = this.position;
        }
        return ch;
    }

    protected void ungetch() {
        this.position--;
        if (this.position < 0) {
            this.position = 0;
        }
        this.column--;
        if (this.column < 0) {
            this.column = 0;
        }
    }

    protected void parseString() {
        buffer.clear();
        char ch = getch();
        while (true) {
            if (ch == EOF) {
                throw new ExpressionSyntaxException(I18N.End_of_string_was_expected_and_end_of_source_was_found(), this);
            }
            if (ch == '\'') {
                ch = getch();
                if (ch == EOF) {
                    break;
                }
                if (ch != '\'') {
                    ungetch();
                    break;
                }
            }
            buffer.add(ch);
            ch = getch();
        }
        token.set(Token.STRING_LITERAL, buffer.toString());
    }
    
//    protected void parseStringBlock() {
//        buffer.clear();
//        char ch = getch();
//        while (true) {
//            if (ch == EOF) {
//                throw new ExpressionSyntaxException(I18N.End_of_string_was_expected_and_end_of_source_was_found(), this);
//            }
//            if (ch == '$') {
//                ch = getch();
//                if (ch == EOF) {
//                    break;
//                }
//                if (ch != '$') {
//                    ungetch();
//                    break;
//                }
//            }
//            buffer.add(ch);
//            ch = getch();
//        }
//        token.set(Token.STRING_LITERAL, buffer.toString());
//    }
    
    protected void parseDMSNumber() {
        char ch;
        
        skipblanks();
        ch = getch();
        if( ch!='@' ) {
            throw new ExpressionSyntaxException(I18N.Wrong_special_number_start(), this);
        }

//        this.nfPos.setIndex(this.position);
//        DMSNumberFormat dmsf = new DMSNumberFormat(true);
//        double dd = dmsf.parse(source, this.nfPos);
//        if (this.nfPos.getIndex() == this.position) {
//            throw new ExpressionRuntimeException(I18N.Expected_a_number_at_position_XpositionX(this.nfPos.getIndex()));
//        }
        
        AbstractLexicalAnalyzer la = this;
        DMSNumberFormat dmsf = new DMSNumberFormat(true) {
            @Override
            protected void skipblanks() {
                la.skipblanks();
            }

            @Override
            protected char getch() {
                return la.getch();
            }

            @Override
            public boolean isEOF() {
                return la.isEOF();
            }

            @Override
            protected void ungetch() {
                la.ungetch();
            }
        };
        dmsf.setEOFMark(EOF);
        double dd;
        try {
            dd = dmsf.parse(null);
        } catch (ParseException ex) {
            throw new ExpressionSyntaxException(ex.getMessage(), la);
        }
        
        token.set(
                Token.FLOATING_POINT_LITERAL,
                String.format("@%s%d %d' %f\"", dmsf.getSign()<0? "-":"+", dmsf.getDegrees(),dmsf.getMinutes(),dmsf.getSeconds()) ,
                dd
        );
    }
    
    protected void parseNumber() {
        this.nfPos.setIndex(this.position);
        Number n = nf.parse(source, this.nfPos);
        if (this.nfPos.getIndex() == this.position) {
            throw new ExpressionRuntimeException(I18N.Expected_a_number_at_position_XpositionX(this.nfPos.getIndex()));
        }
        String literal = source.substring(this.position, this.nfPos.getIndex());
        this.position = this.nfPos.getIndex();
        if( literal.indexOf('.')>-1 || literal.indexOf(',')>-1 ) {
            n = n.doubleValue();
        }
        if( n instanceof Long ) {
            long l = ((Long)n);
            if( l>Integer.MIN_VALUE && l<Integer.MAX_VALUE ) {
                token.set(Token.INTEGER_LITERAL, literal, (int)l);
            } else {
                token.set(Token.INTEGER_LITERAL, literal, n);
            }
        } else if( n instanceof Integer) {
            token.set(Token.INTEGER_LITERAL, literal, n);
        } else {
            token.set(Token.FLOATING_POINT_LITERAL, literal, n);
        }
    }
    
    @Override
    public void setUseBracketsForIdentifiers(boolean useBracketsForIdentifiers) {
        this.useBracketsForIdentifiers = useBracketsForIdentifiers;
    }
    
    @Override
    public boolean getUseBracketsForIdentifiers() {
        return this.useBracketsForIdentifiers;
    }
    
    @Override
    public String getSourceContext() {
        String s = StringUtils.left(source, maxposition) + "[*]" + StringUtils.mid(source, maxposition, 200);
        if( s.length()>200 ) {
            s = "..."+StringUtils.mid(s, maxposition-100, 200)+"...";
        }
        return s;
    }      
    
}
