/*
 * 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;

import com.arjuna.wst.*;
import com.arjuna.mw.wst.BusinessActivityManager;
import com.arjuna.ats.arjuna.common.Uid;

import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.io.Serializable;


import org.jboss.txbridge.ba.data.TaskDescription;
import org.jboss.txbridge.ba.service.ServiceDescription;

import org.jboss.txbridge.ba.participant.*;
import org.jboss.txbridge.ba.participant.Participant;
import org.jboss.txbridge.ba.service.ServiceInformationManager;
import org.jboss.txbridge.ba.service.MethodDescription;
import org.jboss.txbridge.ba.exception.TransactionProcessingException;
import org.jboss.txbridge.ba.exception.ActionExecutionException;
import org.jboss.txbridge.ba.annotation.MethodType;
import org.jboss.txbridge.ba.annotation.AgreementType;
import org.jboss.txbridge.ba.annotation.DataMatch;
import org.apache.log4j.Logger;

/**
 * The Single Transaction Manager component is responsible for maintaining information about
 * a single Business Activity. It knows about the exact order of tasks that have been executed
 * within the Business Activity, the participants that have been instantiated for this specific
 * Business Activity and about participant managers responsible for those participants.
 *
 * @author Maciej P. Machulak (mmachulak@redhat.com)
 * @version 0.1
 */
public class SingleTransactionManager implements Serializable
{
    // Logger
    private static Logger log = Logger.getLogger(SingleTransactionManager.class);

    // Business activity manager
    private static BusinessActivityManager businessActivityManager;

    // Service information manager
    private static ServiceInformationManager serviceInformationManager;

    // Transaction identifier
    private String txId;

    // Completed transaction
    private boolean transactionCompleted;

    // List of tasks for this transaction
    private List<String> taskList;

    // List of tasks for this transaction that should be compensated first
    // TODO: Implement this
    private List<String> taskListFirst;

    // List of tasks for this transaction that should be compensated last
    // TODO: Implement this
    private List<String> taskListLast;


    // List of already executed single service list
    private List<String> singleServiceList;

    // (task id , service id) mapping
    private ConcurrentMap<String,String> taskServiceMapping;

    // (service id , participant) mapping
    private ConcurrentMap<String,Participant> serviceParticipantMapping;

    // (task id (participant) , participant manager) mapping - for participant completion
    private ConcurrentMap<String,BAParticipantManager> taskBAManagerMapping;

    // (participant , participant manager) mapping - for coordinator completion
    private ConcurrentMap<Participant,BAParticipantManager> participantManagerMapping;



    public SingleTransactionManager(String txId)
    {
        log.info("constructor()");

        // Remember the transaction identifier
        this.txId = txId;

        // Get reference to the business activity manager
        businessActivityManager = BusinessActivityManager.getBusinessActivityManager();

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

        // Initialise the list of tasks for this transaction
        taskList = new Stack<String>();

        // Initialise the list of tasks for this transaction
        taskListFirst = new Stack<String>();

        // Initialise the list of tasks for this transaction
        taskListLast = new Stack<String>();

        // Initialise the list of already executed single services
        singleServiceList = new ArrayList<String>();

        // Initialise (task id , service id) mapping
        taskServiceMapping = new ConcurrentHashMap<String,String>();

        // Initialise (service id , participant) mapping
        serviceParticipantMapping = new ConcurrentHashMap<String,Participant>();

        // Initialise (task id (participant) , participant manager mapping)
        taskBAManagerMapping = new ConcurrentHashMap<String,BAParticipantManager>();

        // Initialise (participant, participant manager mapping)
        participantManagerMapping = new ConcurrentHashMap<Participant,BAParticipantManager>();

        // By default transaction is not completed
        transactionCompleted = false;

    }

