/*
 * 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.mw.wst.BusinessActivityManager;
import com.arjuna.mw.wst.TxContext;
import com.arjuna.wst.SystemException;
import org.apache.log4j.Logger;
import org.jboss.txbridge.ba.data.TaskDescription;
import org.jboss.txbridge.ba.exception.TransactionProcessingException;
import org.jboss.txbridge.ba.helper.BAServiceVisitor;
import org.jboss.txbridge.ba.id.IdentifierFactory;
import org.jboss.txbridge.ba.id.UidIdentifierFactory;
import org.jboss.txbridge.ba.participant.Participant;
import org.jboss.txbridge.ba.service.ServiceInformationManager;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * This is the main component - the Business Activity Transaction Manager. It is responsible
 * for associating fully unique task identifiers to different invocations. It also knows about
 * all existing components responsible for single transactions.
 *
 * @author Maciej P. Machulak (mmachulak@redhat.com)
 * @version 0.1
 */
public class BATransactionManager
{
    // Logger
    private static Logger log = Logger.getLogger(BATransactionManager.class);

    // BA Transaction Manager instance
    private volatile static BATransactionManager baTransactionManager = null;

    // Business Activity manager
    private BusinessActivityManager businessActivityManager;

    // Service information manager
    private ServiceInformationManager serviceManager;

    // Mappings:
    // Business Activity ID <-> Transaction Manager
    private ConcurrentMap<String, SingleTransactionManager> singleTransactionManagers;


    // Constructor
    private BATransactionManager()
    {
        log.info("constructor()");

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

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

        // Initialise mappings
        singleTransactionManagers = new ConcurrentHashMap<String, SingleTransactionManager>();

    }

    /**
     * Returns the instance of the BATRansactionManager object.
     *
     * @return the instance of the BATransactionManager.
     */
    public static BATransactionManager getSingletonInstance()
    {
        log.info("getSingletonInstance()");
        if (baTransactionManager == null)
        {
            synchronized (BATransactionManager.class)
            {
                if (baTransactionManager == null)
                {
                    log.info("Creating new instance...");
                    baTransactionManager = new BATransactionManager();
                }
            }
        }
        return baTransactionManager;
    }

    /**
     * This method returns true if there is currently a Business Activity.
     *
     * @return the boolean value.
     * @throws SystemException if it is not possible to get current transaction.
     */
    public synchronized boolean isBAPresent() throws SystemException
    {
        return businessActivityManager.currentTransaction() != null;
    }

    /**
     * This method handles a transaction for this invocation of a method.
     *
     * @param method is the method which is being invoked.
     * @return the task identifier for this method.
     * @throws org.jboss.txbridge.ba.exception.TransactionProcessingException if there is a problem with processing a transaction
     */
    public synchronized TaskDescription handleInvocation(Method method) throws TransactionProcessingException
    {
        log.info("handleInvocation()");
        String txId; // Transaction identifier
        try
        {
            // Get the transaction identifier
            // May throw:
            // - WrongStateException
            // - SystemException
            log.info("Getting transaction identifier");
            TxContext txContext = businessActivityManager.currentTransaction();
            if (txContext == null)
            {
                log.info("No transaction context!");
                throw new TransactionProcessingException("No transaction context");
            }
            txId = txContext.toString();
            log.info(txId);

            // If there was a transaction then we can proceed...

            // Process this service if necessary
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            log.info("Class: " + className);
            log.info("Method: " + methodName);
            log.info("Checking if the service is known");
            if (!serviceManager.knowsAboutByName(className+methodName))
            {
                // If service is unknown - process it
                // May throw:
                // - MethodIncorrectlyAnnotatedException
                // - CompensationMethodNotAccessible
                // TODO: Processing services and checking should be done during deployment time
                log.info("Service unknown - processing");
                serviceManager.storeServiceDescription(BAServiceVisitor.processMethod(method));
            }
            else
            {
                log.info("Service already known...");
            }

            // Get the service identifier
            log.info("Getting service ID");
            String serviceId = serviceManager.getServiceId(className+methodName);
            log.info("Service ID: " + serviceId);

            // Get a new task identifier - has to be done in the main component to ensure uniqueness
            log.info("Getting new task identifier");
            IdentifierFactory idf = new UidIdentifierFactory();
            String taskId = idf.getIdentifier();
            log.info("Task ID: " + taskId);

            // Get a reference to a single transaction manager. If there is none - create a new one.
            log.info("Getting reference to a single transaction manager for: " + txId);
            SingleTransactionManager stm = singleTransactionManagers.get(txId);
            if (stm == null)
            {
                log.info("No manager associated - creating new one");

                // Create a new single transaction manager for this transaction
                stm = new SingleTransactionManager(txId);

                // Remember it
                log.info("Remembering association (txId,singleManager)");
                singleTransactionManagers.put(txId,stm);
            }
            else
            {
                log.info("Transaction already known - single transaction manager retrieved");
            }

            // Delegate transaction management to the single transaction manager
            log.info("Delegating transaction management to the single transaction manager");
            Participant participant = stm.processInvocation(taskId,serviceId);
            if (participant == null)
            {
                throw new TransactionProcessingException("Could not create participant");
            }
            log.info("Returning new task description");
            return new TaskDescription(txId,taskId,serviceId,participant);

        }
        catch (Exception se)
        {
            se.printStackTrace();
            throw new TransactionProcessingException(se.getMessage());
        }
    }

