/**
 * 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.ws.eventing.backend.notification;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.jws.WebService;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.namespace.QName;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.ext.logging.LoggingOutInterceptor;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.ws.eventing.EventType;
import org.apache.cxf.ws.eventing.backend.database.SubscriptionTicket;
import org.apache.cxf.ws.eventing.shared.handlers.ReferenceParametersAddingHandler;

/**
 * Represents the task to send a notification about a particular event to a particular subscribed client.
 * Dispatch is performed according to a provided endpoint interface.
 */
class EventSinkInterfaceNotificationTask implements Runnable {

    protected static final Logger LOG = LogUtils.getLogger(EventSinkInterfaceNotificationTask.class);

    SubscriptionTicket target;
    Object event;
    Class<?> endpointInterface;

    EventSinkInterfaceNotificationTask(SubscriptionTicket ticket, Object event, Class<?> endpointInterface) {
        this.target = ticket;
        this.event = event;
        this.endpointInterface = endpointInterface;

    }

    /**
     * Logic needed to actually send the notification to the subscribed client.
     */
    @Override
    public void run() {
        try {
            LOG.info("Starting notification task for subscription UUID " + target.getUuid());

            Method method = null;
            final Object proxy;
            final Object param;
            final Class<?> eventClass = event.getClass();
            final Class<?>[] eventClassArray = new Class<?>[] {eventClass};
            if (target.isWrappedDelivery()) {
                proxy = getProxy(WrappedSink.class, eventClassArray);
                param = new EventType();
                ((EventType)param).getContent().add(eventClass.isAnnotationPresent(XmlRootElement.class)
                                               ? event : convertToJAXBElement(event));
                method = WrappedSink.class.getMethod("notifyEvent", EventType.class);
            } else {
                proxy = getProxy(endpointInterface);
                // find the method to use
                Method[] methods = endpointInterface.getMethods();
                for (int i = 0; i < methods.length && method == null; i++) {
                    if (Arrays.equals(methods[i].getParameterTypes(), eventClassArray)) {
                        method = methods[i];
                    }
                }
                if (method == null) {
                    LOG.severe("Couldn't find corresponding method for event of type "
                               + eventClass.getCanonicalName() + " in event sink interface"
                               + endpointInterface.getCanonicalName());
                    return;
                }
                param = event;
            }

            method.invoke(proxy, param);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    @SuppressWarnings({
        "unchecked", "rawtypes"
    })
    protected JAXBElement<?> convertToJAXBElement(Object evt) {
        final Class<?> eventClass = evt.getClass();
        String tns = endpointInterface.getAnnotation(WebService.class).targetNamespace();
        return new JAXBElement(new QName(tns, eventClass.getName()), eventClass, evt);
    }

    protected Object getProxy(Class<?> sinkInterface, Class<?>... extraClasses) {
        //needed SOAP handlers
        ReferenceParametersAddingHandler handler = new
                ReferenceParametersAddingHandler(
                target.getNotificationReferenceParams());

        JaxWsProxyFactoryBean service = new JaxWsProxyFactoryBean();
        service.getOutInterceptors().add(new LoggingOutInterceptor());
        service.setServiceClass(sinkInterface);
        service.setAddress(target.getTargetURL());
        service.getHandlers().add(handler);

        // do we need to apply a filter?
        if (target.getFilter() != null && target.getFilter().getContent().size() > 0) {
            service.getOutInterceptors().add(new FilteringInterceptor(target.getFilter()));
        }

        if (extraClasses != null && extraClasses.length > 0) {
            Map<String, Object> props = new HashMap<>();
            props.put("jaxb.additionalContextClasses", extraClasses);
            service.getClientFactoryBean().getServiceFactory().setProperties(props);
        }

        return service.create();
    }

}
