/*
 * JBoss, Home of Professional Open Source
 * Copyright 2007, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags.
 * See the copyright.txt in the distribution for a full listing
 * of individual contributors.
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License, v. 2.0.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License,
 * v. 2.0 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 *
 * (C) 2007,
 * @author JBoss Inc.
 */
package org.jboss.txbridge.ba.participant;

import org.jboss.txbridge.ba.data.ManagedData;
import org.jboss.txbridge.ba.service.ServiceDescription;
import org.jboss.txbridge.ba.data.TaskDescription;
import org.jboss.txbridge.ba.service.ServiceInformationManager;
import org.jboss.txbridge.ba.service.MethodDescription;
import org.jboss.txbridge.ba.execute.ServiceExecutor;
import org.jboss.txbridge.ba.execute.RemoteServiceExecutor;
import org.jboss.txbridge.ba.execute.LocalServiceExecutor;
import org.jboss.txbridge.ba.execute.EJBExecution;
import org.jboss.txbridge.ba.exception.ActionExecutionException;
import org.jboss.txbridge.ba.exception.TaskAssociationFailedException;
import org.jboss.txbridge.ba.BATransactionManager;
import org.jboss.txbridge.ba.SingleTransactionManager;
import org.jboss.txbridge.ba.datamgmt.ExecutionDataProviderImpl;
import org.jboss.txbridge.ba.annotation.ExecutionMode;
import org.jboss.txbridge.ba.annotation.MethodType;
import org.apache.log4j.Logger;

import javax.transaction.UserTransaction;
import javax.naming.InitialContext;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.ArrayList;

/**
 * This is an abstract class for Participant. It provides the implemention of methods that
 * are related to datamgmt and completing the work and are common for following participants:
 * - Coordinator Completion Participant
 * - Participant Completion Participant
 *
 * @author Maciej P. Machulak (mmachulak@redhat.com)
 * @version 0.1
 */
public abstract class Participant
{
    // Logger
    private static Logger log = Logger.getLogger(Participant.class);

    // Service Information Manager
    protected ServiceInformationManager sim = null;

    // BA Manager
    private static BATransactionManager baTransactionManager = null;

    // ManagedDataFactory
    private ManagedDataFactory cdf = null;

    // Single transaction manager
    protected SingleTransactionManager stm = null;

    // Transaction identifier
    private String txId = null;

    // Service identifier
    protected String serviceId = null;

    // List of all tasks
    protected List<String> taskList = null;

    // List of compensated tasks
    private boolean compensated;

    // Mapping (taskId,compensation_data)
    private ConcurrentMap<String, ManagedData> compensationData = null;

    /**
     * Constructor.
     *
     * @param txId is the transaction identifier.
     * @param serviceId is the service identifier.
     * @param cdf is the datamgmt data factory that should be used.
     * @param stm is the single transaction manager.
     */
    public Participant(String txId,String serviceId, SingleTransactionManager stm, ManagedDataFactory cdf)
    {
        log.info("constructor()");

        // Remember the Single Transaction Manager
        this.stm = stm;

        // Remember transaction identifier
        this.txId = txId;

        // Remember the service identifier
        this.serviceId = serviceId;

        // Remember the datamgmt data factory
        this.cdf = cdf;

        // Get reference to the service information manager
        sim = ServiceInformationManager.getSingletonInstance();

        // Initialise the list of tasks
        taskList = new ArrayList<String>();

        // Not compensated
        compensated = false;

        // Initialise (taskId, datamgmt data) mapping
        compensationData = new ConcurrentHashMap<String, ManagedData>();

        // Get reference to the BA Transaction Manager
        baTransactionManager = BATransactionManager.getSingletonInstance();

    }

    /**
     * This method stores an object withing the datamgmt data for a given task.
     * The object is assigned an ID.
     *
     * @param taskId is the ID of the task.
     * @param objectId is the ID of the object.
     * @param object is the object to be stored.
     */
    public void put(String taskId,Object objectId,Object object)
    {
        log.info("put(" + objectId + ")");
        // Get the correct datamgmt data
        ManagedData cd = compensationData.get(taskId);
        if (cd == null)
        {
            return;
        }
        // Put the data
        if (objectId instanceof Integer || objectId instanceof String)
        {
            cd.put(objectId,object);
        }
    }

