/**
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.jbpm.process.workitem.bpmn2;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import javax.xml.namespace.QName;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.endpoint.ClientCallback;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
import org.drools.core.process.instance.impl.WorkItemImpl;
import org.jbpm.bpmn2.core.Bpmn2Import;
import org.jbpm.process.workitem.AbstractLogOrThrowWorkItemHandler;
import org.jbpm.workflow.core.impl.WorkflowProcessImpl;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.manager.RuntimeEngine;
import org.kie.api.runtime.manager.RuntimeManager;
import org.kie.api.runtime.process.WorkItem;
import org.kie.api.runtime.process.WorkItemManager;
import org.kie.internal.runtime.Cacheable;
import org.kie.internal.runtime.manager.RuntimeManagerRegistry;
import org.kie.internal.runtime.manager.context.ProcessInstanceIdContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ServiceTaskHandler extends AbstractLogOrThrowWorkItemHandler implements Cacheable {
    
    public static final String WSDL_IMPORT_TYPE = "http://schemas.xmlsoap.org/wsdl/";
    
    private static final Logger logger = LoggerFactory.getLogger(ServiceTaskHandler.class);
    
    private ConcurrentHashMap<String, Client> clients = new ConcurrentHashMap<String, Client>();
    private JaxWsDynamicClientFactory dcf;
    private KieSession ksession;
    private int asyncTimeout = 10;
    private ClassLoader classLoader;

	enum WSMode {
        SYNC,
        ASYNC,
        ONEWAY;

    }
    
    public ServiceTaskHandler() {
        this.dcf = JaxWsDynamicClientFactory.newInstance();
    }
    
    public ServiceTaskHandler(KieSession ksession) {
        this.dcf = JaxWsDynamicClientFactory.newInstance();
        this.ksession = ksession;
    }
    
    public ServiceTaskHandler(KieSession ksession, ClassLoader classloader) {
        this.dcf = JaxWsDynamicClientFactory.newInstance();
        this.ksession = ksession;
        this.classLoader = classloader;
    }
    
    public ServiceTaskHandler(KieSession ksession, int timeout) {
        this.dcf = JaxWsDynamicClientFactory.newInstance();
        this.ksession = ksession;
        this.asyncTimeout = timeout;
    }

    public void executeWorkItem(WorkItem workItem, final WorkItemManager manager) {
        String implementation = (String) workItem.getParameter("implementation");
        if ("##WebService".equalsIgnoreCase(implementation)) {
            // since JaxWsDynamicClientFactory will change the TCCL we need to restore it after creating client
            ClassLoader origClassloader = Thread.currentThread().getContextClassLoader();
            
            String interfaceRef = (String) workItem.getParameter("interfaceImplementationRef");
            String operationRef = (String) workItem.getParameter("operationImplementationRef");
            Object parameter = workItem.getParameter("Parameter");
            WSMode mode = WSMode.valueOf(workItem.getParameter("mode") == null ? "SYNC" : ((String) workItem.getParameter("mode")).toUpperCase());
            
            try {
                 Client client = getWSClient(workItem, interfaceRef);
                 if (client == null) {
                     throw new IllegalStateException("Unable to create client for web service " + interfaceRef + " - " + operationRef);
                 }
                 switch (mode) {
                case SYNC:
                    Object[] result = client.invoke(operationRef, parameter);
                    
                    Map<String, Object> output = new HashMap<String, Object>();          
   
                    if (result == null || result.length == 0) {
                      output.put("Result", null);
                    } else {
                      output.put("Result", result[0]);
                    }
   
                    manager.completeWorkItem(workItem.getId(), output);
                    break;
                case ASYNC:
                    final ClientCallback callback = new ClientCallback();
                    final long workItemId = workItem.getId();
                    final String deploymentId = nonNull(((WorkItemImpl)workItem).getDeploymentId());
                    final long processInstanceId = workItem.getProcessInstanceId();
                    
                    client.invoke(callback, operationRef, parameter);
                    new Thread(new Runnable() {
                       
                       public void run() {
                           
                           try {
                              
                               Object[] result = callback.get(asyncTimeout, TimeUnit.SECONDS);
                               Map<String, Object> output = new HashMap<String, Object>();          
                               if (callback.isDone()) {
                                   if (result == null) {
                                     output.put("Result", null);
                                   } else {
                                     output.put("Result", result[0]);
                                   }
                               }
                               RuntimeManager manager = RuntimeManagerRegistry.get().getManager(deploymentId);
                               if (manager != null) {
	                               RuntimeEngine engine = manager.getRuntimeEngine(ProcessInstanceIdContext.get(processInstanceId));
	                               
	                               engine.getKieSession().getWorkItemManager().completeWorkItem(workItemId, output);
	                               
	                               manager.disposeRuntimeEngine(engine);
                               } else {
                            	   // in case there is no RuntimeManager available use available ksession, 
                            	   // as it might be used without runtime manager at all
                            	   ksession.getWorkItemManager().completeWorkItem(workItemId, output);
                               }
                           } catch (Exception e) {
                              logger.error("Error encountered while invoking ws operation asynchronously ", e);
                           }
                           
                           
                       }
                   }).start();
                    break;
                case ONEWAY:
                    ClientCallback callbackFF = new ClientCallback();
                    
                    client.invoke(callbackFF, operationRef, parameter);
                    manager.completeWorkItem(workItem.getId(),  new HashMap<String, Object>());
                    break;
                default:
                    break;
                }

             } catch (Exception e) {
                 handleException(e, interfaceRef, "", operationRef, parameter.getClass().getName(), parameter);
             } finally {
         		Thread.currentThread().setContextClassLoader(origClassloader);
         	}
        } else {
            executeJavaWorkItem(workItem, manager);
        }
        

    }
    
    @SuppressWarnings("unchecked")
    protected synchronized Client getWSClient(WorkItem workItem, String interfaceRef) {
        if (clients.containsKey(interfaceRef)) {
            return clients.get(interfaceRef);
        }

        long processInstanceId = ((WorkItemImpl) workItem).getProcessInstanceId();
        WorkflowProcessImpl process = ((WorkflowProcessImpl) ksession.getProcessInstance(processInstanceId).getProcess());
        List<Bpmn2Import> typedImports = (List<Bpmn2Import>)process.getMetaData("Bpmn2Imports");
        
        
        
        if (typedImports != null ){
            Client client = null;
            for (Bpmn2Import importObj : typedImports) {
                
                if (WSDL_IMPORT_TYPE.equalsIgnoreCase(importObj.getType())) {
                
                    try {
                        client = dcf.createClient(importObj.getLocation(), new QName(importObj.getNamespace(), interfaceRef), getInternalClassLoader(), null);
                        clients.put(interfaceRef, client);
                        logger.info("WS Client is created for {" + importObj.getNamespace() + "}" + interfaceRef);
                        return client;
                    } catch (Exception e) {
                        logger.info("Error when creating WS Client. You can ignore this error as long as a client is eventually created", e);
                        continue;
                    }
                }
            }
        }

        return null;

    }

    private ClassLoader getInternalClassLoader() {
		if (this.classLoader != null) {
			return this.classLoader;
		}
		
		return Thread.currentThread().getContextClassLoader();
	}

	public void executeJavaWorkItem(WorkItem workItem, WorkItemManager manager) {
        String i = (String) workItem.getParameter("Interface");
        String iImplementationRef = (String) workItem.getParameter("interfaceImplementationRef"); 
        String operation = (String) workItem.getParameter("Operation");
        String parameterType = (String) workItem.getParameter("ParameterType");
        Object parameter = workItem.getParameter("Parameter");
        
        String[] interfaces = {i, iImplementationRef};
        Class<?> c = null;
        
        for(String interf : interfaces) {
            try {
                c = Class.forName(interf, true, getInternalClassLoader());
                break;
            } catch (ClassNotFoundException cnfe) {
                if(interf.compareTo(interfaces[interfaces.length - 1]) == 0) {
                    handleException(cnfe, i, iImplementationRef, operation, parameterType, parameter);
                }
            }
        }
        
        try {
            Object instance = c.newInstance();
            Class<?>[] classes = null;
            Object[] params = null;
            if (parameterType != null) {
                classes = new Class<?>[] {
                    Class.forName(parameterType, true, getInternalClassLoader())
                };
                params = new Object[] {
                    parameter
                };
            }
            Method method = c.getMethod(operation, classes);
            Object result = method.invoke(instance, params);
            Map<String, Object> results = new HashMap<String, Object>();
            results.put("Result", result);
            manager.completeWorkItem(workItem.getId(), results);
        } catch (ClassNotFoundException e) {
            handleException(e, i, iImplementationRef, operation, parameterType, parameter);
        } catch (InstantiationException e) {
            handleException(e, i, iImplementationRef, operation, parameterType, parameter);
        } catch (IllegalAccessException e) {
            handleException(e, i, iImplementationRef, operation, parameterType, parameter);
        } catch (NoSuchMethodException e) {
            handleException(e, i, iImplementationRef, operation, parameterType, parameter);
        } catch (InvocationTargetException e) {
            handleException(e, i, iImplementationRef, operation, parameterType, parameter);
        }
    }
    
    private void handleException(Throwable cause, String service, String iImplementationRef, String operation, String paramType, Object param) { 
        logger.debug("Handling exception {} inside service {} or {} and operation {} with param type {} and value {}",
                cause.getMessage(), service, operation, paramType, param);
        Map<String, Object> data = new HashMap<String, Object>();

        data.put("Interface", service);
        data.put("InterfaceImplementationRef", iImplementationRef);
        data.put("Operation", operation);
        data.put("ParameterType", paramType);
        data.put("Parameter", param);
        
        handleException(cause, data);
        
    }

    public void abortWorkItem(WorkItem workItem, WorkItemManager manager) {
        // Do nothing, cannot be aborted
    }
    
    public ClassLoader getClassLoader() {
		return classLoader;
	}

	public void setClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}
	
	protected String nonNull(String value) {
		if (value == null) {
			return "";
		}
		
		return value;
	}

	@Override
	public void close() {
		if (clients != null) {
			for (Client client : clients.values()) {
				client.destroy();
			}
		}
	}
}
