/**
 * 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.cxf.tools.corba.processors.idl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.wsdl.Binding;
import javax.wsdl.BindingOperation;
import javax.wsdl.Definition;
import javax.wsdl.Fault;
import javax.wsdl.Operation;
import javax.wsdl.PortType;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.ExtensionRegistry;
import javax.xml.namespace.QName;

import antlr.collections.AST;

import org.apache.cxf.binding.corba.wsdl.BindingType;
import org.apache.cxf.binding.corba.wsdl.CorbaConstants;
import org.apache.cxf.binding.corba.wsdl.CorbaTypeImpl;
import org.apache.cxf.helpers.CastUtils;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaType;

public class PortTypeVisitor extends VisitorBase {   

    ExtensionRegistry extReg;
    PortType portType;
    Definition rootDefinition;

    public PortTypeVisitor(Scope scope,
                           Definition defn,
                           XmlSchema schemaRef,
                           WSDLASTVisitor wsdlASTVisitor) {
        super(scope, defn, schemaRef, wsdlASTVisitor);
        extReg = definition.getExtensionRegistry();
        rootDefinition = wsdlASTVisitor.getDefinition();
    }

    public static boolean accept(AST node) {
        if (node.getType() == IDLTokenTypes.LITERAL_interface) {
            return true;
        }
        return false;
    }
    
    public void visit(AST node) {
        // <interface> ::= <interface_dcl>
        //               | <forward_dcl>
        // <interface_dcl> ::= <interface_header> "{" <interface_body> "}"
        // <forward_dcl> ::= ["abstract" | "local"] "interface" <identifier>
        // <interface_header> ::= ["abstract" | "local"] "interface" <identifier>
        //                        [<interface_inheritance_spec>]
        // <interface_body> ::= <export>*
        // <export> ::= <type_dcl> ";"
        //            | <const_dcl> ";"
        //            | <except_dcl> ";"
        //            | <attr_dcl> ";"
        //            | <op_dcl> ";"
        // <interface_inheritance_spec> ::= ":" <interface_name> { "," <interface_name> }*
        // <interface_name> ::= <scoped_name>
        
        
        AST identifierNode = node.getFirstChild();   
        if (identifierNode.getText().equals("local")) {
            identifierNode = identifierNode.getNextSibling();
        }
        // Check if its a forward declaration
        if (identifierNode.getFirstChild() == null && identifierNode.getNextSibling() == null) {
            visitForwardDeclaredInterface(identifierNode);        
        } else {
            visitInterface(identifierNode);                       
        }       
    }

    // Visits a fully declared interface
    private void visitInterface(AST identifierNode) {
        try {
            String interfaceName = identifierNode.toString();        
            Scope interfaceScope = new Scope(getScope(), interfaceName);
            portType = definition.createPortType();

            String portTypeName = interfaceScope.toString();

            XmlSchema newSchema = schema;
            if (!mapper.isDefaultMapping()) {
                portTypeName = interfaceScope.tail();
                //add a schema based on the interface
                String tns = mapper.map(interfaceScope);
                newSchema = manager.createXmlSchemaForDefinition(definition, tns, schemas);
                definition.addNamespace(interfaceScope.toString("_"), tns);
            }
            String tns = definition.getTargetNamespace();
            portType.setQName(new QName(tns, portTypeName));
            definition.addPortType(portType);
            portType.setUndefined(false);

            Binding binding = createBinding(interfaceScope.toString());
        
            AST specNode = identifierNode.getNextSibling();        
            if  (specNode.getType() == IDLTokenTypes.LCURLY) {
                specNode = specNode.getNextSibling();
            }
        
            AST exportNode = null;        
            if (specNode.getType() == IDLTokenTypes.RCURLY) {
                exportNode = specNode.getNextSibling();        
            } else if (specNode.getType() == IDLTokenTypes.COLON) {
                exportNode = visitInterfaceInheritanceSpec(specNode, binding, interfaceScope);
                exportNode = exportNode.getNextSibling();
            } else {
                exportNode = specNode;
            }
           
            while (exportNode != null  
                   && exportNode.getType() != IDLTokenTypes.RCURLY) {
            
                if (TypeDclVisitor.accept(exportNode)) {
                    TypeDclVisitor visitor = new TypeDclVisitor(interfaceScope,
                                                                definition,
                                                                newSchema,
                                                                wsdlVisitor);
                    visitor.visit(exportNode);
                } else if (ConstVisitor.accept(exportNode)) {
                    ConstVisitor visitor = new ConstVisitor(interfaceScope,
                                                            definition,
                                                            newSchema,
                                                            wsdlVisitor);
                    visitor.visit(exportNode);
                } else if (ExceptionVisitor.accept(exportNode)) {
                    ExceptionVisitor visitor = new ExceptionVisitor(interfaceScope,
                                                                    definition,
                                                                    newSchema,
                                                                    wsdlVisitor);
                    visitor.visit(exportNode);
                } else if (AttributeVisitor.accept(exportNode)) {
                    AttributeVisitor attributeVisitor = new AttributeVisitor(interfaceScope,
                                                                             definition,
                                                                             newSchema,
                                                                             wsdlVisitor,
                                                                             portType,
                                                                             binding);
                    attributeVisitor.visit(exportNode);
                } else if (OperationVisitor.accept(interfaceScope,
                                                   definition,
                                                   newSchema, 
                                                   exportNode,
                                                   wsdlVisitor)) {
                    OperationVisitor visitor = new OperationVisitor(interfaceScope,
                                                                    definition,
                                                                    newSchema,
                                                                    wsdlVisitor,
                                                                    portType,
                                                                    binding);
                    visitor.visit(exportNode);
                } else {
                    throw new RuntimeException("[InterfaceVisitor] Invalid IDL: unknown element "
                                               + exportNode.toString());
                }
            
                exportNode = exportNode.getNextSibling();
            }

            // Once we've finished declaring the interface, we should make sure it has been removed 
            // from the list of scopedNames so that we indicate that is no longer simply forward
            // declared.
            Scope scopedName = new Scope(getScope(), identifierNode);
            scopedNames.remove(scopedName);
        
            if (wsdlVisitor.getDeferredActions() != null) {
                handleDeferredActions(wsdlVisitor.getDeferredActions(), scopedName, identifierNode);
            }

            if (!mapper.isDefaultMapping()) {
                manager.deferAttachSchemaToWSDL(definition, newSchema, false);
                //manager.attachSchemaToWSDL(definition, newSchema, false);
            }

        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }           
    }
    
    private void handleDeferredActions(DeferredActionCollection deferredActions,
                                       Scope scopedName,
                                       AST identifierNode) {
        List<DeferredAction> list = deferredActions.getActions(scopedName);
        if ((list != null) && !list.isEmpty()) {
            XmlSchemaType stype = null;
            CorbaTypeImpl ctype = null;
            if (ObjectReferenceVisitor.accept(getScope(), schema, 
                                              definition, identifierNode, wsdlVisitor)) {
                ObjectReferenceVisitor visitor = new ObjectReferenceVisitor(getScope(),
                                                                            definition,
                                                                            schema,
                                                                            wsdlVisitor);
                visitor.visit(identifierNode);
                stype = visitor.getSchemaType();
                ctype = visitor.getCorbaType();
            }
            for (DeferredAction action : list) {
                if (action instanceof SchemaDeferredAction
                    && (stype != null) && (ctype != null)) {
                    SchemaDeferredAction schemaAction = (SchemaDeferredAction) action;
                    schemaAction.execute(stype, ctype);
                }
            }
            deferredActions.removeScope(scopedName);
        }
    }

    
    public Binding createBinding(String scopedPortTypeName) {
        StringBuilder bname = new StringBuilder(scopedPortTypeName + "CORBABinding");
        QName bqname = new QName(rootDefinition.getTargetNamespace(),
                                 bname.toString());
        int count = 0;
        while (queryBinding(bqname)) {
            bname.append(count);
            bqname = new QName(rootDefinition.getTargetNamespace(), bname.toString());
        }
        Binding binding = rootDefinition.createBinding();
        binding.setPortType(portType);
        binding.setQName(bqname);

        try {
            BindingType bindingType = (BindingType)
                extReg.createExtension(Binding.class, CorbaConstants.NE_CORBA_BINDING);
            String pragmaPrefix = (this.getWsdlVisitor().getPragmaPrefix() != null 
                                        && this.getWsdlVisitor().getPragmaPrefix().length() > 0) 
                ? this.getWsdlVisitor().getPragmaPrefix() + "/" : ""; 
            bindingType.setRepositoryID(CorbaConstants.REPO_STRING
                                        + pragmaPrefix
                                        + scopedPortTypeName.replace('.', '/')
                                        + CorbaConstants.IDL_VERSION);
            binding.addExtensibilityElement(bindingType);
        } catch (WSDLException ex) {
            throw new RuntimeException(ex);
        }
        binding.setUndefined(false);
        rootDefinition.addBinding(binding);
        return binding;
    }

    private boolean queryBinding(QName bqname) {
        Collection<Binding> bindings = CastUtils.cast(definition.getBindings().values());
        for (Binding binding : bindings) {
            if (binding.getQName().getLocalPart().equals(bqname.getLocalPart())) {
                return true;
            }
        }
        return false;
    }
    
    private AST visitInterfaceInheritanceSpec(AST interfaceInheritanceSpecNode, Binding binding,
                                              Scope childScope) {
        // <interface_inheritance_spec> ::= ":" <interface_name> { "," <interface_name> }*
                
        AST interfaceNameNode = interfaceInheritanceSpecNode.getFirstChild();
        BindingType corbaBinding = findCorbaBinding(binding);
        List<Scope> inheritedScopes = new ArrayList<Scope>();
        
        while (interfaceNameNode != null) {            
            //check for porttypes in current & parent scopes
            Scope interfaceScope = null;
            PortType intf = null;
            if (ScopedNameVisitor.isFullyScopedName(interfaceNameNode)) {
                interfaceScope = ScopedNameVisitor.getFullyScopedName(new Scope(), interfaceNameNode);
                intf = findPortType(interfaceScope);
            }
            Scope currentScope = getScope();
            while (intf == null
                   && currentScope != currentScope.getParent()) {
                if (ScopedNameVisitor.isFullyScopedName(interfaceNameNode)) {
                    interfaceScope = ScopedNameVisitor.getFullyScopedName(currentScope, interfaceNameNode);
                } else {
                    interfaceScope = new Scope(currentScope, interfaceNameNode.toString());
                }
                intf = findPortType(interfaceScope);
                currentScope = currentScope.getParent();
            }
            
            if (intf == null) {
                if (ScopedNameVisitor.isFullyScopedName(interfaceNameNode)) {
                    interfaceScope = ScopedNameVisitor.getFullyScopedName(new Scope(), interfaceNameNode);
                } else {
                    interfaceScope = new Scope(new Scope(), interfaceNameNode);
                }
                intf = findPortType(interfaceScope);
            }

            if (intf == null) {
                throw new RuntimeException("[InterfaceVisitor] Unknown Interface: "
                                           + interfaceNameNode.toString());
            }

            Scope defnScope = interfaceScope.getParent();
            Definition defn = manager.getWSDLDefinition(mapper.map(defnScope));
            inheritedScopes.add(interfaceScope);
            
            if (defn != null && !defn.getTargetNamespace().equals(definition.getTargetNamespace())) {
                String key = defnScope.toString("_");
                String fileName = getWsdlVisitor().getOutputDir()
                    + System.getProperty("file.separator")
                    + key;
                manager.addWSDLDefinitionImport(definition,
                                                defn,
                                                key,
                                                fileName);
            }                        
            
            Binding inheritedBinding = findBinding(intf);
            BindingType inheritedCorbaBinding = findCorbaBinding(inheritedBinding);
            corbaBinding.getBases().add(inheritedCorbaBinding.getRepositoryID());

            //add all the operations of the inherited port type.            
            for (Operation op : CastUtils.cast(intf.getOperations(), Operation.class)) {

                //check to see all the inherited namespaces are added.
                String inputNS = op.getInput().getMessage().getQName().getNamespaceURI();
                manager.addWSDLDefinitionNamespace(definition, mapper.mapNSToPrefix(inputNS), inputNS);

                // Make sure we import the wsdl for the input namespace
                if (definition.getImports().get(inputNS) == null && !mapper.isDefaultMapping()
                    && !definition.getTargetNamespace().equals(inputNS)) {
                    manager.addWSDLDefinitionImport(definition, 
                                                    manager.getWSDLDefinition(inputNS), 
                                                    mapper.mapNSToPrefix(inputNS), 
                                                    manager.getImportedWSDLDefinitionFile(inputNS));
                }
                if (op.getOutput() != null) {
                    String outputNS = op.getOutput().getMessage().getQName().getNamespaceURI();
                    manager.addWSDLDefinitionNamespace(definition, mapper.mapNSToPrefix(outputNS), outputNS);

                    // Make sure we import the wsdl for the output namespace
                    if (definition.getImports().get(outputNS) == null && !mapper.isDefaultMapping()
                        && !definition.getTargetNamespace().equals(outputNS)) {
                        manager.addWSDLDefinitionImport(definition, 
                                                        manager.getWSDLDefinition(outputNS), 
                                                        mapper.mapNSToPrefix(outputNS), 
                                                        manager.getImportedWSDLDefinitionFile(outputNS));
                    }
                }

                
                
                for (Iterator<Fault> faults = CastUtils.cast(op.getFaults().values().iterator());
                    faults.hasNext();) {
                    
                    String faultNS = faults.next().getMessage().getQName().getNamespaceURI();
                    manager.addWSDLDefinitionNamespace(definition, mapper.mapNSToPrefix(faultNS), faultNS);
                    // Make sure we import the wsdl for the fault namespace
                    if (definition.getImports().get(faultNS) == null && !mapper.isDefaultMapping()
                        && !definition.getTargetNamespace().equals(faultNS)) {
                        manager.addWSDLDefinitionImport(definition,
                                                        manager.getWSDLDefinition(faultNS), 
                                                        mapper.mapNSToPrefix(faultNS), 
                                                        manager.getImportedWSDLDefinitionFile(faultNS));
                    }
                }
                
                portType.addOperation(op);
            }

            //add all the binding extensions of the inherited corba binding
            for (Iterator<BindingOperation> it = 
                    CastUtils.cast(inheritedBinding.getBindingOperations().iterator());
                 it.hasNext();) {
                binding.addBindingOperation(it.next());
            }
            interfaceNameNode = interfaceNameNode.getNextSibling();
        }
        
        if ((!inheritedScopes.isEmpty()) 
            && (wsdlVisitor.getInheritedScopeMap() != null)) {
            wsdlVisitor.getInheritedScopeMap().put(childScope, inheritedScopes);             
        }
        
        return interfaceInheritanceSpecNode.getNextSibling();
    }
    
    private void visitForwardDeclaredInterface(AST identifierNode) {
        String interfaceName = identifierNode.toString();        
        Scope interfaceScope = new Scope(getScope(), interfaceName);
        
        ScopeNameCollection scopedNames = wsdlVisitor.getScopedNames();
        if (scopedNames.getScope(interfaceScope) == null) {
            scopedNames.add(interfaceScope);
        }
        
    }


    private PortType findPortType(Scope intfScope) {
        String tns = mapper.map(intfScope.getParent());
        String intfName = intfScope.toString();
        Definition defn = definition;
        if (tns != null) {           
            defn = manager.getWSDLDefinition(tns);
            intfName = intfScope.tail();
        }
        if (defn != null) {
            tns = defn.getTargetNamespace();
            QName name = new QName(tns, intfName);
            return defn.getPortType(name);
        }
        return null;
    }

    private Binding findBinding(PortType intf) {
        Object[] bindings = rootDefinition.getBindings().values().toArray();   
        for (int i = 0; i < bindings.length; i++) {
            Binding binding = (Binding) bindings[i];
            if (binding.getPortType().getQName().equals(intf.getQName())) {
                return binding;
            }
        }
        throw new RuntimeException("[InterfaceVisitor] Couldn't find binding for porttype "
                                   + intf.getQName());
    }

    private BindingType findCorbaBinding(Binding binding) {
        List<?> list = binding.getExtensibilityElements();
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i) instanceof BindingType) {
                return (BindingType) list.get(i);
            }
        }
        throw new RuntimeException("[InterfaceVisitor] Couldn't find Corba binding in Binding "
                                   + binding.getQName());
    }
}