    /**
     * This method processes a single invocation within a scope of a transaction this single transaction
     * manager is responsible for. It either creates a new participant for the service that is being invoked
     * or associates this invocation with an existing participant.
     *
     * @param taskId is the task identifier.
     * @param serviceId is the service identifier.
     * @return the participant associated with the invocation.
     * @throws org.jboss.txbridge.ba.exception.TransactionProcessingException if an error occurs
     */
    public Participant processInvocation(String taskId, String serviceId) throws TransactionProcessingException
    {
        log.info("processInvocation()");

        // If the manager is aware of this task than there must be an error
        if (taskList.contains(taskId) || taskServiceMapping.containsKey(taskId))
        {
            log.info("Task is already known");
            throw new TransactionProcessingException("Task identifier already known");
        }

        try
        {
            // Remember the task and its service
            log.info("Remembering the task and the service.");
            taskList.add(taskId);
            taskServiceMapping.put(taskId,serviceId);

            // Check if there is an existing participant associated with this service and transaction
            // If not - then create a new one basing upon the required agreement protocol
            // 1) Get information about the service
            ServiceDescription sd = serviceInformationManager.getServiceById(serviceId);
            if (sd == null)
            {
                throw new TransactionProcessingException("Service unknown");
            }
            // 2) Get the participant - if null then create a new one
            log.info("Checking for participant...");
            Participant participant = serviceParticipantMapping.get(serviceId);
            boolean participantCreated = !(participant == null);
            AgreementType agreementType = sd.getAgreementType();
            if ( participant == null )
            {
                log.info("Participant for this transaction and this service is unknown");

                // Create a new participant
                // 1) Determine if the service will need a real managed object
                ManagedDataFactory cdf;
                MethodType serviceType = sd.getMethodType();
                cdf = new ManagedDataFactoryImpl();
                if (serviceType == MethodType.READONLY)
                {
                    cdf = new DummyManagedDataFactory();
                }
                // 2) Create the participant
                log.info("Creating a new participant");

                if (agreementType == AgreementType.PARTICIPANT_COMPLETION)
                {
                    log.info("Agreement protocol type: " + AgreementType.PARTICIPANT_COMPLETION);
                    participant = new ParticipantCompletionParticipant(txId,serviceId,this,cdf);
                }
                else if (agreementType == AgreementType.COORDINATOR_COMPLETION)
                {
                    log.info("Agreement protocol type: " + AgreementType.COORDINATOR_COMPLETION);
                    participant = new CoordinatorCompletionParticipant(txId,serviceId,this,cdf);
                }
                else
                {
                    throw new TransactionProcessingException("Unsupported agreement type: " + agreementType);
                }

                // Remember the participant
                log.info("Storing reference to the participant");
                serviceParticipantMapping.put(serviceId,participant);
            }

            // Enlist the participant
            log.info("Enlisting participant if necessary");
            BAParticipantManager participantManager;
            if (agreementType == AgreementType.PARTICIPANT_COMPLETION)
            {
                log.info("Protocol: " + AgreementType.PARTICIPANT_COMPLETION);
                participantManager = businessActivityManager.enlistForBusinessAgreementWithParticipantCompletion((BusinessAgreementWithParticipantCompletionParticipant)participant,new Uid().toString());

                // Remember the participant manager for this specific participant
                log.info("Storing reference to the participant manager for task: " + taskId);
                taskBAManagerMapping.put(taskId,participantManager);
            }
            else if (agreementType == AgreementType.COORDINATOR_COMPLETION)
            {
                log.info("Protocol: " + AgreementType.COORDINATOR_COMPLETION);
                if (!participantCreated)
                {
                    participantManager = businessActivityManager.enlistForBusinessAgreementWithCoordinatorCompletion((BusinessAgreementWithCoordinatorCompletionParticipant)participant,new Uid().toString());

                    // Remember the participant manager for this specific participant
                    log.info("Storing reference to the participant manager for task: " + taskId);
                    taskBAManagerMapping.put(taskId,participantManager);
                    participantManagerMapping.put(participant,participantManager);
                }
                else
                {
                    participantManager = participantManagerMapping.get(participant);
                    if ( participantManager == null)
                    {
                        throw new TransactionProcessingException("Cannot obtain reference to the BA Participant Manager");
                    }
                    log.info("Storing reference to the participant manager for task: " + taskId);
                    taskBAManagerMapping.put(taskId,participantManager);
                }
            }
            else
            {
                throw new TransactionProcessingException("Unsupported agreement type: " + sd.getAgreementType());
            }

            // As now we have the reference to the participant and we have enlisted it to play role in a business
            // activity we can associate a task with it.
            log.info("Associating task with the participant: " + taskId);
            participant.associateTask(taskId);

            // We return the reference to the participant.
            return participant;
        }
        catch (Exception e)
        {
            log.info("Removing task identifier");
            taskList.remove(taskId);
            log.info("Removing (task,service) mapping");
            taskServiceMapping.remove(taskId,serviceId);
            log.info("Removing (task,participant manager) mapping");
            taskBAManagerMapping.remove(taskId);
            throw new TransactionProcessingException(e.getMessage());
        }



    }

