/*
 * Copyright 2017 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.runtime.manager.impl;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import org.drools.core.impl.EnvironmentFactory;
import org.jbpm.process.audit.AbstractAuditLogger;
import org.jbpm.process.audit.AuditLoggerFactory;
import org.jbpm.process.audit.event.AuditEventBuilder;
import org.jbpm.runtime.manager.impl.jpa.EntityManagerFactoryManager;
import org.jbpm.services.task.audit.JPATaskLifeCycleEventListener;
import org.jbpm.services.task.audit.TaskAuditLoggerFactory;
import org.jbpm.services.task.audit.jms.AsyncTaskLifeCycleEventProducer;
import org.jbpm.services.task.wih.LocalHTWorkItemHandler;
import org.kie.api.event.process.ProcessEventListener;
import org.kie.api.event.rule.AgendaEventListener;
import org.kie.api.event.rule.RuleRuntimeEventListener;
import org.kie.api.runtime.Environment;
import org.kie.api.runtime.EnvironmentName;
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.WorkItemHandler;
import org.kie.api.task.TaskLifeCycleEventListener;
import org.kie.internal.runtime.Cacheable;
import org.kie.internal.runtime.Closeable;
import org.kie.internal.runtime.conf.AuditMode;
import org.kie.internal.runtime.conf.DeploymentDescriptor;
import org.kie.internal.runtime.conf.NamedObjectModel;
import org.kie.internal.runtime.conf.ObjectModel;
import org.kie.internal.runtime.conf.ObjectModelResolver;
import org.kie.internal.runtime.conf.ObjectModelResolverProvider;
import org.kie.internal.runtime.manager.InternalRuntimeEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Default implementation of the <code>RegisterableItemsFactory</code> responsible for providing 
 * a common set of WorkItemHandlers and EventListeners. This factory should not be used in CDI container.
 * <br/>
 * It will deliver fully configured instances of the following:
 * <ul>
 *  <li>a WorkItemHandler for "Human Task" that is configured with local task service</li>
 *  <li>a JPA audit logger - for history logging</li>
 *  <li>a event listener to trigger rules automatically without a need of invoking fireAllRules</li>
 * </ul>
 * Moreover, it will invoke its super methods to define the rest of the registerable items, that might override defaults
 * when they are added to the resulting map at the end.
 * 
 * @see InjectableRegisterableItemsFactory
 */
public class DefaultRegisterableItemsFactory extends SimpleRegisterableItemsFactory {
	
	private static final Logger logger = LoggerFactory.getLogger(DefaultRegisterableItemsFactory.class);

    private AuditEventBuilder auditBuilder = new ManagedAuditEventBuilderImpl();
    private AbstractAuditLogger jmsLogger = null;
    private AsyncTaskLifeCycleEventProducer jmsTaskLogger = null;
    
    @Override
    public Map<String, WorkItemHandler> getWorkItemHandlers(RuntimeEngine runtime) {
        Map<String, WorkItemHandler> defaultHandlers = new HashMap<String, WorkItemHandler>();
        //HT handler 
        WorkItemHandler handler = getHTWorkItemHandler(runtime);
        if (handler != null) {
            defaultHandlers.put("Human Task", handler);
        }
        // add any custom registered
        defaultHandlers.putAll(super.getWorkItemHandlers(runtime));
        // add handlers from descriptor
        defaultHandlers.putAll(getWorkItemHandlersFromDescriptor(runtime));
        
        return defaultHandlers;
    }


