/*
 * 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.CompensationData;
import org.jboss.txbridge.ba.service.ServiceDescription;
import org.jboss.txbridge.ba.data.TaskDescription;
import org.jboss.txbridge.ba.data.CompensationDataParam;
import org.jboss.txbridge.ba.data.CompensationDataReturn;
import org.jboss.txbridge.ba.service.ServiceInformationManager;
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.CompensationActionUnsuccessfulException;
import org.jboss.txbridge.ba.exception.TaskAssociationFailedException;
import org.jboss.txbridge.ba.BATransactionManager;
import org.jboss.txbridge.ba.SingleTransactionManager;
import org.jboss.txbridge.ba.compensation.CompensationDataProviderImpl;
import org.jboss.txbridge.ba.annotation.BACompensationMode;
import org.jboss.txbridge.ba.annotation.BAServiceType;
import org.apache.log4j.Logger;

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 compensation 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;

    // CompensationDataFactory
    private CompensationDataFactory 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;

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

    /**
     * Constructor.
     *
     * @param txId is the transaction identifier.
     * @param serviceId is the service identifier.
     * @param cdf is the compensation data factory that should be used.
     * @param stm is the single transaction manager.
     */
    public Participant(String txId,String serviceId, SingleTransactionManager stm,CompensationDataFactory 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 compensation data factory
        this.cdf = cdf;

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

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

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

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

    }

    /**
     * This method stores an object withing the compensation 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 compensation data
        CompensationData 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 compensation 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 compensation data
        CompensationData 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 compensation object
        log.info("Creating new compensation object");
        CompensationData cd = cdf.createCompensationData();

        // Remember the (taskid,compensationdata) association
        log.info("Remembering compensation 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 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();
    }


    /**
     * This method provides the implementation for executing the compensation 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.CompensationActionUnsuccessfulException if compensation was not successful.
     */
    public void compensateTask(String taskId) throws CompensationActionUnsuccessfulException
    {
        log.info("compensateTask()");

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

        // TODO: Redesign this !!!
        ServiceDescription sd = sim.getServiceById(serviceId);
        if (sd.getServiceType() == BAServiceType.READONLY)
        {
            return;
        }

        // 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 serviceDescription = sim.getServiceById(serviceId);

        // If the compensation is remote than use remote executor. Otherwise use the local one.
        // TODO: Following "if" statement is bad - I should change that -> factory! :)
        BACompensationMode compensationMode = serviceDescription.getCompensationMode();
        log.info("Compensation mode: " + compensationMode);
        ServiceExecutor se;
        if (compensationMode == BACompensationMode.UDII)
        {
            // Use the remote executor.
            log.info("Using remote executor");
            se = new RemoteServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
        }
        else if (compensationMode == BACompensationMode.POJO)
        {
            // Use the local executor.
            log.info("Using local executor");
            se = new LocalServiceExecutor(new TaskDescription(txId,taskId,serviceId,this));
        }
        else if (compensationMode == BACompensationMode.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 CompensationActionUnsuccessfulException("Incorrect compensation mode");
        }
        // Invoke the compensation action
        log.info("Invoking the compensation action");
        se.invokeService(serviceDescription,new CompensationDataProviderImpl(taskId,this));

    }

    /**
     * This method stores a list of arguments that was passed for method's invocation. It is used
     * if the service has its BACompensationType 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 compensation data
        CompensationData cd = compensationData.get(taskId);
        if (cd == null || !(cd instanceof CompensationDataParam))
        {
            return;
        }
        // Put the data
        ((CompensationDataParam)cd).putArguments(arguments);
    }

    /**
     * This method retrieves a list of arguments that was passed for method's invocation. It is
     * used when executing the compensation action if the BACompensationType 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 compensation data
        CompensationData cd = compensationData.get(taskId);
        if (cd == null || !(cd instanceof CompensationDataParam))
        {
            return null;
        }
        // Get the data
        return ((CompensationDataParam)cd).getArguments();
    }

    /**
     * This method stores the return value that will be used as an argument when executing compensation
     * action (when BACompensationType 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 compensation data
        CompensationData cd = compensationData.get(taskId);
        if (cd == null || !(cd instanceof CompensationDataReturn))
        {
            return;
        }
        // Put the data
        ((CompensationDataReturn)cd).putReturnObject(returnObject);
    }

    /**
     * This method gets the return value that will be used as an argument when executing compensation
     * action (when BACompensationType 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 compensation data
        CompensationData cd = compensationData.get(taskId);
        if (cd == null || !(cd instanceof CompensationDataReturn))
        {
            return null;
        }
        // Get the data
        return ((CompensationDataReturn)cd).getReturnObject();
    }

}
