/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.axis2.corba.idl.parser;

import antlr.collections.AST;
import org.apache.axis2.corba.idl.types.ArrayType;
import org.apache.axis2.corba.idl.types.CompositeDataType;
import org.apache.axis2.corba.idl.types.DataType;
import org.apache.axis2.corba.idl.types.EnumType;
import org.apache.axis2.corba.idl.types.ExceptionType;
import org.apache.axis2.corba.idl.types.IDL;
import org.apache.axis2.corba.idl.types.Interface;
import org.apache.axis2.corba.idl.types.Member;
import org.apache.axis2.corba.idl.types.Operation;
import org.apache.axis2.corba.idl.types.PrimitiveDataType;
import org.apache.axis2.corba.idl.types.SequenceType;
import org.apache.axis2.corba.idl.types.Struct;
import org.apache.axis2.corba.idl.types.Typedef;
import org.apache.axis2.corba.idl.types.UnionMember;
import org.apache.axis2.corba.idl.types.UnionType;
import org.apache.axis2.corba.idl.types.ValueType;
import org.apache.axis2.corba.exceptions.InvalidIDLException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.Map;

/**
 * Created by IntelliJ IDEA.
 * User: Eranga
 * Date: Feb 11, 2007
 * Time: 10:06:23 PM
 */
public class IDLVisitor /*implements ASTVisitor*/ {
    private static final Log log = LogFactory.getLog(IDLVisitor.class);
    private IDL idl = new IDL();
    private String module = "";
    private String moduleForInnerTypes = null;
    private static final String INNERTYPE_SUFFIX = "Package";

    public IDL getIDL() {
        return idl;
    }

    public void setIDL(IDL idl) {
        this.idl = idl;
    }

    public void visit(AST node) throws InvalidIDLException {
        while (node != null) {
            switch (node.getType()) {

                case IDLTokenTypes.LITERAL_interface: {
                    idl.addInterface(visitInterface(node));
                    break;
                }

                case IDLTokenTypes.LITERAL_module: {
                    AST moduleName = node.getFirstChild();
                    IDLVisitor moduleVisitor = new IDLVisitor();
                    moduleVisitor.setIDL(idl);
                    moduleVisitor.setModule(module + moduleName);
                    moduleVisitor.visit(moduleName.getNextSibling());
                    idl.addIDL(moduleVisitor.getIDL());
                    break;
                }

                case IDLTokenTypes.LITERAL_struct: {
                    idl.addType(visitStruct(node));
                    break;
                }

                case IDLTokenTypes.LITERAL_valuetype: {
                    idl.addType(visitValueType(node));
                    break;
                }
                
                case IDLTokenTypes.LITERAL_exception: {
                    idl.addType(visitException(node));
                    break;
                }

                case IDLTokenTypes.LITERAL_enum: {
                    idl.addType(visitEnum(node));
                    break;
                }

                case IDLTokenTypes.LITERAL_union: {
                    idl.addType(visitUnion(node));
                    break;
                }

                case IDLTokenTypes.LITERAL_typedef: {
                    visitAndAddTypedefs(node, module);
                    break;
                }

                default: {
                    throw new InvalidIDLException("Unsupported IDL token " + node);
                }
            }
            node = node.getNextSibling();
        }
    }

    private Struct visitStruct(AST node) throws InvalidIDLException {
        AST structNode = node.getFirstChild();
        String structName = structNode.toString();
        Struct struct = new Struct();
        struct.setModule(module);
        struct.setName(structName);
        AST memberTypeNode = structNode.getNextSibling();
        while (memberTypeNode != null) {
            Member member = new Member();
            DataType dataType = findDataType(memberTypeNode, structName);
            AST memberNode = memberTypeNode.getNextSibling();
            String memberName = memberNode.getText();
            int dimensions = memberNode.getNumberOfChildren();
            if (dimensions > 0) {
                AST dimensionNode = memberNode.getFirstChild();
                ArrayType arrayType = null;
                ArrayType rootArrayType = null;
                int i = 1;
                while(dimensionNode!=null) {
                    ArrayType temp = new ArrayType();
                    temp.setElementModule(module);
                    temp.setElementName(memberName);
                    temp.setDepth(i);
                    i++;
                    if (arrayType != null) {
                        arrayType.setDataType(temp);
                    } else {
                        rootArrayType = temp;
                    }
                    arrayType = temp;
                    arrayType.setElementCount(Integer.parseInt(dimensionNode.getText()));
                    dimensionNode = dimensionNode.getNextSibling();
                }
                if (arrayType != null) {
                    arrayType.setDataType(dataType);
                }
//                dataType = rootArrayType;
                Typedef typedef = new Typedef();
                typedef.setDataType(rootArrayType);
                typedef.setModule(module);
                typedef.setName(structName + '_' + memberName);
                idl.addType(typedef);
                dataType = typedef;
            }

            member.setDataType(dataType);
            member.setName(memberName);
            struct.addMember(member);
            memberTypeNode = memberNode.getNextSibling();
        }
        return struct;
    }