    @Override
    public List<ProcessEventListener> getProcessEventListeners(RuntimeEngine runtime) {
        KieSession ksession = ((InternalRuntimeEngine) runtime).internalGetKieSession();
        List<ProcessEventListener> defaultListeners = new ArrayList<ProcessEventListener>();
        DeploymentDescriptor descriptor = getRuntimeManager().getDeploymentDescriptor();
        if (descriptor == null) {
        	// register JPAWorkingMemoryDBLogger
	        AbstractAuditLogger logger = AuditLoggerFactory.newJPAInstance(ksession.getEnvironment());
	        logger.setBuilder(getAuditBuilder(runtime));
	        defaultListeners.add(logger);
        } else if (descriptor.getAuditMode() == AuditMode.JPA) {
        	// register JPAWorkingMemoryDBLogger
        	AbstractAuditLogger logger = null;
        	if (descriptor.getPersistenceUnit().equals(descriptor.getAuditPersistenceUnit())) {
        		logger = AuditLoggerFactory.newJPAInstance(ksession.getEnvironment());
        	} else {
        		Environment env = EnvironmentFactory.newEnvironment();
        		env.set(EnvironmentName.ENTITY_MANAGER_FACTORY, EntityManagerFactoryManager.get().getOrCreate(descriptor.getAuditPersistenceUnit()));
        		logger = AuditLoggerFactory.newJPAInstance(env);
        	}
	        
	        logger.setBuilder(getAuditBuilder(runtime));
	        defaultListeners.add(logger);
        } else if (descriptor.getAuditMode() == AuditMode.JMS) {
            try {
                if (jmsLogger == null) {
                    Properties properties = loadJMSProperties();
                    logger.debug("Creating AsyncAuditLogProducer {}", properties);

                    jmsLogger = AuditLoggerFactory.newJMSInstance((Map) properties);
                    jmsLogger.setBuilder(getAuditBuilder(runtime));
                }
                defaultListeners.add(jmsLogger);
            } catch (IOException e) {
                logger.error("Unable to load jms audit properties from {}", "/jbpm.audit.jms.properties", e);
            }
        }
        // add any custom listeners
        defaultListeners.addAll(super.getProcessEventListeners(runtime));
        // add listeners from descriptor
        defaultListeners.addAll(getEventListenerFromDescriptor(runtime, ProcessEventListener.class));        
        return defaultListeners;
    }

    @Override
    public List<AgendaEventListener> getAgendaEventListeners(RuntimeEngine runtime) {
        List<AgendaEventListener> defaultListeners = new ArrayList<AgendaEventListener>();
        // add any custom listeners
        defaultListeners.addAll(super.getAgendaEventListeners(runtime));
        // add listeners from descriptor
        defaultListeners.addAll(getEventListenerFromDescriptor(runtime, AgendaEventListener.class));        
        return defaultListeners;
    }

    @Override
    public List<RuleRuntimeEventListener> getRuleRuntimeEventListeners(RuntimeEngine runtime) {
        List<RuleRuntimeEventListener> defaultListeners = new ArrayList<RuleRuntimeEventListener>();
        
        // add any custom listeners
        defaultListeners.addAll(super.getRuleRuntimeEventListeners(runtime));
        // add listeners from descriptor
        defaultListeners.addAll(getEventListenerFromDescriptor(runtime, RuleRuntimeEventListener.class));
        return defaultListeners;
    }


    @Override
	public List<TaskLifeCycleEventListener> getTaskListeners() {
    	List<TaskLifeCycleEventListener> defaultListeners = new ArrayList<TaskLifeCycleEventListener>();
    	DeploymentDescriptor descriptor = getRuntimeManager().getDeploymentDescriptor();
        if (descriptor == null) {
            defaultListeners.add(new JPATaskLifeCycleEventListener(true));
        } else if (descriptor.getAuditMode() == AuditMode.JPA) {
            
            if (descriptor.getPersistenceUnit().equals(descriptor.getAuditPersistenceUnit())) {
                defaultListeners.add(TaskAuditLoggerFactory.newJPAInstance());
            } else {                
                defaultListeners.add(TaskAuditLoggerFactory.newJPAInstance(EntityManagerFactoryManager.get().getOrCreate(descriptor.getAuditPersistenceUnit())));
            }
            
        } else if (descriptor.getAuditMode() == AuditMode.JMS) {
            if (jmsTaskLogger == null) {
                 try {
                    Properties properties = loadJMSProperties();
                    
                    jmsTaskLogger = TaskAuditLoggerFactory.newJMSInstance((Map) properties);
                } catch (IOException e) {
                    logger.error("Unable to load jms audit properties from {}", "/jbpm.audit.jms.properties", e);
                }
            }
            defaultListeners.add(jmsTaskLogger);
        }
        // add any custom listeners
        defaultListeners.addAll(super.getTaskListeners());
        // add listeners from deployment descriptor
        defaultListeners.addAll(getTaskListenersFromDescriptor());
        
        return defaultListeners;
	}


