/**
 * 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 javax.wsdl.Binding;
import javax.wsdl.BindingFault;
import javax.wsdl.BindingInput;
import javax.wsdl.BindingOperation;
import javax.wsdl.BindingOutput;
import javax.wsdl.Definition;
import javax.wsdl.Fault;
import javax.wsdl.Input;
import javax.wsdl.Message;
import javax.wsdl.Operation;
import javax.wsdl.Output;
import javax.wsdl.Part;
import javax.wsdl.PortType;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.ExtensionRegistry;
import javax.xml.namespace.QName;

import antlr.collections.AST;

import org.apache.cxf.binding.corba.wsdl.ArgType;
import org.apache.cxf.binding.corba.wsdl.CorbaConstants;
import org.apache.cxf.binding.corba.wsdl.CorbaTypeImpl;
import org.apache.cxf.binding.corba.wsdl.OperationType;
import org.apache.cxf.binding.corba.wsdl.RaisesType;
import org.apache.cxf.tools.corba.common.ReferenceConstants;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaComplexType;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaSequence;
import org.apache.ws.commons.schema.XmlSchemaType;

public class OperationVisitor extends VisitorBase {

    private static final String REQUEST_SUFFIX = "Request";
    private static final String RESPONSE_SUFFIX = "Response";

    private static final String IN_PARAMETER = "inparameter";
    private static final String OUT_PARAMETER = "outparameter";
    //private static final String INOUT_PARAMETER = "inoutparameter";
    private static final String RETURN_PARAMETER = "return";

    private ExtensionRegistry extReg;
    private PortType portType;
    private Binding binding;

    private Message inputMsg;
    private Message outputMsg;


    private OperationType corbaOperation;

    public OperationVisitor(Scope scope,
                            Definition defn,
                            XmlSchema schemaRef,
                            WSDLASTVisitor wsdlVisitor,
                            PortType wsdlPortType,
                            Binding wsdlBinding) {
        super(scope, defn, schemaRef, wsdlVisitor);
        extReg = definition.getExtensionRegistry();
        portType = wsdlPortType;
        binding = wsdlBinding;
    }

    public static boolean accept(Scope scope,
                                 Definition def,
                                 XmlSchema schema,
                                 AST node,
                                 WSDLASTVisitor wsdlVisitor) {
        boolean result = false;
        AST node2 = node.getFirstChild();

        if (null == node2) {
           // throw whatever appropriate error
           // or do nothing and return false
        } else if (node2.getType() == IDLTokenTypes.LITERAL_oneway) {
            result = true;
        } else {
            int type = node2.getType();
            result =
                type == IDLTokenTypes.LITERAL_void
                || PrimitiveTypesVisitor.accept(node2)
                || StringVisitor.accept(node2)
                || ScopedNameVisitor.accept(scope, def, schema, node2, wsdlVisitor)
                //REVISIT, change this to be def & then schema
                || ObjectReferenceVisitor.accept(scope, schema, def, node2, wsdlVisitor);
        }
        return result;
    }

    public void visit(AST node) {
        // <op_dcl> ::= [<op_attribute>] <op_type_spec>
        //              <identifier> <parameter_dcls>
        //              [<raises_expr>] [<context_expr>]
        // <op_attribute> ::= "oneway"
        // <op_type_spec> ::= <param_type_spec>
        //                  | "void"
        // <parameter_dcls> ::= "(" <param_dcl> {"," <param_dcl>}* ")"
        //                    | "(" ")"
        // <raises_expr> ::= "raises" "(" <scoped_name> {"," <scoped_name>}* ")"
        // <context_expr> ::= "context" "(" <string_literal> {"," <string_literal>}* ")"

        QName operationQName = new QName(schema.getTargetNamespace(), node.toString());
        boolean isDuplicate = false;
        if (schema.getElements().containsKey(operationQName)) {
            isDuplicate = true;
        }

        Operation operation = generateOperation(operationQName.getLocalPart(), isDuplicate);

        final BindingOperation bindingOperation;
        if (isDuplicate) {
            bindingOperation = generateBindingOperation(binding, operation, operationQName.getLocalPart());
        } else {
            bindingOperation = generateBindingOperation(binding, operation, operation.getName());
        }

        XmlSchemaSequence inputWrappingSequence = new XmlSchemaSequence();
        XmlSchemaElement inputElement = generateWrapper(new QName(schema.getTargetNamespace(),
                                                                  operation.getName()),
                                                        inputWrappingSequence);
        inputMsg = generateInputMessage(operation, bindingOperation);
        generateInputPart(inputMsg, inputElement);

        // <op_attribute>
        node = node.getFirstChild();
        XmlSchemaSequence outputWrappingSequence = null;
        if (node != null && (node.getType() == IDLTokenTypes.LITERAL_oneway)) {
            // oneway operations map to operations with only input message
            // no outputMsg nor outputPart need be created
            node = node.getNextSibling();
        } else {
            // normal operations map to request-response operations
            // with input and output messages
            outputWrappingSequence = new XmlSchemaSequence();
            XmlSchemaElement outputElement = generateWrapper(new QName(schema.getTargetNamespace(),
                                                      operation.getName() + RESPONSE_SUFFIX),
                                            outputWrappingSequence);
            outputMsg = generateOutputMessage(operation, bindingOperation);
            generateOutputPart(outputMsg, outputElement);
        }

        // <op_type_spec>
        visitOpTypeSpec(node, outputWrappingSequence);

        // <parameter_dcls>
        node = TypesUtils.getCorbaTypeNameNode(node);
        while (ParamDclVisitor.accept(node)) {

            ParamDclVisitor visitor = new ParamDclVisitor(getScope(),
                                                          definition,
                                                          schema,
                                                          wsdlVisitor,
                                                          inputWrappingSequence,
                                                          outputWrappingSequence,
                                                          corbaOperation);
            visitor.visit(node);

            node = node.getNextSibling();
        }

        // <raises_expr>
        if (node != null
            && node.getType() == IDLTokenTypes.LITERAL_raises) {
            node = node.getFirstChild();

            while (node != null) {
                //
                ScopedNameVisitor visitor = new ScopedNameVisitor(getScope(),
                                                                  definition,
                                                                  schema,
                                                                  wsdlVisitor);
                visitor.setExceptionMode(true);
                visitor.visit(node);
                CorbaTypeImpl corbaType = visitor.getCorbaType();
                XmlSchemaType schemaType = visitor.getSchemaType();
                //REVISIT, schema type ends with Type for exceptions, so strip it to get the element name.
                int pos = schemaType.getQName().getLocalPart().indexOf("Type");
                QName elementQName = new QName(schemaType.getQName().getNamespaceURI(),
                                               schemaType.getQName().getLocalPart().substring(0, pos));
                createFaultMessage(corbaType, operation, bindingOperation, elementQName);
                node = node.getNextSibling();
                visitor.setExceptionMode(false);
            }
        }

    }

    private Operation generateOperation(String name, boolean isDuplicate) {
        Operation op = definition.createOperation();
        if (isDuplicate) {
            // Replace '.' in the scoping name with '_'.  This results in the final
            // operation name being more readable (otherwise generated code removes
            // the '.' and merges all of the scoping names together)
            String prefix = getScope().toString().replace('.', '_');
            name = prefix + "_" + name;
        }
        op.setName(name);
        op.setUndefined(false);
        portType.addOperation(op);
        return op;
    }

    private BindingOperation generateBindingOperation(Binding wsdlBinding, Operation op,
                                                      String corbaOpName) {
        BindingOperation bindingOperation = definition.createBindingOperation();
        //OperationType operationType = null;
        try {
            corbaOperation = (OperationType)extReg.createExtension(BindingOperation.class,
                                                                   CorbaConstants.NE_CORBA_OPERATION);
        } catch (WSDLException ex) {
            throw new RuntimeException(ex);
        }
        corbaOperation.setName(corbaOpName);
        bindingOperation.addExtensibilityElement((ExtensibilityElement)corbaOperation);
        bindingOperation.setOperation(op);
        bindingOperation.setName(op.getName());
        binding.addBindingOperation(bindingOperation);
        return bindingOperation;
    }


    public Message generateInputMessage(Operation operation, BindingOperation bindingOperation) {
        Message msg = definition.createMessage();
        QName msgName;
        if (!mapper.isDefaultMapping()) {
            //mangle the message name
            //REVISIT, do we put in the entire scope for mangling
            msgName = new QName(definition.getTargetNamespace(),
                                getScope().tail() + "." + operation.getName());
        } else {
            msgName = new QName(definition.getTargetNamespace(), operation.getName());
        }
        msg.setQName(msgName);
        msg.setUndefined(false);

        String inputName = operation.getName() + REQUEST_SUFFIX;
        Input input = definition.createInput();
        input.setName(inputName);
        input.setMessage(msg);

        BindingInput bindingInput = definition.createBindingInput();
        bindingInput.setName(inputName);

        bindingOperation.setBindingInput(bindingInput);
        operation.setInput(input);

        definition.addMessage(msg);

        return msg;
    }

    public Message generateOutputMessage(Operation operation, BindingOperation bindingOperation) {
        Message msg = definition.createMessage();
        QName msgName;
        if (!mapper.isDefaultMapping()) {
            //mangle the message name
            //REVISIT, do we put in the entire scope for mangling
            msgName = new QName(definition.getTargetNamespace(),
                                getScope().tail() + "." + operation.getName() + RESPONSE_SUFFIX);
        } else {
            msgName = new QName(definition.getTargetNamespace(),
                                operation.getName() + RESPONSE_SUFFIX);
        }
        msg.setQName(msgName);
        msg.setUndefined(false);

        String outputName = operation.getName() + RESPONSE_SUFFIX;
        Output output = definition.createOutput();
        output.setName(outputName);
        output.setMessage(msg);

        BindingOutput bindingOutput = definition.createBindingOutput();
        bindingOutput.setName(outputName);

        bindingOperation.setBindingOutput(bindingOutput);
        operation.setOutput(output);

        definition.addMessage(msg);

        return msg;
    }

    private Part generateInputPart(Message inputMessage, XmlSchemaElement element) {
        // message - part
        Part part = definition.createPart();
        part.setName(IN_PARAMETER);
        part.setElementName(element.getQName());
        inputMessage.addPart(part);
        return part;
    }

    private Part generateOutputPart(Message outputMessage, XmlSchemaElement element) {
        // message - part
        Part part = definition.createPart();
        part.setName(OUT_PARAMETER);
        part.setElementName(element.getQName());
        outputMessage.addPart(part);
        return part;
    }

    /*-
     * Build the Wrapped Document Style wrapping elements
     * i.e. <xs:element name="...">
     *       <xs:complexType>
     *        <xs:sequence>
     *         ...
     *        </xs:sequence>
     *       </xs:complexType>
     *      </xs:element>
     */
    private XmlSchemaElement generateWrapper(QName el, XmlSchemaSequence wrappingSequence) {
        XmlSchemaComplexType schemaComplexType = new XmlSchemaComplexType(schema, false);
        schemaComplexType.setParticle(wrappingSequence);

        XmlSchemaElement wrappingSchemaElement = new XmlSchemaElement(schema, true);
        wrappingSchemaElement.setName(el.getLocalPart());
        wrappingSchemaElement.setSchemaType(schemaComplexType);

        return wrappingSchemaElement;
    }

    private XmlSchemaElement addElement(XmlSchemaSequence schemaSequence,
                                        XmlSchemaType schemaType,
                                        Scope fqName,
                                        String name) {
        XmlSchemaElement element = new XmlSchemaElement(schema, false);
        element.setName(name);
        if (schemaType != null) {
            element.setSchemaTypeName(schemaType.getQName());
            if (schemaType.getQName().equals(ReferenceConstants.WSADDRESSING_TYPE)) {
                element.setNillable(true);
            }
        } else {
            wsdlVisitor.getDeferredActions().
                add(fqName, new OperationDeferredAction(element));
        }
        schemaSequence.getItems().add(element);

        return element;
    }

    private void visitOpTypeSpec(AST node, XmlSchemaSequence outputWrappingSequence) {
        if (node.getType() == IDLTokenTypes.LITERAL_void) {
            // nothing to do here, move along
            return;
        }
        ParamTypeSpecVisitor visitor = new ParamTypeSpecVisitor(getScope(),
                                                                definition,
                                                                schema,
                                                                wsdlVisitor);

        visitor.visit(node);

        XmlSchemaType schemaType = visitor.getSchemaType();
        CorbaTypeImpl corbaType = visitor.getCorbaType();
        Scope fqName = visitor.getFullyQualifiedName();

        addElement(outputWrappingSequence, schemaType, fqName, RETURN_PARAMETER);
        addCorbaReturn(corbaType, fqName, RETURN_PARAMETER);
    }

    private void addCorbaReturn(CorbaTypeImpl corbaType, Scope fqName, String partName) {
        ArgType param = new ArgType();
        param.setName(partName);
        if (corbaType != null) {
            param.setIdltype(corbaType.getQName());
        } else {
            wsdlVisitor.getDeferredActions().
                add(fqName, new OperationDeferredAction(param));
        }
        corbaOperation.setReturn(param);
    }

    private void createFaultMessage(CorbaTypeImpl corbaType,
                                    Operation operation,
                                    BindingOperation bindingOperation,
                                    QName elementQName) {
        String exceptionName = corbaType.getQName().getLocalPart();

        Definition faultDef = manager.getWSDLDefinition(elementQName.getNamespaceURI());
        if (faultDef == null) {
            faultDef = definition;
        }
        Message faultMsg = faultDef.getMessage(new QName(faultDef.getTargetNamespace(), exceptionName));
        if (faultMsg == null) {
            throw new RuntimeException("Fault message for exception " + exceptionName + " not found");
        }

        // porttype - operation - fault
        Fault fault = definition.createFault();
        fault.setMessage(faultMsg);
        fault.setName(faultMsg.getQName().getLocalPart());
        operation.addFault(fault);

        // binding - operation - corba:operation - corba:raises
        RaisesType raisesType = new RaisesType();
        raisesType.setException(new QName(typeMap.getTargetNamespace(),
                                          exceptionName));
        corbaOperation.getRaises().add(raisesType);

        // binding - operation - fault
        BindingFault bindingFault = definition.createBindingFault();
        bindingFault.setName(faultMsg.getQName().getLocalPart());
        bindingOperation.addBindingFault(bindingFault);

        //add the fault element namespace to the definition
        String nsURI = elementQName.getNamespaceURI();
        manager.addWSDLDefinitionNamespace(definition, mapper.mapNSToPrefix(nsURI), nsURI);
    }
}
