/*
 * 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 Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.soap.jaxws;

import com.caucho.server.util.ScheduledThreadPool;
import com.caucho.soap.jaxws.handlerchain.HandlerChains;
import com.caucho.soap.reflect.WebServiceIntrospector;
import com.caucho.soap.skeleton.Skeleton;
import com.caucho.soap.wsdl.WSDLDefinitions;
import com.caucho.soap.wsdl.WSDLParser;
import com.caucho.util.L10N;

import javax.activation.DataSource;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import static javax.xml.soap.SOAPConstants.*;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.transform.Source;

import javax.jws.HandlerChain;

import javax.xml.ws.Binding;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.http.HTTPBinding;
import javax.xml.ws.soap.SOAPBinding;
import javax.xml.ws.spi.ServiceDelegate;

import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.logging.Logger;

/**
 * ServiceDelegate
 */
public class ServiceDelegateImpl extends ServiceDelegate {
  private final static Logger log
    = Logger.getLogger(ServiceDelegateImpl.class.getName());
  private final static L10N L = new L10N(ServiceDelegateImpl.class);

  private final Map<QName,PortInfoImpl> _portMap 
    = new HashMap<QName,PortInfoImpl>();

  private final Unmarshaller _handlerUnmarshaller;

  private final ClassLoader _classLoader;

  private final URL _wsdl;
  private final QName _serviceName;
  private final Class _serviceClass;

  private HandlerResolver _handlerResolver;
  private Executor _executor = ScheduledThreadPool.getLocal();

  ServiceDelegateImpl(URL wsdl, QName serviceName, Class serviceClass)
  {
    _classLoader = Thread.currentThread().getContextClassLoader();
    
    _wsdl = wsdl;
    _serviceName = serviceName;
    _serviceClass = serviceClass;

    try {
      JAXBContext context = 
        JAXBContext.newInstance("com.caucho.soap.jaxws.handlerchain");
      _handlerUnmarshaller = context.createUnmarshaller();
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void addPort(QName portName, String bindingId, String endpointAddress)
  {
    PortInfoImpl portInfo = 
      new PortInfoImpl(bindingId, portName, _serviceName, endpointAddress);

    _portMap.put(portName, portInfo);
  }

  public <T> Dispatch<T> createDispatch(QName portName,
                                        Class<T> type,
                                        Service.Mode mode)
    throws WebServiceException
  {
    PortInfoImpl port = _portMap.get(portName);
    String bindingId = SOAPBinding.SOAP11HTTP_BINDING;
    String endpointAddress = null;

    if (port != null) {
      bindingId = port.getBindingID();
      endpointAddress = port.getEndpointAddress();
    }

    if (endpointAddress == null)
      endpointAddress = findEndpointAddress();

    Dispatch<T> dispatch = null;
    Binding binding = getBinding(bindingId);

    if (_handlerResolver != null)
      binding.setHandlerChain(_handlerResolver.getHandlerChain(port));

    if (Source.class.equals(type)) {
      dispatch = (Dispatch<T>) new SourceDispatch(bindingId, binding, 
                                                  mode, _executor);
    }
    else if (SOAPMessage.class.equals(type)) {
      dispatch = (Dispatch<T>) new SOAPMessageDispatch(bindingId, binding, 
                                                       mode, _executor);
    }
    else if (DataSource.class.equals(type)) {
      dispatch = (Dispatch<T>) new DataSourceDispatch(bindingId, binding,
                                                      mode, _executor);
    }

    if (dispatch == null) {
      throw new WebServiceException(L.l("{0} is an unsupported Dispatch type",
                                        type));
    }

    if (endpointAddress != null) {
      Map<String,Object> requestContext = dispatch.getRequestContext();
      requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, 
                         endpointAddress);
    }

    return dispatch;
  }

  public Dispatch<Object> createDispatch(QName portName,
                                                                       JAXBContext context,
                                         Service.Mode mode)
    throws WebServiceException
  {
    PortInfoImpl port = _portMap.get(portName);
    String bindingId = SOAPBinding.SOAP11HTTP_BINDING;
    String endpointAddress = null;

    if (port != null) {
      bindingId = port.getBindingID();
      endpointAddress = port.getEndpointAddress();
    }

    if (endpointAddress == null)
      endpointAddress = findEndpointAddress();

    Binding binding = getBinding(bindingId);

    if (_handlerResolver != null)
      binding.setHandlerChain(_handlerResolver.getHandlerChain(port));

    JAXBDispatch dispatch = 
      new JAXBDispatch(bindingId, binding, mode, _executor, context);

    if (endpointAddress != null) {
      Map<String,Object> requestContext = dispatch.getRequestContext();
      requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
                         endpointAddress);
    }

    return dispatch;
  }

  public Executor getExecutor()
  {
    return _executor;
  }