    /**
     * This method compensates a transaction with a given ID. It delegates the compensation management
     * to an appropriate single transaction manager.
     *
     * @param txId is the ID of the transaction which should be compensated.
     */
    public void compensateTransaction(String txId)
    {
        log.info("compensateTransaction()");
        // Get the single transaction manager for this transaction and remove it from the list
        SingleTransactionManager singleTransactionManager = null;
        log.info("Getting reference to the Single Transaction Manager");
        singleTransactionManager = singleTransactionManagers.remove(txId);
        if (singleTransactionManager == null)
        {
            log.info("Compensation already delegated to the Single Transaction Manager");
            return;
        }

        // Execute compensation
        log.info("Compensating tasks");
        singleTransactionManager.compensate();
        log.info("Compensation completed.");
    }

    /**
     * This method cleans data associated with a given transaction. It removes a single transaction
     * manager from the list.
     *
     * @param txId is the ID of the transaction which should be finished.
     */
    public void closeTransaction(String txId)
    {
        log.info("closeTransaction()");

        // Remove the single transaction manager from the list
        SingleTransactionManager stm = singleTransactionManagers.remove(txId);
        if (stm != null)
        {
            log.info("Transaction has been removed from the list");
        }
        else
        {
            log.info("Transaction has been already removed from the list");
        }
    }

    /**
     * This method completes the invocation of a service within a scope of a transaction. It is responsible
     * for remembering necessary arguments that were passed to this service and a return object (those objects
     * may be used later if compensation action is being invoked).
     *
     * @param taskDesc describes the task.
     * @param arguments is the list of the arguments for this service.
     * @param returnObject is the return object of this service.
     * @throws org.jboss.txbridge.ba.exception.TransactionProcessingException if it was impossible to complete invocation.
     */
    public synchronized void completeInvocation(TaskDescription taskDesc, Object[] arguments, Object returnObject)
            throws TransactionProcessingException
    {
        log.info("completeInvocation()");
        // Get reference to the single transaction manager
        SingleTransactionManager stm = singleTransactionManagers.get(taskDesc.getTxId());
        if (stm == null)
        {
            return;
        }

        // TODO: Reconsider this
        // Complete the invocation
        try
        {
            stm.completeInvocation(taskDesc,arguments,returnObject);
        }
        catch (Exception e)
        {
            // If there was an error pass the exception
            e.printStackTrace();
            throw new TransactionProcessingException(e);
        }

    }

    /**
     * This method is invoked if the business logic could not complete and the fault
     * is to be propagated to the client. It removes information about this task.
     *
     * @param taskDesc is the description of the task.
     * @throws org.jboss.txbridge.ba.exception.TransactionProcessingException if cleaning the invocation was not successful.
     */
    public synchronized void cleanInvocation(TaskDescription taskDesc)
            throws TransactionProcessingException
    {
        log.info("cleanInvocation()");
        // Get reference to the single transaction manager
        SingleTransactionManager stm = singleTransactionManagers.get(taskDesc.getTxId());
        if (stm == null)
        {
            return;
        }

        // TODO: Reconsider this
        // Clean the invocation
        try
        {
            stm.cleanInvocation(taskDesc);
        }
        catch (Exception e)
        {
            // If there was an error pass it
            e.printStackTrace();
            throw new TransactionProcessingException(e);
        }
    }

}