    /**
     * This method handles transaction management for a certain invocation of a method when
     * this invocation has already completed.
     *
     * @param taskDesc is the task description.
     * @param arguments is the list of original arguments.
     * @param returnObject is the return object.
     * @throws SystemException if there was an error.
     * @throws UnknownTransactionException if there was an error.
     * @throws WrongStateException if there was an error.
     * @throws TransactionProcessingException if there was an error.
     */
    public void completeInvocation(TaskDescription taskDesc, Object[] arguments, Object returnObject)
            throws SystemException, UnknownTransactionException, WrongStateException, TransactionProcessingException
    {
        log.info("completeInvocation()");

        // Get the service description
        ServiceDescription sd = serviceInformationManager.getServiceById(taskDesc.getServiceId());
        if (sd == null)
        {
            // If there is no servive than we throw an exception
            log.info("No service description.");
            throw new TransactionProcessingException("No service description");
        }

        // Get method descriptions
        MethodDescription originalMethod = sd.getMethod("original");
        MethodDescription completeMethod = sd.getMethod("complete");
        MethodDescription compensateMethod = sd.getMethod("compensate");

        // Get reference to the participant
        Participant participant = taskDesc.getParticipant();
        String taskId = taskDesc.getTaskId();

        // Remember necessary data basing upon the service type
        MethodType serviceType = sd.getMethodType();
        if (serviceType == MethodType.MODIFY)
        {
            if (completeMethod.getDataMatch() == DataMatch.CUSTOM || compensateMethod.getDataMatch() == DataMatch.CUSTOM)
            {

                // 2) Get the annotation values
                log.info("Getting annotation values");
                Object[] annotations = originalMethod.getParameterAnnotations();

                // 3) Remember parameters
                log.info("Remembering parameters");
                for (int i = 0; i < annotations.length ; i++)
                {
                    if (annotations[i] != null)
                    {
                        log.info("Annotation: " + annotations[i]);
                        log.info("Object: " + arguments[i]);
                        participant.put(taskId,annotations[i],arguments[i]);
                    }
                }

                // 4) Remember the return object if necessary
                log.info("Remembering return object if necessary");
                Object returnId = originalMethod.getReturnId();
                if (returnId != null)
                {
                    log.info("Return ID: " + returnId);
                    participant.put(taskId,returnId,returnObject);
                }
            }
            if (completeMethod.getDataMatch() == DataMatch.PARAMETERS_MATCH || compensateMethod.getDataMatch() == DataMatch.PARAMETERS_MATCH)
            {
                log.info("Remembering arguments");
                participant.putArguments(taskId,arguments);
            }
            if (completeMethod.getDataMatch() == DataMatch.RETURN_VALUE || compensateMethod.getDataMatch() == DataMatch.RETURN_VALUE)
            {
                log.info("Remembering return value");
                participant.putReturn(taskId,returnObject);
            }

        }

        if (sd.getAgreementType() == AgreementType.PARTICIPANT_COMPLETION)
        {
            log.info("Completeing work");
            completeTask(taskId,serviceType);
        }
        else
        {
            log.info("Work might be completed later");
        }
    }

    /**
     * This method completes the work done by the given participant within a certain transaction.
     *
     * @param taskId is the participant which wants to completeTask its work.
     * @param serviceType is the type of the service.
     * @throws org.jboss.txbridge.ba.exception.TransactionProcessingException if processing was not successful.
     * @throws com.arjuna.wst.SystemException if processing was not successful.
     * @throws com.arjuna.wst.UnknownTransactionException if processing was not successful.
     * @throws com.arjuna.wst.WrongStateException if processing was not successful.
     */
    public void completeTask(String taskId, MethodType serviceType)
            throws TransactionProcessingException, SystemException, UnknownTransactionException, WrongStateException
    {
        log.info("completeTask()");
        log.info("Getting reference to the BA Participant Manager for task: " + taskId);
        BAParticipantManager baParticipantManager = taskBAManagerMapping.get(taskId);
        if ( baParticipantManager != null )
        {
            if (serviceType == MethodType.MODIFY)
            {
                log.info("Informing coordinator that work has completed");
                baParticipantManager.completed();
            }
            else if (serviceType == MethodType.READONLY)
            {
                log.info("Exiting from BA");
                baParticipantManager.exit();
            }
            else
            {
                throw new TransactionProcessingException("Unsupported service type: " + serviceType);
            }
        }
        else
        {
            throw new TransactionProcessingException("This task does not have a Participant Manager associated with it");
        }
    }