  public HandlerResolver getHandlerResolver()
  {
    return _handlerResolver;
  }

  public void setHandlerResolver(HandlerResolver handlerResolver)
  {
    _handlerResolver = handlerResolver;
  }

  public <T> T getPort(Class<T> serviceEndpointInterface)
  {
    return getPort(null, serviceEndpointInterface);
  }

  public <T> T getPort(QName portName, Class<T> api)
    throws WebServiceException
  {
    try {
      WSDLDefinitions definitions = WSDLParser.parse(api);

      if (definitions == null && _wsdl != null) {
        try {
          definitions = WSDLParser.parse(_wsdl);
        }
        catch (WebServiceException e) {}

        /*
        if (definitions == null)
          throw new WebServiceException(L.l("Unable to parse WSDL"));*/
      }

      String wsdlLocation = null;

      if (_wsdl != null)
        wsdlLocation = _wsdl.toString();
      else
        wsdlLocation = null;

      Skeleton skeleton = 
        WebServiceIntrospector.introspect(api, wsdlLocation, definitions);

      String endpointAddress = findEndpointAddress();
      String bindingId = SOAPBinding.SOAP11HTTP_BINDING;
      //definitions.getBindingId(_serviceName, portName);

      Binding binding = getBinding(bindingId);
      PortProxyHandler handler = 
        new PortProxyHandler(skeleton, endpointAddress, binding);

      if (portName == null)
        portName = new QName(skeleton.getNamespace(), skeleton.getPortName());

      PortInfoImpl portInfo = new PortInfoImpl(bindingId, portName, 
                                               _serviceName, endpointAddress);

      // Check for @HandlerChain on the service API
      if (_handlerResolver == null) {
        HandlerChain handlerChain = 
          (HandlerChain) api.getAnnotation(HandlerChain.class);

        if (handlerChain != null)
          _handlerResolver = JAXWSUtil.createHandlerResolver(api, handlerChain);
      }

      // Check for @HandlerChain on the Service
      if (_handlerResolver == null) {
        HandlerChain handlerChain = 
          (HandlerChain) _serviceClass.getAnnotation(HandlerChain.class);

        if (handlerChain != null)
          _handlerResolver = JAXWSUtil.createHandlerResolver(api, handlerChain);
      }

      if (_handlerResolver != null)
        binding.setHandlerChain(_handlerResolver.getHandlerChain(portInfo));

      _portMap.put(portName, portInfo);

      Class[] interfaces = new Class[] {api, BindingProvider.class};

      Object proxy = Proxy.newProxyInstance(_classLoader, interfaces, handler);

      return (T) proxy;
    }
    catch (WebServiceException e) {
      throw e;
    }
    catch (Exception e) {
      throw new WebServiceException(e);
    }
  }

  public Iterator<QName> getPorts()
  {
    return _portMap.keySet().iterator();
  }

  public QName getServiceName()
  {
    return _serviceName;
  }

  public URL getWSDLDocumentLocation()
  {
    return _wsdl;
  }

  public void setExecutor(Executor executor)
  {
    _executor = executor;
  }

  public String toString()
  {
    return ("ServiceDelegateImpl[" + getServiceName().getNamespaceURI()
            + "," + getServiceName().getLocalPart() + "]");
  }

  private String findEndpointAddress()
  {
    if (getWSDLDocumentLocation() != null) {
      int p = getWSDLDocumentLocation().toString().lastIndexOf('?');
      return getWSDLDocumentLocation().toString().substring(0, p);
    }
    else
      return null;

    /*
    WSDLDefinitions definitions = WSDLParser.parse(getWSDLDocumentLocation());

    if (definitions != null) {
      endpointAddress = 
        definitions.getEndpointAddress(_serviceName, portName);

      if (endpointAddress != null && endpointAddress.indexOf(':') < 0) {
        definitions = WSDLParser.parse(endpointAddress);

        if (definitions != null) {
          endpointAddress = 
            definitions.getEndpointAddress(_serviceName, portName);
        }
      }
    }*/
  }

  static Binding getBinding(String bindingId)
    throws WebServiceException
  {
    if (bindingId.equals(SOAPBinding.SOAP11HTTP_BINDING) ||
        bindingId.equals(SOAPBinding.SOAP11HTTP_MTOM_BINDING) ||
        bindingId.equals(SOAPBinding.SOAP12HTTP_BINDING) ||
        bindingId.equals(SOAPBinding.SOAP12HTTP_MTOM_BINDING)) {
      try {
        return new SOAPBindingImpl(bindingId);
      }
      catch (SOAPException e) {
        throw new WebServiceException(e);
      }
    }
    else if (bindingId.equals(HTTPBinding.HTTP_BINDING))
      return new HTTPBindingImpl();
    else
      throw new WebServiceException(L.l("Unknown binding id: {0}", bindingId));
  }
}