	@Override
	public Map<String, Object> getGlobals(RuntimeEngine runtime) {
		Map<String, Object> defaultGlobals = new HashMap<String, Object>();
				
		defaultGlobals.putAll(super.getGlobals(runtime));
		// add globals from descriptor
		defaultGlobals.putAll(getGlobalsFromDescriptor(runtime));
		return defaultGlobals;
	}


	
    protected WorkItemHandler getHTWorkItemHandler(RuntimeEngine runtime) {

        LocalHTWorkItemHandler humanTaskHandler = new LocalHTWorkItemHandler();
        humanTaskHandler.setRuntimeManager(((RuntimeEngineImpl)runtime).getManager());
                
        return humanTaskHandler;
    }


    public AuditEventBuilder getAuditBuilder() {
        return auditBuilder;
    }

    public AuditEventBuilder getAuditBuilder(RuntimeEngine engine) {
    	if (this.auditBuilder != null && this.auditBuilder instanceof ManagedAuditEventBuilderImpl) {
    		String identifier = ((RuntimeEngineImpl)engine).getManager().getIdentifier();
    		((ManagedAuditEventBuilderImpl) this.auditBuilder).setOwnerId(identifier);
    	}
    	
    	return this.auditBuilder;
    }

    public void setAuditBuilder(AuditEventBuilder auditBuilder) {
        this.auditBuilder = auditBuilder;
    }    
    
    protected Object getInstanceFromModel(ObjectModel model, ClassLoader classloader, Map<String, Object> contaxtParams) {
    	ObjectModelResolver resolver = ObjectModelResolverProvider.get(model.getResolver());
		if (resolver == null) {
		    throw new IllegalStateException("Unable to find ObjectModelResolver for " + model.getResolver());
		}
		
		return resolver.getInstance(model, classloader, contaxtParams);
    }    
    
    protected Map<String, Object> getParametersMap(RuntimeEngine runtime) {
        RuntimeManager manager = ((RuntimeEngineImpl)runtime).getManager();
        Map<String, Object> parameters = new HashMap<String, Object>();
        parameters.put("ksession", ((InternalRuntimeEngine) runtime).internalGetKieSession());
        
        try {
            parameters.put("taskService", runtime.getTaskService());
        } catch (UnsupportedOperationException e) {
            // in case task service was not configured
        }
        parameters.put("runtimeManager", manager);
        parameters.put("classLoader", getRuntimeManager().getEnvironment().getClassLoader());
        parameters.put("entityManagerFactory", 
                ((InternalRuntimeEngine) runtime).internalGetKieSession().getEnvironment().get(EnvironmentName.ENTITY_MANAGER_FACTORY));
        parameters.put("kieContainer", getRuntimeManager().getKieContainer());
        parameters.put("identityProvider", ((SimpleRuntimeEnvironment)getRuntimeManager().getEnvironment()).getEnvironmentTemplate().get(EnvironmentName.IDENTITY_PROVIDER));

        Map<Object, Object> env = new HashMap<>();
        for(Entry<Object, Object> set : System.getProperties().entrySet()) {
            env.put(set.getKey().toString(), set.getValue());
        }
        parameters.put("env", env);
        
        return parameters;
    }
    