    /**
     * This method cleans invocation - if there was an error with invoking the required service
     * then transaction-related information is removed.
     *
     * @param taskDesc is the description of a certain task.
     * @throws TransactionProcessingException an internal error.
     */
    public void cleanInvocation(TaskDescription taskDesc)
            throws TransactionProcessingException
    {
        log.info("cleanInvocation()");

        // Remove task from the list
        log.info("Removing task from the list");
        taskList.remove(taskDesc.getTaskId());

        // Remove (task,service) mapping
        log.info("Removing (task,service) mapping");
        taskServiceMapping.remove(taskDesc.getTaskId(),taskDesc.getServiceId());

        // Get reference to the participant
        log.info("Getting reference to the participant");
        Participant participant = taskDesc.getParticipant();

        // Ask the participant to remove the task
        participant.removeTask(taskDesc.getTaskId());

        // Get reference to the BA Participant Manager
        log.info("Checking for BA Participant Manager");
        BAParticipantManager baParticipantManager = taskBAManagerMapping.get(taskDesc.getTaskId());
        if ( baParticipantManager != null )
        {
            log.info("BA Participant Manager found.");
            // TODO: Communicate an error?
        }
        else
        {
            throw new TransactionProcessingException("This task does not have a Participant Manager associated with it");
        }
    }

    /**
     * This method is executed by the participant. It asks the Single Transaction Manager to complete
     * all the work that needs to be completed within the transaction.
     */
    public void complete()
    {
        log.info("complete()");
        transactionCompleted = true;
        synchronized(this)
        {
            log.info("Transaction asked for completion: " + transactionCompleted);
            if (!transactionCompleted)
            {
                return;
            }
        }

        log.info("Building list of CoordinatorCompletionParticipants");
        List<CoordinatorCompletionParticipant> participantList = new ArrayList<CoordinatorCompletionParticipant>();
        Map<Participant,BAParticipantManager> managerList = new HashMap<Participant,BAParticipantManager>();

        for (String task : taskList)
        {
            log.info("Checking task: " + task);
            // Get reference to the required participant
            log.info("Getting service ID for this task");
            String serviceId = taskServiceMapping.get(task);
            log.info("Service ID: " + serviceId);
            if (serviceId == null)
            {
                // TODO: Reconsider this
                log.info("Incorrect service!");
                continue;
            }
            ServiceDescription sd = serviceInformationManager.getServiceById(serviceId);
            Participant participant = serviceParticipantMapping.get(serviceId);
            if (participant instanceof CoordinatorCompletionParticipant)
            {
                log.info("This service was enlisted for CoordinatorCompletion protocol");
                if (!participantList.contains(participant))
                {
                    // Get reference to the BA Participant manager
                    log.info("Getting reference to the BA Participant Manager for task: " + task);
                    BAParticipantManager participantManager = taskBAManagerMapping.get(task);
                    if (participantManager != null)
                    {
                        log.info("Adding participant");
                        participantList.add((CoordinatorCompletionParticipant)participant);
                        managerList.put(participant,participantManager);
                    }
                    else
                    {
                        log.info("Cannot find participant manager - participant not added.");
                    }
                }
                else
                {
                    log.info("Participant already added to the list");
                }

            }
            else
            {
                log.info("This service was enlisted for ParticipantCompletion protocol");
            }

        }
        completeList(participantList,managerList);
    }

    /**
     * This method loops through a list of participants that should complete its work. After each
     * participant completes its work, its associated BAParticipantManager informs the coordinator
     * that the work has completed. Otherwise a fault is sent to the Coordinator.
     *
     * @param participantList is the list of participants that should complete their work.
     * @param managerList is the list of BAParticipantManagers associated with participants
     */
    private void completeList(List<CoordinatorCompletionParticipant> participantList, Map<Participant,BAParticipantManager> managerList)
    {
        log.info("completeList()");

        for (CoordinatorCompletionParticipant participant : participantList)
        {
            BAParticipantManager participantManager = managerList.get(participant);
            if (participantManager != null)
            {
                try
                {
                    log.info("Completing work of a single participant.");
                    participant.completeWork();
                    log.info("Informing coordinator that work has completed");
                    participantManager.completed();
                }
                catch (Exception e)
                {
                    log.info("Could not inform coordinator that work has completed!");
                    e.printStackTrace();
                    try
                    {
                        log.info("Informing coordinator that there is a fault!");
                        participantManager.fault();
                    }
                    catch (SystemException e1)
                    {
                        log.info("Could not inform coordinator that there is a fault!");
                        e1.printStackTrace();
                    }
                }
            }
        }
    }

