/*
 Copyright (c) 2009-2013 Olivier Chafik, All Rights Reserved
	
 This file is part of JNAerator (http://jnaerator.googlecode.com/).
	
 JNAerator 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 3 of the License, or
 (at your option) any later version.
	
 JNAerator 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 JNAerator.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.ochafik.lang.jnaerator;

import java.util.Map;
import java.util.LinkedHashMap;
import com.ochafik.lang.jnaerator.parser.*;
import static com.ochafik.lang.jnaerator.parser.Statement.*;
import com.ochafik.lang.jnaerator.parser.StoredDeclarations.*;
import com.ochafik.lang.jnaerator.parser.TypeRef.*;
import com.ochafik.lang.jnaerator.parser.Expression.*;
import com.ochafik.lang.jnaerator.parser.Declarator.*;
import static com.ochafik.lang.jnaerator.parser.ElementsHelper.*;

public class Symbols {

    public final Map<Integer, Element> resolvedVariables = new LinkedHashMap<Integer, Element>();
    public final Map<Integer, Element> resolvedTypes = new LinkedHashMap<Integer, Element>();
    public final Map<Element, SymbolTable> elementTables = new LinkedHashMap<Element, SymbolTable>();

    public Element getType(Identifier ident) {
        return resolvedTypes.get(ident.getId());
    }

    public Element getVariable(Identifier ident) {
        return resolvedVariables.get(ident.getId());
    }

    public SymbolTable getEnclosingSymbolTable(Element element) {
        while (element != null) {
            SymbolTable st = elementTables.get(element);
            if (st != null) {
                return st;
            }
            element = element.getParentElement();
        }
        return null;
    }

    public boolean isClassType(TypeRef tr) {
        if (tr instanceof SimpleTypeRef) {
            return getType(((SimpleTypeRef) tr).getName()) instanceof Struct;
        }
        return false;
    }

    public boolean isEnumType(TypeRef tr) {
        if (tr instanceof SimpleTypeRef) {
            return getType(((SimpleTypeRef) tr).getName()) instanceof com.ochafik.lang.jnaerator.parser.Enum;
        }
        return false;
    }

    public boolean isEnumItem(Identifier ident) {
        Element v = getVariable(ident);
        if (v instanceof Declarator) {
            v = ((Declarator) v).getParentElement();
        }
        if (v instanceof Declaration) {
            v = ((Declaration) v).getValueType();
        }

        Element type;
        if (v instanceof SimpleTypeRef) {
            return getType(((SimpleTypeRef) v).getName()) instanceof com.ochafik.lang.jnaerator.parser.Enum;
        }

        return false; // TODO ?
    }

    public static class SymbolTable {

        public final SymbolTable parent;
        public final Symbols symbols;
        public final Map<Identifier, Element> variableDefinitions = new LinkedHashMap<Identifier, Element>();
        public final Map<Identifier, Element> typeDefinitions = new LinkedHashMap<Identifier, Element>();
        public final Identifier subNamespace;
        public final Element owner;

        public SymbolTable(Symbols symbols) {
            this.parent = null;
            this.symbols = symbols;
            this.owner = null;
            this.subNamespace = null;
        }

        public SymbolTable(SymbolTable parent, Identifier subNamespace, Element owner) {
            this.parent = parent;
            this.symbols = parent.symbols;
            this.subNamespace = subNamespace;
            this.owner = owner;
            if (owner != null) {
                symbols.elementTables.put(owner, this);
            }
        }

        public void defineVariable(String name, Element element) {
            defineVariable(ident(name), element);
        }

        public void defineVariable(Identifier name, Element element) {
            variableDefinitions.put(name, element);
        }

        public void defineType(Identifier name, Element element) {
            typeDefinitions.put(name, element);
        }

        public Element resolveVariable(Identifier ident) {
            return resolve(ident, true);
        }

        public Element resolveType(Identifier ident) {
            return resolve(ident, false);
        }

        private Element resolve(Identifier ident, boolean varOrType) {
            if (ident == null) {
                return null;
            }
            // TODO handle new namespace
            Identifier lastIdent = ident.resolveLastSimpleIdentifier();
            Element resolved = (varOrType ? variableDefinitions : typeDefinitions).get(lastIdent);
            if (resolved == null) {
                if (parent != null) {
                    return parent.resolve(ident, varOrType);
                }
                //if (ident.getParentElement() instanceof TypeRef && ident.getParentElement().getParentElement() instanceof VariablesDeclaration)
                //    System.out.println("hehehe");
                return null;
            }
            //System.out.println("Resolved " + ident + " as " + resolved);
            //System.err.println("Resolved " + ident + " as " + resolved);
            (varOrType ? symbols.resolvedVariables : symbols.resolvedTypes).put(ident.getId(), resolved);
            return resolved;
        }
    }

    static Symbols resolveSymbols(Element root) {
        Symbols symbols = new Symbols();
        final SymbolTable rootTable = new SymbolTable(symbols);
        root.accept(new Scanner() {
            SymbolTable currentTable = rootTable;

            /*public void visitTypeRef(TypeRef element) {
             super.visitTypeRef(element);
             }*/
            public void visitSimpleTypeRef(SimpleTypeRef element) {
                super.visitSimpleTypeRef(element);
                currentTable.resolveType(element.getName());
            }

            public void visitVariableRef(VariableRef element) {
                super.visitVariableRef(element);
                currentTable.resolveVariable(element.getName());
            }

            @Override
            public void visitArg(final Arg element) {
                super.visitArg(element);
                currentTable.defineVariable(element.getName(), element);
            }

            @Override
            public void visitDirectDeclarator(final DirectDeclarator element) {
                super.visitDirectDeclarator(element);
                currentTable.defineVariable(element.getName(), element);
            }

            @Override
            public void visitBlock(final Block element) {
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitBlock(element);
                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitFunction(final Function element) {
                currentTable.defineVariable(element.getName(), element);
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitFunction(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitStruct(final Struct element) {
                currentTable.defineType(element.getTag(), element);
                currentTable = new SymbolTable(currentTable, element.getTag(), element);
                try {
                    super.visitStruct(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitNamespace(final Namespace element) {
                currentTable = new SymbolTable(currentTable, element.getName(), element);
                try {
                    super.visitNamespace(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitFor(final For element) {
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitFor(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitIf(final If element) {
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitIf(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitTry(final Try element) {
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitTry(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitCatch(final Catch element) {
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitCatch(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitWhile(final While element) {
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitWhile(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }

            @Override
            public void visitDoWhile(final DoWhile element) {
                currentTable = new SymbolTable(currentTable, null, element);
                try {
                    super.visitDoWhile(element);

                } finally {
                    currentTable = currentTable.parent;
                }
            }
        });

        return symbols;
    }
}
