/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.optimistic;

import EDU.oswego.cs.dl.util.concurrent.Latch;
import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArrayList;
import org.jboss.cache.Fqn;
import org.jboss.cache.OptimisticTreeNode;
import org.jboss.cache.TreeCache;
import org.jboss.cache.interceptors.Interceptor;
import org.jboss.cache.interceptors.OptimisticCreateIfNotExistsInterceptor;
import org.jboss.cache.interceptors.TxInterceptor;
import org.jboss.cache.marshall.JBCMethodCall;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jboss.cache.misc.TestingUtil;
import org.jgroups.blocks.MethodCall;

import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 */
public class ConcurrentTransactionTest extends AbstractOptimisticTestCase
{
   private TreeCache cache;
   private Fqn f = Fqn.fromString("/a/b");
   private List exceptions = new CopyOnWriteArrayList();


    public ConcurrentTransactionTest(String name)
    {
        super(name);
    }

    public void setUp()
    {
        try
        {
            //cache = createCache();
           cache = createCacheUnstarted();
           cache.setUseRegionBasedMarshalling(true);
           cache.startService();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    public void tearDown()
    {
        if (cache != null)
        {
            cache.stopService();
            cache = null;
        }
       exceptions.clear();
    }

    public void testConcurrentTransactions() throws Exception
    {
        TransactionManager tm = cache.getTransactionManager();
        cache.put("/a/b/c/d", key, value);

        assertEquals(value, cache.get("/a/b/c/d", key));

        tm.begin();
        Transaction tx = tm.getTransaction();

        cache.put("/a/b/x/y", key, value);
        tm.suspend();

        // a number of random puts in unrelated sub nodes.
        cache.put("/a/b/c/d", key, value+value);
        cache.put("/a/b/c/e", key, value);
        cache.put("/a/b/c/f", key, value);
        cache.put("/a/b/c/g", key, value);

        assertEquals(value+value, cache.get("/a/b/c/d", key));
        assertEquals(value, cache.get("/a/b/c/e", key));
        assertEquals(value, cache.get("/a/b/c/f", key));
        assertEquals(value, cache.get("/a/b/c/g", key));

        tm.resume(tx);
        tx.commit();

        assertEquals(value, cache.get("/a/b/x/y", key));

        OptimisticTreeNode n = (OptimisticTreeNode) cache.get(Fqn.ROOT);
        System.out.println(n.getVersion());
    }

   public void testConcurrentCreationTestWithEmptyCache() throws Exception
   {
      doConcurrentCreationTest(false);
   }

   public void testConcurrentCreationTestWithEmptyCacheActivated() throws Exception
   {
      assertNotNull("Region manager is null!", cache.getRegionManager());
      cache.activateRegion("/parent");
      assertTrue(cache.exists("/parent"));
      doConcurrentCreationTest(false);
   }

   public void testConcurrentCreationTestWithPopulatedCache() throws Exception
   {
      doConcurrentCreationTest(true);
   }

   public void testConcurrentReadAndRemove() throws Exception
   {
      final List exceptions = new LinkedList();
      final Latch readerLatch = new Latch();
      final Latch readerFinishedLatch = new Latch();
      final Fqn fqn = Fqn.fromString("/parent/child");

      cache.put(fqn, "k", "v");

      class Reader extends Thread
      {
         public void run()
         {
            try
            {
               cache.getTransactionManager().begin();
               cache.get(fqn, "k"); // read
               readerFinishedLatch.release();
               readerLatch.acquire(); // wait
               cache.getTransactionManager().commit();
            }
            catch (Exception e)
            {
               e.printStackTrace();
               exceptions.add(e);

            }
         }
      }

      Thread reader = new Reader();

      reader.start();
      readerFinishedLatch.acquire();
      cache.remove(fqn.getParent());
      assertFalse(cache.exists(fqn.getParent()));      
      readerLatch.release();
      reader.join();

      assertTrue("Should not have caught any exceptions!!", exceptions.isEmpty());
   }
   
   public void testConcurrentPutReadAndRemove() throws Exception
   {
      final List exceptions = new LinkedList();
      final Latch readerLatch = new Latch();
      final Latch readerFinishedLatch = new Latch();
      final Fqn fqn = Fqn.fromString("/parent/child");

      cache.put(fqn, "k", "v");

      class Reader extends Thread
      {
         public void run()
         {
            try
            {
               cache.getTransactionManager().begin();
               cache.put(Fqn.ROOT, "x", "y"); // a dummy put to ensure that validation occurs
               cache.get(fqn, "k"); // read
               readerFinishedLatch.release();
               readerLatch.acquire(); // wait
               cache.getTransactionManager().commit();
            }
            catch (Exception e)
            {
               e.printStackTrace();
               exceptions.add(e);

            }
         }
      }

      Thread reader = new Reader();

      reader.start();
      readerFinishedLatch.acquire();
      cache.remove(fqn.getParent());
      assertFalse(cache.exists(fqn.getParent()));
      readerLatch.release();
      reader.join();

      assertTrue("Should not have caught any exceptions!!", exceptions.isEmpty());
   }


   private void doConcurrentCreationTest(boolean prepopulateParent) throws Exception
   {
      if (prepopulateParent) cache.put("/parent/dummy", "k", "v");

      final List exceptions = new LinkedList();
      final Latch latch = new Latch();

      class ConcurrentCreator extends Thread
      {
         private String name;
         public ConcurrentCreator(String name)
         {
            this.name = name;
         }

         public void run()
         {
            try
            {
               cache.getTransactionManager().begin();
               cache.put("/parent/child" + name, "key", "value");
               latch.acquire();
               cache.getTransactionManager().commit();
            }
            catch (Exception e)
            {
               e.printStackTrace();
               exceptions.add(e);
            }
         }
      }

      Thread one = new ConcurrentCreator("one");
      Thread two = new ConcurrentCreator("two");

      one.start();
      two.start();

      latch.release();

      one.join();
      two.join();

      assertTrue("Should not have caught any exceptions!!", exceptions.isEmpty());
   }
   
   public void testConcurrentCreationWithoutTx() throws Exception
     {
        final String slowThreadName = "SLOW";
        Interceptor slowdownInterceptor = new Interceptor()
        {
           public Object invoke(MethodCall c) throws Throwable
           {
              if (Thread.currentThread().getName().equals(slowThreadName))
              {
                 Thread.sleep(1000);
              }
              return super.invoke(c);
           }
        };

        List interceptors = cache.getInterceptors();
        int i=0;
        for (i=0; i<interceptors.size(); i++)
        {
           if (interceptors.get(i) instanceof OptimisticCreateIfNotExistsInterceptor) break;
        }

        Interceptor prev = (Interceptor) interceptors.get(i);
        Interceptor next = (Interceptor) interceptors.get(i+1);

        slowdownInterceptor.setNext(next);
        prev.setNext(slowdownInterceptor);

        cache.setInterceptorChain((Interceptor) interceptors.get(0));

        System.out.println(cache.getInterceptorChain());

        // now create 2 threads to do concurrent puts.
        Putter slow = new Putter(slowThreadName);
        Putter fast = new Putter("FAST");

        // start the slow putter first
        slow.start();
        TestingUtil.sleepThread(200);
        fast.start();

        fast.join();
        slow.join();

        for (Iterator e = exceptions.iterator() ; e.hasNext() ;) ((Exception) e.next()).printStackTrace();
        assertEquals(0, exceptions.size());
     }

     public void testConcurrentDeletionWithoutTx() throws Exception
     {
        final String slowThreadName = "SLOW";
        Interceptor slowdownInterceptor = new Interceptor()
        {
           public Object invoke(MethodCall c) throws Throwable
           {
              if (Thread.currentThread().getName().equals(slowThreadName) &&
                      ((JBCMethodCall) c).getMethodId() == MethodDeclarations.optimisticPrepareMethod_id)
              {
                 Thread.sleep(1000);
              }
              return super.invoke(c);
           }
        };

        List interceptors = cache.getInterceptors();
        int i=0;
        for (i=0; i<interceptors.size(); i++)
        {
           if (interceptors.get(i) instanceof TxInterceptor) break;
        }

        Interceptor prev = (Interceptor) interceptors.get(i);
        Interceptor next = (Interceptor) interceptors.get(i+1);

        slowdownInterceptor.setNext(next);
        prev.setNext(slowdownInterceptor);

        cache.setInterceptorChain((Interceptor) interceptors.get(0));

        System.out.println(cache.getInterceptorChain());

        // now create 2 threads to do concurrent puts.
        Remover slow = new Remover(slowThreadName);
        Remover fast = new Remover("FAST");

        cache.put(f, "hello", "world");

        // start the slow putter first
        slow.start();
        TestingUtil.sleepThread(200);
        fast.start();

        fast.join();
        slow.join();

        for (Iterator e = exceptions.iterator() ; e.hasNext() ;) ((Exception) e.next()).printStackTrace();
        assertEquals(0, exceptions.size());
     }

   public class Putter extends Thread
     {
        public Putter(String name)
        {
           super(name);
        }

        public void run()
        {
           try
           {
              assertFalse(cache.exists(f));
              cache.put(new Fqn(f, getName()), "a", "b");
              assertTrue(cache.exists(f));
           }
           catch (Exception e)
           {
              exceptions.add(e);
           }
        }
     }

   public class Remover extends Thread
   {
      public Remover(String name)
      {
         super(name);
      }

      public void run()
      {
         try
         {
            cache.getTransactionManager().begin();
            cache.remove(f);
            cache.getTransactionManager().commit();
            assertFalse(cache.exists(f));
         }
         catch (Exception e)
         {
            exceptions.add(e);
         }
      }
   }

}