    /**
     * This method executes datamgmt for all tasks that are associated with this transaction.
     * It executes a compensationList() method on all lists that have been defined.
     */
    public void compensate()
    {
        log.info("compensate()");

        // Compensate tasks that should be compensated first
        // compensateList((Stack)taskListFirst);

        // Compensate normal tasks
        compensateList((Stack<String>)taskList);

        // Compensate tasks that should be compensated last
        // compensateList((Stack)taskListLast);

    }

    /**
     * This method executes a datamgmt for a given list of tasks. It gets reference to participants
     * responsible for different tasks and asks them to compensate those tasks. If there is an error with
     * compensating a task it is communicated to the coordinator so that the client knows if heuristics
     * occur.
     *
     * @param taskList is the list of tasks which should be compensated.
     */
    private void compensateList(Stack<String> taskList)
    {
        log.info("compensateList()");

        // Loop through all the tasks and compensate each task separately
        while (!taskList.isEmpty())
        {
            // Get the task from the top
            log.info("Getting task from the top");
            String taskId = taskList.pop();
            log.info("Task ID: " + taskId);

            // Get reference to the required participant
            log.info("Getting service ID for this task");
            String serviceId = taskServiceMapping.get(taskId);
            log.info("Service ID: " + serviceId);

            if (serviceId == null)
            {
                // TODO: Reconsider this
                log.info("Incorrect service!");
                continue;
            }

            // If service was read only then take another task
            log.info("Checking if the service was read-only");
            ServiceDescription sd = serviceInformationManager.getServiceById(serviceId);
            if (sd.getMethodType() == MethodType.READONLY)
            {
                log.info("Service was read-only. There is nothing to compensate.");
                continue;
            }
            else
            {
                log.info("Service might need to be compensated");
            }

            // Check if the service should be compensated only once
            MethodDescription compensationMethod = sd.getMethod("compensate");
            log.info("Checking if the service should be compensated only once");
            if (compensationMethod.isSingle())
            {
                log.info("Service should be compensated only once");
                // If it has been already executed then take another task
                if (singleServiceList.contains(serviceId))
                {
                    log.info("Service has been already compensated");
                    continue;
                }
                else
                {
                    // Remember this service
                    log.info("Service has not been yet compensated");
                    singleServiceList.add(serviceId);
                }
            }

            log.info("This task is associated with service ID: " + serviceId);
            Participant participant = serviceParticipantMapping.get(serviceId);
            MethodDescription originalMethod = sd.getMethod("original");
            log.info("Compensating: " + originalMethod.getMethodName());
            log.info("Compensation action: " + compensationMethod.getMethodName());
            if ( participant != null )
            {
                // Order the participant to compensate a given single task
                // TODO: Reconsider this invocation
                log.info("Participant found for this service - trying to compensate");
                try
                {
                    participant.compensateTask(taskId);
                }
                catch (ActionExecutionException e)
                {
                    e.printStackTrace();
                    log.info("Could not compensate a task - informing the coordinator about an error");

                    // Get reference to the BA Participant manager
                    log.info("Getting reference to the BA Participant Manager for task: " + taskId);
                    BAParticipantManager participantManager = taskBAManagerMapping.get(taskId);
                    if (participantManager != null)
                    {
                        try
                        {
                            // Communicate an error to the coordinator
                            participantManager.fault();
                            log.info("Fault communicated to the coordinator.");
                        }
                        catch (SystemException e1)
                        {
                            e1.printStackTrace();
                            log.info("Could not communicate error to the coordinator: " + e1.getMessage());
                        }
                    }
                    else
                    {
                        // ...this should never occur...
                        log.info("BA Participant Manager for this participant has not been found.");
                    }

                }
            }
            else
            {
                // ...this should never occur...
                log.info("Participant for this service has not been found");
            }
        }

    }
}