/**
 * 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.jca.outbound;

import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.resource.NotSupportedException;
import javax.resource.ResourceException;
import javax.resource.spi.ConnectionEvent;
import javax.resource.spi.ConnectionEventListener;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.LocalTransaction;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionMetaData;
import javax.security.auth.Subject;
import javax.transaction.xa.XAResource;
import javax.xml.ws.BindingProvider;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.bus.spring.SpringBusFactory;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.configuration.Configurer;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.frontend.ClientProxyFactoryBean;
import org.apache.cxf.jaxws.EndpointUtils;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.jca.core.logging.LoggerHelper;

/**
 * Represents a "physical" connection to EIS, which provides access to target
 * web service.  ManagedConnectionImpl creates connection handles for
 * applications to use the connection backed by this object.
 */
public class ManagedConnectionImpl implements ManagedConnection {
    private static final Logger LOG = LogUtils.getL7dLogger(ManagedConnectionImpl.class);

    private Set<ConnectionEventListener> listeners =
        Collections.synchronizedSet(new HashSet<>());

    private Map<Object, Subject> handles =
        Collections.synchronizedMap(new HashMap<Object, Subject>());
    private PrintWriter printWriter;

    private ManagedConnectionFactoryImpl mcf;
    private ConnectionRequestInfo connReqInfo;
    private boolean isClosed;
    private Bus bus;
    private Object associatedHandle;
    private Object clientProxy;

    public ManagedConnectionImpl(ManagedConnectionFactoryImpl mcf,
            ConnectionRequestInfo connReqInfo, Subject subject) {
        this.mcf = mcf;
        this.connReqInfo = connReqInfo;
    }

    /* -------------------------------------------------------------------
     * ManagedConnection Methods
     */