    private ValueType visitValueType(AST node) throws InvalidIDLException {
        AST valueNode = node.getFirstChild();
        ValueType value = new ValueType();
        value.setModule(module);
        String valueName = valueNode.toString();
        value.setName(valueName);
        AST memberModifierNode = valueNode.getNextSibling();
        while (memberModifierNode != null) {
            String memberModifierName = memberModifierNode.toString();
            if (!memberModifierName.equals("private") && !memberModifierName.equals("public")) {
                if (IDLTokenTypes.PREPROC_DIRECTIVE==memberModifierNode.getType()) {
                    if (memberModifierName.startsWith("pragma ID ")) {
                        memberModifierName = memberModifierName.substring(10);
                        String[] pragma = memberModifierName.split(" ");
                        if (pragma.length==2 && pragma[0]!=null && pragma[1]!=null && pragma[0].equals(value.getName())) {
                            pragma[1] = pragma[1].replace('"', ' ');
                            value.setId(pragma[1].trim());
                        }
                    }
                } else {
                    // abstract operation
                    value.addOperation(visitOperation(memberModifierNode));
                }
                memberModifierNode = memberModifierNode.getNextSibling();
                continue;
            }
            Member memberType = new Member();
            memberType.setModifier(memberModifierName);
            AST memberTypeNode = memberModifierNode.getNextSibling();
            memberType.setDataType(findDataType(memberTypeNode, valueName));
            AST memberNode = memberTypeNode.getNextSibling();
            memberType.setName(memberNode.toString());
            value.addMember(memberType);
            memberModifierNode = memberNode.getNextSibling();
        }
        return value;
    }

    private Interface visitInterface(AST node) throws InvalidIDLException {
        Interface intf = new Interface();
        intf.setModule(module);
        AST interfaceNode = node.getFirstChild();
        String interfaceName = interfaceNode.toString();
        intf.setName(interfaceName);
        moduleForInnerTypes = module + interfaceName + INNERTYPE_SUFFIX + CompositeDataType.MODULE_SEPERATOR;
        AST node2 = interfaceNode.getNextSibling();
        while (node2 != null) {
            switch (node2.getType()) {
            case IDLTokenTypes.LITERAL_struct:
                Struct innerStruct = visitStruct(node2);
                innerStruct.setModule(moduleForInnerTypes);
                idl.addType(innerStruct);
                break;
            case IDLTokenTypes.LITERAL_valuetype:
                log.error("Unsupported IDL token " + node2);
                // CORBA 3.O spec does not support this
                break;
            case IDLTokenTypes.LITERAL_exception:
                Struct innerEx = visitException(node2);
                innerEx.setModule(moduleForInnerTypes);
                idl.addType(innerEx);
                break;
            case IDLTokenTypes.LITERAL_enum:
                EnumType innerEnum = visitEnum(node2);
                innerEnum.setModule(moduleForInnerTypes);
                idl.addType(innerEnum);
                break;
            case IDLTokenTypes.LITERAL_union:
                UnionType innerUnion = visitUnion(node2);
                innerUnion.setModule(moduleForInnerTypes);
                idl.addType(innerUnion);
                break;
            case IDLTokenTypes.LITERAL_typedef:
                visitAndAddTypedefs(node2, moduleForInnerTypes);
                break;
            case IDLTokenTypes.LITERAL_const:
                log.error("Unsupported IDL token " + node2);
                break;
            case IDLTokenTypes.LITERAL_attribute:
                intf.addOperation(visitGetAttribute(node2));
                intf.addOperation(visitSetAttribute(node2));
                break;
            default:
                if (node2.toString().startsWith("pragma ID ")) {
                    String pragmaId = node2.toString().substring(10);
                    String[] pragma = pragmaId.split(" ");
                    if (pragma.length==2 && pragma[0]!=null && pragma[1]!=null && pragma[0].equals(intf.getName())) {
                        pragma[1] = pragma[1].replace('"', ' ');
                        intf.setId(pragma[1].trim());
                    }
                } else {
                    intf.addOperation(visitOperation(node2));
                }
                break;
            }
            node2 = node2.getNextSibling();
        }
        moduleForInnerTypes = null;
        return intf;
    }

