/**
 * 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.misc.processor;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.wsdl.Binding;
import javax.wsdl.BindingInput;
import javax.wsdl.BindingOperation;
import javax.wsdl.BindingOutput;
import javax.wsdl.Input;
import javax.wsdl.Operation;
import javax.wsdl.Output;
import javax.wsdl.Port;
import javax.wsdl.PortType;
import javax.wsdl.Service;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.xml.WSDLWriter;
import javax.xml.namespace.QName;

import org.apache.cxf.common.i18n.Message;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.tools.common.ToolConstants;
import org.apache.cxf.tools.common.ToolException;
import org.apache.cxf.tools.misc.processor.address.Address;
import org.apache.cxf.tools.misc.processor.address.AddressFactory;
import org.apache.cxf.wsdl.WSDLConstants;
import org.apache.cxf.wsdl.WSDLExtensibilityPlugin;

public class WSDLToXMLProcessor extends AbstractWSDLToProcessor {

    private static final String NEW_FILE_NAME_MODIFIER = "-xmlbinding";

    private Map<QName, Service> services;
    private Service service;
    private Map<String, Port> ports;
    private Port port;

    private Map<QName, PortType> portTypes;
    private PortType portType;
    private Binding binding;

    public void process() throws ToolException {
        init();
        if (isBindingExisted()) {
            Message msg = new Message("BINDING_ALREADY_EXIST", LOG);
            throw new ToolException(msg);
        }
        if (!isPortTypeExisted()) {
            Message msg = new Message("PORTTYPE_NOT_EXIST", LOG);
            throw new ToolException(msg);
        }
        if (isServicePortExisted()) {
            Message msg = new Message("SERVICE_PORT_EXIST", LOG);
            throw new ToolException(msg);
        }
        doAppendBinding();
        doAppendService();
        writeToWSDL();
    }

    private boolean isServicePortExisted() {
        return isServiceExisted() && isPortExisted();
    }

    private boolean isServiceExisted() {
        services = CastUtils.cast(wsdlDefinition.getServices());
        if (services == null) {
            return false;
        }
        for (Map.Entry<QName, Service> entry : services.entrySet()) {
            String serviceName = entry.getKey().getLocalPart();
            if (serviceName.equals(env.get(ToolConstants.CFG_SERVICE))) {
                service = entry.getValue();
                break;
            }
        }
        return (service == null) ? false : true;
    }

    private boolean isPortExisted() {
        ports = CastUtils.cast(service.getPorts());
        if (ports == null) {
            return false;
        }
        for (Map.Entry<String, Port> entry : ports.entrySet()) {
            if (entry.getKey().equals(env.get(ToolConstants.CFG_PORT))) {
                port = entry.getValue();
                break;
            }
        }
        return (port == null) ? false : true;
    }

    private boolean isPortTypeExisted() {
        portTypes = CastUtils.cast(wsdlDefinition.getPortTypes());
        if (portTypes == null) {
            return false;
        }
        for (Map.Entry<QName, PortType> entry : portTypes.entrySet()) {
            String existPortName = entry.getKey().getLocalPart();
            if (existPortName.equals(env.get(ToolConstants.CFG_PORTTYPE))) {
                portType = entry.getValue();
                break;
            }
        }
        return (portType == null) ? false : true;
    }

    private boolean isBindingExisted() {
        Map<QName, Binding> bindings = CastUtils.cast(wsdlDefinition.getBindings());
        if (bindings == null) {
            return false;
        }
        for (Map.Entry<QName, Binding> entry : bindings.entrySet()) {
            String existBindingName = entry.getKey().getLocalPart();
            String bindingName = (String)env.get(ToolConstants.CFG_BINDING);
            if (bindingName.equals(existBindingName)) {
                binding = entry.getValue();
            }
        }
        return (binding == null) ? false : true;
    }

    protected void init() throws ToolException {
        parseWSDL((String)env.get(ToolConstants.CFG_WSDLURL));
        if (wsdlDefinition.getNamespace(ToolConstants.XML_FORMAT_PREFIX) == null) {
            wsdlDefinition.addNamespace(ToolConstants.XML_FORMAT_PREFIX, ToolConstants.NS_XML_FORMAT);
        }
        if (wsdlDefinition.getNamespace(ToolConstants.XML_HTTP_PREFIX) == null) {
            wsdlDefinition.addNamespace(ToolConstants.XML_HTTP_PREFIX, ToolConstants.NS_XML_HTTP);
        }
    }

    private void doAppendBinding() throws ToolException {
        if (binding == null) {
            binding = wsdlDefinition.createBinding();
            binding.setQName(new QName(wsdlDefinition.getTargetNamespace(), (String)env
                .get(ToolConstants.CFG_BINDING)));
            binding.setUndefined(false);
            binding.setPortType(portType);
        }
        setXMLBindingExtElement();
        addBindingOperation();
        wsdlDefinition.addBinding(binding);
    }

    private void setXMLBindingExtElement() throws ToolException {
        if (extReg == null) {
            extReg = wsdlFactory.newPopulatedExtensionRegistry();
        }
        try {
            binding.addExtensibilityElement(getWSDLPlugin("xml", Binding.class).createExtension(null));
        } catch (WSDLException wse) {
            Message msg = new Message("FAIL_TO_CREATE_XMLBINDING", LOG);
            throw new ToolException(msg, wse);
        }
    }

    @SuppressWarnings("unchecked")
    private void addBindingOperation() throws ToolException {
        List<Operation> ops = portType.getOperations();
        for (Operation op : ops) {
            BindingOperation bindingOperation = wsdlDefinition.createBindingOperation();
            bindingOperation.setName(op.getName());
            if (op.getInput() != null) {
                bindingOperation.setBindingInput(getBindingInput(op.getInput(), op.getName()));
            }
            if (op.getOutput() != null) {
                bindingOperation.setBindingOutput(getBindingOutput(op.getOutput(), op.getName()));
            }
            if (op.getFaults() != null && op.getFaults().size() > 0) {
                addXMLFaults(op, bindingOperation);
            }
            bindingOperation.setOperation(op);
            binding.addBindingOperation(bindingOperation);
        }
    }

    private BindingInput getBindingInput(Input input, String operationName) throws ToolException {
        BindingInput bi = wsdlDefinition.createBindingInput();
        bi.setName(input.getName());
        //This ext element in some scenario is optional, but if provided, won't cause error
        bi.addExtensibilityElement(getXMLBody(BindingInput.class, operationName));
        return bi;
    }

    private BindingOutput getBindingOutput(Output output, String operationName) throws ToolException {
        BindingOutput bo = wsdlDefinition.createBindingOutput();
        bo.setName(output.getName());
        bo.addExtensibilityElement(getXMLBody(BindingOutput.class, operationName));
        return bo;
    }

    private void addXMLFaults(Operation op, BindingOperation bo) {
        // TODO
    }

    private ExtensibilityElement getXMLBody(Class<?> clz, String operationName) throws ToolException {
        if (extReg == null) {
            extReg = wsdlFactory.newPopulatedExtensionRegistry();
        }
        Map<String, Object> args = new HashMap<>();
        args.put(QName.class.getName(), new QName(wsdlDefinition.getTargetNamespace(), operationName));
        args.put(Class.class.getName(), clz);

        try {
            return getWSDLPlugin("xml", clz).createExtension(args);
        } catch (WSDLException wse) {
            Message msg = new Message("FAIL_TO_CREATE_XMLBINDING", LOG);
            throw new ToolException(msg);
        }
    }

    private void doAppendService() throws ToolException {
        if (service == null) {
            service = wsdlDefinition.createService();
            service
                .setQName(new QName(WSDLConstants.WSDL_PREFIX, (String)env.get(ToolConstants.CFG_SERVICE)));
        }
        if (port == null) {
            port = wsdlDefinition.createPort();
            port.setName((String)env.get(ToolConstants.CFG_PORT));
            port.setBinding(binding);
        }
        setAddrElement();
        service.addPort(port);
        wsdlDefinition.addService(service);
    }

    private void setAddrElement() throws ToolException {
        Address address = AddressFactory.getInstance().getAddresser("xml");

        for (Map.Entry<String, String> entry : address.getNamespaces(env).entrySet()) {
            wsdlDefinition.addNamespace(entry.getKey(), entry.getValue());
        }

        WSDLExtensibilityPlugin generator = getWSDLPlugin("xml", Port.class);
        try {
            ExtensibilityElement extElement = generator.createExtension(address.buildAddressArguments(env));

            port.addExtensibilityElement(extElement);
        } catch (WSDLException wse) {
            Message msg = new Message("FAIL_TO_CREATE_SOAPADDRESS", LOG);
            throw new ToolException(msg);
        }
    }

    private void writeToWSDL() throws ToolException {
        WSDLWriter wsdlWriter = wsdlFactory.newWSDLWriter();
        Writer outputWriter = getOutputWriter(NEW_FILE_NAME_MODIFIER);
        try {
            wsdlWriter.writeWSDL(wsdlDefinition, outputWriter);
        } catch (WSDLException wse) {
            Message msg = new Message("FAIL_TO_WRITE_WSDL", LOG);
            throw new ToolException(msg);
        }
        try {
            outputWriter.close();
        } catch (IOException ioe) {
            Message msg = new Message("FAIL_TO_CLOSE_WSDL_FILE", LOG);
            throw new ToolException(msg);
        }
    }

}