    /**
     * This method retrieves an object with a given identifier from the datamgmt data
     * that is stored for a given task id.
     *
     * @param taskId is the ID of the task.
     * @param objectId is the ID of the object.
     * @return the object with a given ID.
     */
    public Object get(String taskId,Object objectId)
    {
        log.info("get(" + objectId + ")");
        // Get the correct datamgmt data
        ManagedData cd = compensationData.get(taskId);
        if (cd == null)
        {
            return null;
        }
        if (objectId instanceof Integer || objectId instanceof String)
        {
            return cd.get(objectId);
        }
        return null;
    }

    /**
     * This method associates a new task with the existing participant.
     *
     * @param taskId is the task identifier.
     * @throws org.jboss.txbridge.ba.exception.TaskAssociationFailedException if task association fails.
     */
    public void associateTask(String taskId) throws TaskAssociationFailedException
    {

        log.info("associateTask()");

        // Check correctness of parameters
        if (taskList.contains(taskId))
        {
            throw new TaskAssociationFailedException("Task already associated");
        }

        // Remember the task
        log.info("Remember task ID: " + taskId);
        taskList.add(taskId);

        // Create a new datamgmt object
        log.info("Creating new datamgmt object");
        ManagedData cd = cdf.createManagedData();

        // Remember the (taskid,compensationdata) association
        log.info("Remembering datamgmt data");
        compensationData.put(taskId,cd);

    }

    /**
     *
     * @param taskId is the ID of a task that should be removed from the list.
     */
    public void removeTask(String taskId)
    {
        log.info("removeTask()");

        // Remove the task
        if (taskList.remove(taskId))
        {
            log.info("Task " + taskId + " remove successfully.");
        }
        else
        {
            log.info("Task could not be removed.");
        }

        // Remove the (task,compensation_data) mapping
        compensationData.remove(taskId);
    }

    /**
     * This method is invoked by the participant object if a transaction should complete.
     * It asks the Single Transaction Manager to complete the transaction.
     */
    void completeTransaction()
    {
        log.info("completeTransaction()");
        Thread completionNotifier = new Thread(new CompletionNotifier(txId,stm));
        completionNotifier.start();
    }

    /**
     * This method is invoked by the participant object if a transaction should compensate.
     * It asks the BA Transaction Manager to compensate the transaction.
     */
    void compensateTransaction()
    {
        log.info("compensateTransaction()");
        Thread compensationNotifier = new Thread(new CompensationNotifier(txId,baTransactionManager));
        compensationNotifier.start();
    }

    /**
     * This method is invoked by the participant object if a transaction is finished.
     * It asks the BA Transaction Manager to forget about the transaction.
     */
    void closeTransaction()
    {
        log.info("closeTransaction()");
        Thread closeNotifier = new Thread(new CloseNotifier(txId,baTransactionManager));
        closeNotifier.start();
    }


    public void completeWork() throws ActionExecutionException
    {
        log.info("completeWork()");
        log.info("Starting transaction");
        try
        {
            InitialContext initialContext = new InitialContext();
            UserTransaction transaction = (UserTransaction) initialContext.lookup("java:comp/UserTransaction");
            transaction.begin();
            for (String taskId : taskList)
            {
                log.info("Completeing task ID: " + taskId);

                // Obtain information about the service
                // TODO: Participant should have access to the service description, not just the ID
                log.info("Getting information about the service ID: " + serviceId);
                ServiceDescription sd = sim.getServiceById(serviceId);

                // If the execution is remote than use remote executor. Otherwise use the local one.
                // TODO: Following "if" statement is bad - I should change that -> factory! :)
                MethodDescription completeMethod = sd.getCompletionService();
                ExecutionMode executionMode = completeMethod.getExecutionMode();
                log.info("Execution mode: " + executionMode);
                ServiceExecutor se;
                if (executionMode == ExecutionMode.UDII)
                {
                    // Use the remote executor.
                    log.info("Using remote executor");
                    se = new RemoteServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
                }
                else if (executionMode == ExecutionMode.POJO)
                {
                    // Use the local executor.
                    log.info("Using local executor");
                    se = new LocalServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
                }
                else if (executionMode == ExecutionMode.EJB)
                {
                    // Use the local executor.
                    log.info("Using local executor");
                    se = new LocalServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
                    se.setExecutionType(new EJBExecution());
                }
                else
                {
                    throw new ActionExecutionException("Incorrect execution mode");
                }
                // Invoke the datamgmt action
                log.info("Invoking the completion action");
                se.invokeService(completeMethod,new ExecutionDataProviderImpl(taskId,this));
            }
            log.info("Committing");
            transaction.commit();
        }
        catch (Exception e)
        {
            throw new ActionExecutionException(e);
        }

    }