    public void addConnectionEventListener(ConnectionEventListener listener) {
        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("add listener : " + listener);
        }
        listeners.add(listener);
    }

    public void associateConnection(Object connection) throws ResourceException {
        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("associate handle : " + connection);
        }
        associatedHandle = connection;
        // nothing needs to be done as app gets a copy of client proxy
    }

    public void cleanup() throws ResourceException {
        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("cleanup");
        }
        handles.clear();
        isClosed = false;
    }

    public void destroy() throws ResourceException {
        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("destroy");
        }

        Client client = ClientProxy.getClient(clientProxy);
        client.destroy();

        handles.clear();
        isClosed = false;
        bus = null;
        connReqInfo = null;
    }

    public Object getConnection(Subject subject,
            ConnectionRequestInfo cxRequestInfo) throws ResourceException {
        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("get handle for subject=" + subject + " cxRequestInfo="
                    + cxRequestInfo);
        }

        if (isClosed) {
            throw new ResourceException("connection has been closed");
        }

        // check request info
        if (!connReqInfo.equals(cxRequestInfo)) {
            throw new ResourceException("connection request info: " + cxRequestInfo
                    + " does not match " + connReqInfo);
        }

        CXFConnectionSpec spec = (CXFConnectionSpec)cxRequestInfo;

        Object handle = createConnectionHandle(spec);
        handles.put(handle, subject);
        associatedHandle = handle;
        return handle;

    }

    public LocalTransaction getLocalTransaction() throws ResourceException {
        throw new NotSupportedException("LocalTransaction is not supported.");
    }

    public PrintWriter getLogWriter() throws ResourceException {
        return printWriter;
    }

    public ManagedConnectionMetaData getMetaData() throws ResourceException {
        return new CXFManagedConnectionMetaData(getUserName());
    }

    public XAResource getXAResource() throws ResourceException {
        throw new NotSupportedException("XAResource is not supported.");
    }

    public void removeConnectionEventListener(ConnectionEventListener listener) {
        listeners.remove(listener);
    }

    public void setLogWriter(PrintWriter out) throws ResourceException {
        printWriter = out;

        if (printWriter != null) {
            LoggerHelper.initializeLoggingOnWriter(printWriter);
        }
    }

    /* -------------------------------------------------------------------
     * Public Methods
     */

    public ConnectionRequestInfo getRequestInfo() {
        return connReqInfo;
    }

    public ManagedConnectionFactoryImpl getManagedConnectionFactoryImpl() {
        return mcf;
    }

    /* -------------------------------------------------------------------
     * Private Methods
     */

    private void sendEvent(final ConnectionEvent coEvent) {
        synchronized (listeners) {
            Iterator<ConnectionEventListener> iterator = listeners.iterator();
            while (iterator.hasNext()) {
                sendEventToListener(iterator.next(), coEvent);
            }
        }
    }

    private void sendEventToListener(ConnectionEventListener listener,
            ConnectionEvent coEvent) {
        if (coEvent.getId() == ConnectionEvent.CONNECTION_CLOSED) {
            listener.connectionClosed(coEvent);
        }

        if (coEvent.getId() == ConnectionEvent.LOCAL_TRANSACTION_COMMITTED) {
            listener.localTransactionCommitted(coEvent);
        }

        if (coEvent.getId() == ConnectionEvent.LOCAL_TRANSACTION_ROLLEDBACK) {
            listener.localTransactionRolledback(coEvent);
        }

        if (coEvent.getId() == ConnectionEvent.LOCAL_TRANSACTION_STARTED) {
            listener.localTransactionStarted(coEvent);
        }

        if (coEvent.getId() == ConnectionEvent.CONNECTION_ERROR_OCCURRED) {
            listener.connectionErrorOccurred(coEvent);
        }

    }

    private String getUserName() {
        if (associatedHandle != null) {
            Subject subject = handles.get(associatedHandle);
            if (subject != null) {
                return subject.toString();
            }
        }
        return null;

    }

    private Object createConnectionHandle(final CXFConnectionSpec spec) {

        Class<?>[] interfaces = {CXFConnection.class, BindingProvider.class,
                spec.getServiceClass()};

        return Proxy.newProxyInstance(spec.getServiceClass().getClassLoader(),
                interfaces, new ConnectionInvocationHandler(
                        createClientProxy(spec), spec));
    }

    private synchronized Object createClientProxy(final CXFConnectionSpec spec) {
        if (clientProxy == null) {
            validateConnectionSpec(spec);
            final ClientProxyFactoryBean factory;

            if (EndpointUtils.hasWebServiceAnnotation(spec.getServiceClass())) {
                factory = new JaxWsProxyFactoryBean();
            } else {
                factory = new ClientProxyFactoryBean();
            }

            factory.setBus(getBus(spec.getBusConfigURL()));
            factory.setServiceClass(spec.getServiceClass());
            factory.getServiceFactory().setEndpointName(spec.getEndpointName());
            factory.getServiceFactory().setServiceName(spec.getServiceName());
            factory.getServiceFactory().setWsdlURL(spec.getWsdlURL());

            if (spec.getAddress() != null) {
                factory.setAddress(spec.getAddress());
            }

            configureObject(spec.getEndpointName().toString() + ".jaxws-client.proxyFactory", factory);

            clientProxy = factory.create();
        }

        return clientProxy;
    }

    private void validateConnectionSpec(CXFConnectionSpec spec) {
        if (spec.getServiceClass() == null) {
            throw new IllegalArgumentException("no serviceClass in connection spec");
        }

        if (spec.getEndpointName() == null) {
            throw new IllegalArgumentException("no endpointName in connection spec");
        }

        if (spec.getServiceName() == null) {
            throw new IllegalArgumentException("no serviceName in connection spec");
        }

        if (spec.getWsdlURL() == null) {
            throw new IllegalArgumentException("no wsdlURL in connection spec");
        }
    }

    private void configureObject(String name, Object instance) {
        Configurer configurer = bus.getExtension(Configurer.class);
        if (null != configurer) {
            configurer.configureBean(name, instance);
        }
    }

    private synchronized Bus getBus(URL busConfigLocation) {
        if (bus == null) {
            if (busConfigLocation != null) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Create bus from location " + busConfigLocation);
                }
                bus = new SpringBusFactory().createBus(busConfigLocation);
            } else if (mcf.getBusConfigURL() != null) {

                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Create bus from URL " + mcf.getBusConfigURL());
                }

                URL url = null;
                try {
                    url = new URL(mcf.getBusConfigURL());
                } catch (MalformedURLException e) {
                    LOG.warning("Malformed URL " + mcf.getBusConfigURL());
                }

                if (url != null) {
                    bus = new SpringBusFactory().createBus(url);
                }
            }

            if (bus == null) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Create default bus");
                }
                bus = BusFactory.getDefaultBus();
            }
        }
        return bus;
    }

    private class ConnectionInvocationHandler implements InvocationHandler {
        private Object target;
        private CXFConnectionSpec spec;

        ConnectionInvocationHandler(Object target, CXFConnectionSpec spec) {
            this.target = target;
            this.spec = spec;
        }

        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {

            if (LOG.isLoggable(Level.FINEST)) {
                LOG.finest("invoke connection spec:" + spec + " method=" + method);
            }

            if ("hashCode".equals(method.getName())) {
                return method.invoke(Proxy.getInvocationHandler(proxy), args);
            }

            if ("equals".equals(method.getName())) {
                // These are proxies.  We don't really care if their targets are equal.
                // We do care if these are the same proxy instances that we created.
                // Therefore, if their proxy and invocation handler are consistent,
                // we believe they are equal.
                boolean result = false;
                try {
                    result = proxy == args[0] && this == Proxy.getInvocationHandler(args[0]);
                } catch (Exception e) {
                    // ignore and assume not equal
                }
                return result;
            }

            if ("toString".equals(method.getName())) {
                return "ManagedConnection: " + spec;
            }

            if (!handles.containsKey(proxy)) {
                throw new IllegalArgumentException("Stale connection");
            }

            if ("getService".equals(method.getName())) {
                return handleGetServiceMethod(proxy, method, args);
            } else if ("close".equals(method.getName())) {
                return handleCloseMethod(proxy, method, args);

            } else {
                throw new IllegalArgumentException("Unhandled method " + method);
            }
        }

        private Object handleGetServiceMethod(Object proxy, Method method,
                Object[] args) {

            if (!spec.getServiceClass().equals(args[0])) {
                throw new IllegalArgumentException("serviceClass "
                        + args[0] + " does not match " + spec.getServiceClass());
            }

            return target;
        }

        private Object handleCloseMethod(Object proxy, Method method,
                Object[] args) {

            handles.remove(proxy);
            associatedHandle = null;
            ConnectionEvent event = new ConnectionEvent(ManagedConnectionImpl.this,
                    ConnectionEvent.CONNECTION_CLOSED);
            event.setConnectionHandle(proxy);
            sendEvent(event);

            return null;
        }
    }

}