    private Operation visitGetAttribute(AST node) throws InvalidIDLException {
        Operation operation = new Operation();
        AST type = node.getFirstChild();
        AST name = type.getNextSibling();
        String attrName = name.toString();
        operation.setReturnType(findDataType(type, attrName));
        operation.setName("_get_" + attrName);
        return operation;
    }

    private Operation visitSetAttribute(AST node) throws InvalidIDLException {
        Operation operation = new Operation();
        AST type = node.getFirstChild();
        operation.setReturnType(PrimitiveDataType.getPrimitiveDataType("void"));
        AST name = type.getNextSibling();
        String attrName = name.toString();
        operation.setName("_set_" + attrName);
        Member param = new Member();
        param.setName(attrName);
        param.setDataType(findDataType(type, attrName));
        operation.addParam(param);
        return operation;
    }

    private Operation visitOperation(AST node) throws InvalidIDLException {
        Operation operation = new Operation();
        String opName = node.toString();
        operation.setName(opName);
        AST type = node.getFirstChild();
        operation.setReturnType(findDataType(type, opName));
        AST mode = type.getNextSibling();
        while(mode != null) {
            if (IDLTokenTypes.LITERAL_raises == mode.getType()) {
                AST idlType = mode.getFirstChild();
                while(idlType != null) {
                    operation.addRaises((ExceptionType) findDataType(idlType, opName));
                    idlType = idlType.getNextSibling();
                }
            } else {
                Member param = new Member();
                param.setMode(mode.toString());
                AST idlType = mode.getFirstChild();
                AST paramName = idlType.getNextSibling();
                String paramNameStr = paramName.toString();
                param.setDataType(findDataType(idlType, paramNameStr));
                param.setName(paramNameStr);
                operation.addParam(param);
            }
            mode = mode.getNextSibling();
        }
        return operation;
    }

    private ExceptionType visitException(AST node) throws InvalidIDLException {
        ExceptionType raisesType = new ExceptionType();
        AST exNode = node.getFirstChild();
        String exName = exNode.toString();
        raisesType.setModule(module);
        raisesType.setName(exName);
        AST memberTypeNode = exNode.getNextSibling();
        while (memberTypeNode != null) {
            Member member = new Member();
            member.setDataType(findDataType(memberTypeNode, exName));
            AST memberNode = memberTypeNode.getNextSibling();
            member.setName(memberNode.toString());
            raisesType.addMember(member);
            memberTypeNode = memberNode.getNextSibling();
        }
        return raisesType;
    }