    /**
     * This method provides the implementation for executing the datamgmt action.
     * It uses the Executor class for executing a single method/service.
     *
     * @param taskId is the ID of the task that is supposed to be compensated.
     * @throws org.jboss.txbridge.ba.exception.ActionExecutionException if datamgmt was not successful.
     */
    public void compensateTask(String taskId) throws ActionExecutionException
    {
        log.info("compensateTask()");

        log.info("Compensating task ID: " + taskId);

// TODO: Redesign this !!!
        log.info("Getting information about the service ID: " + serviceId);
        ServiceDescription sd = sim.getServiceById(serviceId);
        if (sd.getMethodType() == MethodType.READONLY)
        {
            return;
        }

        // If the datamgmt is remote than use remote executor. Otherwise use the local one.
        // TODO: Following "if" statement is bad - I should change that -> factory! :)
        MethodDescription compensationMethod = sd.getCompensationService();

        ExecutionMode executionMode = compensationMethod.getExecutionMode();
        log.info("Execution mode: " + executionMode);
        ServiceExecutor se;
        if (executionMode == ExecutionMode.UDII)
        {
            // Use the remote executor.
            log.info("Using remote executor");
            se = new RemoteServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
        }
        else if (executionMode == ExecutionMode.POJO)
        {
            // Use the local executor.
            log.info("Using local executor");
            se = new LocalServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
        }
        else if (executionMode == ExecutionMode.EJB)
        {
            // Use the local executor.
            log.info("Using local executor");
            se = new LocalServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
            se.setExecutionType(new EJBExecution());
        }
        else
        {
            throw new ActionExecutionException("Incorrect execution mode");
        }
        // Invoke the datamgmt action
        log.info("Invoking the compensation action");
        se.invokeService(sd.getCompensationService(),new ExecutionDataProviderImpl(taskId,this));

    }

    /**
     * This method stores a list of arguments that was passed for method's invocation. It is used
     * if the service has its DataMatch set to PARAM_MATCH.
     *
     * @param taskId is the ID of the task.
     * @param arguments is the list of arguments.
     */
    public void putArguments(String taskId, Object[] arguments)
    {
        log.info("putArguments()");
// Get the correct datamgmt data
        ManagedData cd = compensationData.get(taskId);
        if (cd == null)
        {
            return;
        }
        // Put the data
        cd.putArguments(arguments);
    }

    /**
     * This method retrieves a list of arguments that was passed for method's invocation. It is
     * used when executing the datamgmt action if the DataMatch is set to PARAM_MATCH.
     *
     * @param taskId is the task ID.
     * @return the list of arguments.
     */
    public Object[] getArguments(String taskId)
    {
        log.info("getArguments()");
// Get the correct datamgmt data
        ManagedData cd = compensationData.get(taskId);
        if (cd == null)
        {
            return null;
        }
        // Get the data
        return cd.getArguments();
    }

    /**
     * This method stores the return value that will be used as an argument when executing datamgmt
     * action (when DataMatch is set to RETURN_VALUE)
     *
     * @param taskId is the task identifier.
     * @param returnObject is the return object.
     */
    public void putReturn(String taskId, Object returnObject)
    {
        log.info("putReturn()");
// Get the correct datamgmt data
        ManagedData cd = compensationData.get(taskId);
        if (cd == null)
        {
            return;
        }
        // Put the data
        cd.putReturnObject(returnObject);
    }

    /**
     * This method gets the return value that will be used as an argument when executing datamgmt
     * action (when DataMatch is set to RETURN_VALUE)
     *
     * @param taskId is the task ID.
     * @return the return object.
     */
    public Object[] getReturn(String taskId)
    {
        log.info("getReturn()");
// Get the correct datamgmt data
        ManagedData cd = compensationData.get(taskId);
        if (cd == null)
        {
            return null;
        }
        // Get the data
        return cd.getReturnObject();
    }

}
