/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.soap.wsdl;

import java.io.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

import javax.xml.namespace.QName;

import static com.caucho.soap.wsdl.WSDLConstants.*;

import com.caucho.java.JavaWriter;
import com.caucho.util.L10N;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;

import com.caucho.xml.schema.Type;

/**
 * WSDL Definitions top level
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name="definitions", namespace=WSDL_NAMESPACE)
public class WSDLDefinitions extends WSDLExtensibleDocumented {
  @XmlTransient
  private static final L10N L = new L10N(WSDLDefinitions.class);

  @XmlAttribute(name="name")
  private String _name;

  @XmlAttribute(name="targetNamespace")
  private String _targetNamespace;

  @XmlElement(name="import", namespace=WSDL_NAMESPACE)
  private List<WSDLImport> _imports;

  @XmlElement(name="types", namespace=WSDL_NAMESPACE, required=true)
  private List<WSDLTypes> _types;

  @XmlElement(name="message", namespace=WSDL_NAMESPACE, required=true)
  private List<WSDLMessage> _messages;

  @XmlElement(name="portType", namespace=WSDL_NAMESPACE, required=true)
  private List<WSDLPortType> _portTypes;

  @XmlElement(name="binding", namespace=WSDL_NAMESPACE, required=true)
  private List<WSDLBinding> _bindings;

  @XmlElement(name="service", namespace=WSDL_NAMESPACE, required=true)
  private List<WSDLService> _services;

  @XmlTransient
  private final Map<QName,WSDLMessage> _messageMap 
    = new HashMap<QName,WSDLMessage>();

  @XmlTransient
  private final Map<QName,WSDLPortType> _portTypeMap 
    = new HashMap<QName,WSDLPortType>();

  @XmlTransient
  private final Map<QName,WSDLBinding> _bindingMap 
    = new HashMap<QName,WSDLBinding>();

  @XmlTransient
  private String _endpointAddress;

  @XmlTransient
  private String _bindingId;

  /**
   * Sets the definition name.
   */
  public void setName(String name)
  {
    _name = name;
  }

  public String getName()
  {
    return _name;
  }

  /**
   * Sets the target namespace
   */
  public void setTargetNamespace(String uri)
  {
    _targetNamespace = uri;
  }

  /**
   * Gets the target namespace
   */
  public String getTargetNamespace()
  {
    return _targetNamespace;
  }

  public List<WSDLImport> getImports()
  {
    if (_imports == null)
      return _imports = new ArrayList<WSDLImport>();

    return _imports;
  }

  public List<WSDLTypes> getTypes()
  {
    if (_types == null)
      return _types = new ArrayList<WSDLTypes>();

    return _types;
  }

  public List<WSDLMessage> getMessages()
  {
    if (_messages == null)
      return _messages = new ArrayList<WSDLMessage>();

    return _messages;
  }

  public List<WSDLPortType> getPortTypes()
  {
    if (_portTypes == null)
      return _portTypes = new ArrayList<WSDLPortType>();

    return _portTypes;
  }

  public List<WSDLBinding> getBindings()
  {
    if (_bindings == null)
      return _bindings = new ArrayList<WSDLBinding>();

    return _bindings;
  }

  public List<WSDLService> getServices()
  {
    if (_services == null)
      return _services = new ArrayList<WSDLService>();

    return _services;
  }

  public String getBindingId(QName serviceName, QName portName)
  {
    if (_bindingId == null) {
      if (! serviceName.getNamespaceURI().equals(getTargetNamespace()))
        return null;

      // first find out which binding we're looking for
      QName bindingName = null;

      if (_services != null) {
        for (int i = 0; i < _services.size(); i++) {
          WSDLService service = _services.get(i);

          if (serviceName.getLocalPart().equals(service.getName())) {
            List<WSDLPort> ports = service.getPorts();

            for (int j = 0; j < ports.size(); j++) {
              WSDLPort port = ports.get(j);

              if (portName.getLocalPart().equals(port.getName())) {
                bindingName = port.getBinding();
                break;
              }
            }

            if (bindingName != null)
              break;
          }
        }
      }

      if (bindingName != null && _bindings != null) {
        for (int i = 0; i < _bindings.size(); i++) {
          WSDLBinding binding = _bindings.get(i);

          if (bindingName.getLocalPart().equals(binding.getName())) {
            List<Object> any = binding.getAny();

            for (int j = 0; j < any.size(); j++) {
              if (any.get(j) instanceof SOAPBinding) {
                SOAPBinding soapBinding = (SOAPBinding) any.get(j);

                _bindingId = soapBinding.getTransport();

                return _bindingId;
              }
            }
          }
        }
      }
    }

    return _bindingId;
  }

  public String getEndpointAddress(QName serviceName, QName portName)
  {
    if (_endpointAddress == null) {
      if (! serviceName.getNamespaceURI().equals(getTargetNamespace()))
        return null;

      // dig down to find the <soap:address location="..."/>
      if (_services != null) {
        for (int i = 0; i < _services.size(); i++) {
          WSDLService service = _services.get(i);

          if (serviceName.getLocalPart().equals(service.getName())) {
            List<WSDLPort> ports = service.getPorts();

            for (int j = 0; j < ports.size(); j++) {
              WSDLPort port = ports.get(j);

              if (portName.getLocalPart().equals(port.getName())) {
                List<Object> any = port.getAny();

                for (int k = 0; k < any.size(); k++) {
                  if (any.get(k) instanceof SOAPAddress) {
                    SOAPAddress address = (SOAPAddress) any.get(k);

                    _endpointAddress = address.getLocation();

                    return _endpointAddress;
                  }
                }
              }
            }
          }
        }
      }
    }

    return _endpointAddress;
  }

  /**
   * Sets up the cached, transient data.
   **/
  public void afterUnmarshal(Unmarshaller u, Object o)
    throws JAXBException, WSDLValidationException
  {
    if (_messages != null) {
      for (WSDLMessage message : _messages) {
        QName name = null;

        if (_targetNamespace != null)
          name = new QName(_targetNamespace, message.getName());
        else
          name = new QName(message.getName());

        _messageMap.put(name, message);
      }
    }

    if (_portTypes != null) {
      for (WSDLPortType portType : _portTypes) {
        QName name = null;

        if (_targetNamespace != null)
          name = new QName(_targetNamespace, portType.getName());
        else
          name = new QName(portType.getName());

        _portTypeMap.put(name, portType);
      }
    }

    if (_bindings != null) { 
      for (WSDLBinding binding : _bindings) {
        QName name = null;

        if (_targetNamespace != null)
          name = new QName(_targetNamespace, binding.getName());
        else
          name = new QName(binding.getName());

        _bindingMap.put(name, binding);
      }
    }

    resolveImports(u);

    for (WSDLMessage message : getMessages()) {
      for (WSDLPart part : message.getParts()) {
        Type type = getType(part.getElement());

        if (type == null)
          throw new WSDLValidationException(L.l("Element type {0} for part {1} of message {2} is not defined in this WSDL's schema", part.getElement(), part.getName(), message.getName()));

        part.setType(type);
      }
    }

    // check that all the messages referenced by operations are defined
    for (WSDLPortType portType : getPortTypes()) {
      portType.setDefinitions(this);

      for (WSDLOperation operation : portType.getOperations()) {
        operation.setPortType(portType);

        for (WSDLOperationInput input : operation.getInputs()) {
          WSDLMessage message = getMessage(input.getMessageName());

          if (message == null)
            throw new WSDLValidationException(L.l("Input message {0} for operation {1} is not defined in this WSDL", input.getMessageName(), operation.getName()));

          input.setMessage(message);
        }

        for (WSDLOperationOutput output : operation.getOutputs()) {
          WSDLMessage message = getMessage(output.getMessageName());

          if (message == null)
            throw new WSDLValidationException(L.l("Output message {0} for operation {1} is not defined in this WSDL", output.getMessageName(), operation.getName()));

          output.setMessage(message);
        }

        for (WSDLOperationFault fault : operation.getFaults()) {
          WSDLMessage message = getMessage(fault.getMessageName());

          if (message == null)
            throw new WSDLValidationException(L.l("Fault message {0} for operation {1} is not defined in this WSDL", fault.getMessageName(), operation.getName()));

          fault.setMessage(message);
        }
      }
    }

    // assign the binding to a portType and check that all 
    // the operations are defined
    for (WSDLBinding binding : getBindings()) { 
      WSDLPortType portType = getPortType(binding.getType());

      if (portType == null)
        throw new WSDLValidationException(L.l("PortType {0} for binding {1} is not defined in this WSDL", binding.getType(), binding.getName()));

      binding.setPortType(portType);

      for (WSDLBindingOperation bindingOp : binding.getOperations()) {
        WSDLOperation operation = portType.getOperation(bindingOp.getName());

        if (operation == null)
          throw new WSDLValidationException(L.l("PortType {0} has no operation {1} for binding {2}", portType.getName(), bindingOp.getName(), binding.getName()));

        bindingOp.setOperation(operation);
      }
    }
  }

  public WSDLMessage getMessage(QName name)
  {
    return _messageMap.get(name);
  }

  public WSDLPortType getPortType(QName name)
  {
    return _portTypeMap.get(name);
  }

  public WSDLBinding getBinding(QName name)
  {
    return _bindingMap.get(name);
  }

  public Type getType(QName typeName)
  {
    if (_types == null)
      return null;

    for (int i = 0; i < _types.size(); i++) {
      WSDLTypes types = _types.get(i);

      Type type = types.getType(typeName);
       
      if (type != null)
        return type;
    }

    return null;
  }

  public void resolveImports(Unmarshaller u)
    throws JAXBException
  {
    if (_types == null)
      return;

    for (int i = 0; i < _types.size(); i++) {
      WSDLTypes types = _types.get(i);
      types.resolveImports(u);
    }
  }

  public void writeJAXBClasses(File outputDirectory, String pkg)
    throws IOException
  {
    if (_types == null)
      return;

    for (int i = 0; i < _types.size(); i++) {
      WSDLTypes types = _types.get(i);
      types.writeJAXBClasses(outputDirectory, pkg);
    }
  }
  
  public void generateJava(Unmarshaller u, 
                           File sourceDir, File classDir, 
                           String pkg)
    throws WSDLValidationException, JAXBException, IOException
  {
    for (WSDLService service : getServices()) { 
      for (WSDLPort port : service.getPorts()) {
        WSDLBinding binding = getBinding(port.getBinding());

        if (binding == null)
          throw new WSDLValidationException(L.l("Binding {0} for port {1} not defined in this WSDL", port.getBinding(), port.getName()));

        WSDLPortType portType = binding.getPortType();

        File dir = new File(sourceDir, pkg.replace(".", File.separator));

        dir.mkdirs();

        File output = new File(dir, portType.getName() + ".java");
        WriteStream os = null;

        try {
          os = Vfs.openWrite(output.toString());
          JavaWriter out = new JavaWriter(os);

          out.println("package " + pkg + ";");
          out.println();
          out.println("import java.math.BigDecimal;");
          out.println("import java.math.BigInteger;");
          out.println("import java.util.List;");
          out.println("import javax.jws.WebMethod;");
          out.println("import javax.jws.WebParam;");
          out.println("import javax.jws.WebResult;");
          out.println("import javax.jws.WebService;");
          out.println("import javax.xml.datatype.XMLGregorianCalendar;");
          out.println("import javax.xml.ws.RequestWrapper;");
          out.println("import javax.xml.ws.ResponseWrapper;");
          out.println();
          out.print("@WebService(name=\"" + portType.getName() + "\",");
          out.println("targetNamespace=\"" + getTargetNamespace() + "\")");
          out.println("public interface " + portType.getName() + "{");

          out.pushDepth();

          for (WSDLBindingOperation bindingOp : binding.getOperations()) {
            bindingOp.generateJava(out);
            out.println();
            out.println();
          }

          out.popDepth();

          out.println("}");
        }
        finally {
          if (os != null)
            os.close();
        }

        output = new File(dir, portType.getName() + "Service.java");
        os = null;

        try {
          os = Vfs.openWrite(output.toString());
          JavaWriter out = new JavaWriter(os);

          out.println("package " + pkg + ";");
          out.println();
          out.println("import java.net.URL;");
          out.println("import javax.xml.namespace.QName;");
          out.println("import javax.xml.ws.Service;");
          out.println();
          out.println("public class " + portType.getName() + "Service");
          out.println("  extends Service");
          out.println("{");

          out.pushDepth();

          out.print("public " + portType.getName() + "Service");
          out.println("(URL wsdlDocumentLocation, QName serviceName)");
          out.println("{");
          out.pushDepth();
          out.println("super(wsdlDocumentLocation, serviceName);");
          out.popDepth();
          out.println("}");

          out.popDepth();

          out.println("}");
        }
        finally {
          if (os != null)
            os.close();
        }

      }
    }

    writeJAXBClasses(sourceDir, pkg);

    File dir = new File(sourceDir, pkg.replace(".", File.separator));
    File[] sources = dir.listFiles(new FilenameFilter() {
      public boolean accept(File dir, String name) 
      {
        return name.endsWith(".java");
      }
    });

    if (sources.length == 0) {
      // XXX Warning message?
      System.out.println(" No sources found in " + dir + "!!!!!!");
      return;
    }

    com.sun.tools.javac.Main javac = new com.sun.tools.javac.Main();

    String[] args = new String[2 + sources.length];

    args[0] = "-d";
    args[1] = classDir.getAbsolutePath();

    for (int i = 0; i < sources.length; i++)
      args[i + 2] = sources[i].getAbsolutePath();

    javac.compile(args);
  }
}