    private DataType findDataType(AST typeNode, String parentName) throws InvalidIDLException {
        return findDataType(typeNode, parentName, true, false);
    }
    private DataType findDataType(AST typeNode, String parentName, boolean root, boolean noTypeDefForSeqs) throws InvalidIDLException {
        // Check for sequences
        if (typeNode.getType()==IDLTokenTypes.LITERAL_sequence) {
            SequenceType sequenceType = visitAnonymousSequence(typeNode, parentName, root);
            if (noTypeDefForSeqs) {
                return sequenceType;
            }
            Typedef typedef = new Typedef();
            typedef.setDataType(sequenceType);
            typedef.setModule(module);
            typedef.setName(parentName + '_' + sequenceType.getName());
            idl.addType(typedef);
            return typedef;
        }

        DataType dataType = null;
        Map compositeDataTypes = idl.getCompositeDataTypes();

        String typeName;
        if (typeNode.getType() == IDLTokenTypes.LITERAL_unsigned) {
            AST nextNode = typeNode.getNextSibling();
            if (nextNode == null) {
                throw new InvalidIDLException("'unsigned' without a data type");
            } else if (nextNode.getType() == IDLTokenTypes.LITERAL_short) {
                typeNode.setNextSibling(nextNode.getNextSibling());
                typeNode.setFirstChild(nextNode.getFirstChild());
                typeName = "ushort";
            } else if (nextNode.getType() == IDLTokenTypes.LITERAL_long) {
                AST nextToLong = nextNode.getNextSibling();
                if (nextToLong == null) {
                    throw new InvalidIDLException("an identifier is required after the 'long' keyword");
                } else if (nextToLong.getType() == IDLTokenTypes.LITERAL_long) {
                    typeNode.setNextSibling(nextToLong.getNextSibling());
                    typeNode.setFirstChild(nextToLong.getFirstChild());
                    typeName = "ulonglong";
                } else {
                    typeNode.setNextSibling(nextNode.getNextSibling());
                    typeNode.setFirstChild(nextNode.getFirstChild());
                    typeName = "ulong";
                }
            } else {
                throw new InvalidIDLException("either 'long' or 'short' is expected after the 'unsigned' keyword");
            }
        } else if (typeNode.getType() == IDLTokenTypes.LITERAL_long) {
            AST nextToLong = typeNode.getNextSibling();
            if (nextToLong == null) {
                throw new InvalidIDLException("an identifier is required after the 'long' keyword");
            } else if (nextToLong.getType() == IDLTokenTypes.LITERAL_long) {
                typeNode.setNextSibling(nextToLong.getNextSibling());
                typeNode.setFirstChild(nextToLong.getFirstChild());
                typeName = "longlong";
            } else {
                typeName = "long";
            }
        } else {
            typeName = getTypeName(typeNode);    
        }


        if (compositeDataTypes!=null) {
            if (!module.equals("")) {
                if (!typeName.startsWith(module)) {
                    dataType = (DataType) idl.getCompositeDataTypes().get(module + typeName);
                }
            }

            if (dataType==null && moduleForInnerTypes!=null) {
                if (!typeName.startsWith(module)) {
                    dataType = (DataType) idl.getCompositeDataTypes().get(moduleForInnerTypes + typeName);
                }
            }

            if (dataType==null)
                dataType = (DataType) idl.getCompositeDataTypes().get(typeName);
        }

        if (dataType==null)
            dataType = PrimitiveDataType.getPrimitiveDataType(typeName);

        if (dataType == null)
            throw new InvalidIDLException("Invalid data type: " + typeName);
        return dataType;
    }

    public String getTypeName(AST node) {
        String typeName = node.getText();
        AST memberTypeNodeChild = node.getFirstChild();
        while(memberTypeNodeChild!=null) {
            typeName = typeName + CompositeDataType.MODULE_SEPERATOR + memberTypeNodeChild.toString();
            memberTypeNodeChild = memberTypeNodeChild.getNextSibling();
        }
        return typeName;
    }

    public void setModule(String module) {
        if (module==null || module.length()<1)
            module = "";
        else if (!module.endsWith(CompositeDataType.MODULE_SEPERATOR))
            module += CompositeDataType.MODULE_SEPERATOR;
        this.module = module;
    }

    private EnumType visitEnum(AST node) {
        AST enumNode = node.getFirstChild();
        String enumName = enumNode.toString();
        EnumType enumType = new EnumType();
        enumType.setModule(module);
        enumType.setName(enumName);
        AST memberTypeNode = enumNode.getNextSibling();
        while (memberTypeNode != null) {
            enumType.addEnumMember(memberTypeNode.toString());
            memberTypeNode = memberTypeNode.getNextSibling();
        }
        return enumType;
    }