    protected List<TaskLifeCycleEventListener> getTaskListenersFromDescriptor() {
    	List<TaskLifeCycleEventListener> defaultListeners = new ArrayList<TaskLifeCycleEventListener>();
        DeploymentDescriptor descriptor = getRuntimeManager().getDeploymentDescriptor();
        if (descriptor != null) {
        	Map<String, Object> params = new HashMap<String, Object>();
        	params.put("runtimeManager", getRuntimeManager());
        	params.put("classLoader", getRuntimeManager().getEnvironment().getClassLoader());
        	params.put("kieContainer", getRuntimeManager().getKieContainer());
        	for (ObjectModel model : descriptor.getTaskEventListeners()) {
        		Object taskListener = getInstanceFromModel(model, getRuntimeManager().getEnvironment().getClassLoader(), params);
        		if (taskListener != null) {
        			defaultListeners.add((TaskLifeCycleEventListener) taskListener);
        		}
        	}
        }
        
        return defaultListeners;
    }
    
    protected Map<String, WorkItemHandler> getWorkItemHandlersFromDescriptor(RuntimeEngine runtime) {
    	Map<String, WorkItemHandler> defaultHandlers = new HashMap<String, WorkItemHandler>();
        DeploymentDescriptor descriptor = getRuntimeManager().getDeploymentDescriptor();
        if (descriptor != null) {
        	Map<String, Object> params = getParametersMap(runtime);
        	for (NamedObjectModel model : descriptor.getWorkItemHandlers()) {
        		Object hInstance = getInstanceFromModel(model, getRuntimeManager().getEnvironment().getClassLoader(), params);
        		if (hInstance != null) {
        			defaultHandlers.put(model.getName(), (WorkItemHandler) hInstance);
        		}
        	}
        }
        
        return defaultHandlers;
    }
    
    @SuppressWarnings("unchecked")
	protected <T> List<T>  getEventListenerFromDescriptor(RuntimeEngine runtime, Class<T> type) {
    	List<T> listeners = new ArrayList<T>();
        DeploymentDescriptor descriptor = getRuntimeManager().getDeploymentDescriptor();
        if (descriptor != null) {
        	Map<String, Object> params = getParametersMap(runtime);
        	for (ObjectModel model : descriptor.getEventListeners()) {
        		Object listenerInstance = getInstanceFromModel(model, getRuntimeManager().getEnvironment().getClassLoader(), params);
        		if (listenerInstance != null && type.isAssignableFrom(listenerInstance.getClass())) {
        			listeners.add((T) listenerInstance);
        		} else {
        		    // close/cleanup instance as it is not going to be used at the moment, except these that are cacheable
        		    if (listenerInstance instanceof Closeable && !(listenerInstance instanceof Cacheable)) {
        		        ((Closeable) listenerInstance).close();
        		    }
        		}
        	}
        }
        
        return listeners;
    }
    
    protected Map<String, Object> getGlobalsFromDescriptor(RuntimeEngine runtime) {
    	Map<String, Object> globals = new HashMap<String, Object>();
        DeploymentDescriptor descriptor = getRuntimeManager().getDeploymentDescriptor();
        if (descriptor != null) {
        	Map<String, Object> params = getParametersMap(runtime);
        	for (NamedObjectModel model : descriptor.getGlobals()) {
        		Object gInstance = getInstanceFromModel(model, getRuntimeManager().getEnvironment().getClassLoader(), params);
        		if (gInstance != null) {
        			globals.put(model.getName(), gInstance);
        		}
        	}
        }
        
        return globals;
    }
    
    protected Properties loadJMSProperties() throws IOException {
        Properties properties = new Properties();
        InputStream input = getRuntimeManager().getEnvironment().getClassLoader().getResourceAsStream("/jbpm.audit.jms.properties");
        // required for junit test
        if (input == null) {
            input = getRuntimeManager().getEnvironment().getClassLoader().getResourceAsStream("jbpm.audit.jms.properties");
        }
        properties.load(input);
        
        return properties;
    }
}