    private UnionType visitUnion(AST node) throws InvalidIDLException {
        UnionType unionType = new UnionType();
        AST unNode = node.getFirstChild();
        String unName = unNode.toString();
        unionType.setModule(module);
        unionType.setName(unName);
        AST switchTypeNode = unNode.getNextSibling();
        unionType.setDiscriminatorType(findDataType(switchTypeNode, unName));
        AST caseOrDefaultNode = switchTypeNode.getNextSibling();
        while (caseOrDefaultNode != null) {
            UnionMember unionMember = new UnionMember();
            AST typeNode;
            if (IDLTokenTypes.LITERAL_default == caseOrDefaultNode.getType()) {
                unionMember.setDefault(true);
                typeNode = caseOrDefaultNode.getFirstChild();
            } else {
                unionMember.setDefault(false);
                AST caseValueNode = caseOrDefaultNode.getFirstChild();
                unionMember.setDiscriminatorValue(caseValueNode.getText());
                typeNode = caseValueNode.getNextSibling();
            }


            unionMember.setDataType(findDataType(typeNode, unName));

            AST memberNode = typeNode.getNextSibling();
            unionMember.setName(memberNode.toString());
            unionType.addMember(unionMember);
            caseOrDefaultNode = caseOrDefaultNode.getNextSibling();
        }
        return unionType;
    }

    private void visitAndAddTypedefs(AST node, String moduleName) throws InvalidIDLException {
        AST typedefNode = node.getFirstChild();

        DataType dataType;
        //SequenceType sequence = null;
        //if (typedefNode.getType()==IDLTokenTypes.LITERAL_sequence) {
        //    sequence = visitAnonymousSequence(typedefNode);
        //    dataType = sequence.getDataType();
        //} else {
            dataType = findDataType(typedefNode, null, true, true);
        //}
        AST typedefNameNode = typedefNode.getNextSibling();
        AST dimensionNode;
        String typedefName = typedefNameNode.toString();
        Typedef typedef;
        while (typedefNameNode != null) {
            int dimensions = typedefNameNode.getNumberOfChildren();
            if (dimensions > 0) {
                dimensionNode = typedefNameNode.getFirstChild();
                ArrayType arrayType = null;
                ArrayType rootArrayType = null;
                int i = 1;
                while(dimensionNode!=null) {
                    ArrayType temp = new ArrayType();
                    temp.setElementModule(moduleName);
                    temp.setElementName(typedefName);
                    temp.setDepth(i);
                    i++;
                    if (arrayType != null) {
                        arrayType.setDataType(temp);
                    } else {
                        rootArrayType = temp;
                    }
                    arrayType = temp;
                    arrayType.setElementCount(Integer.parseInt(dimensionNode.getText()));
                    dimensionNode = dimensionNode.getNextSibling();
                }
                if (arrayType != null) {
                    arrayType.setDataType(dataType);
                }
                dataType = rootArrayType;
            }
            typedef = new Typedef();
            typedef.setDataType(dataType);
            typedef.setModule(moduleName);
            typedef.setName(typedefName);
            idl.addType(typedef);
            typedefNameNode = typedefNameNode.getNextSibling();
        }
    }

    private SequenceType visitAnonymousSequence(AST node, String parentName, boolean root) throws InvalidIDLException {
        AST typeNode = node.getFirstChild();
        SequenceType sequenceType = new SequenceType();
        DataType dataType = findDataType(typeNode, parentName, false, false);
        sequenceType.setDataType(dataType);
        sequenceType.setElementModule(module);
        AST elementNode = node.getNextSibling();
        if (elementNode != null && root) {
            String elementName = elementNode.getText();
            sequenceType.setName(elementName);
            SequenceType tempSeqType = sequenceType;
            int i = 1;
            DataType tempDataType;
            while(true) {
                tempSeqType.setElementName(elementName);
                tempSeqType.setDepth(i);
                i++;
                tempDataType = tempSeqType.getDataType();
                if (tempDataType instanceof SequenceType) {
                    tempSeqType = (SequenceType) tempDataType;    
                } else {
                    break;
                }
            }
        }

        AST countNode = typeNode.getNextSibling();
        if (countNode!=null) {
            sequenceType.setElementCount(Integer.parseInt(countNode.getText()));
            //sequenceType.setBounded(true);
        } else {
            sequenceType.setElementCount(0);
            //sequenceType.setBounded(false);
        }
        return sequenceType;
    }

}
