/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.gatein.pc.portlet.state.consumer;

import org.gatein.common.NotYetImplemented;
import org.gatein.pc.api.InvalidPortletIdException;
import org.gatein.pc.api.NoSuchPortletException;
import org.gatein.pc.api.Portlet;
import org.gatein.pc.api.PortletContext;
import org.gatein.pc.api.PortletInvokerException;
import org.gatein.pc.api.PortletStateType;
import org.gatein.pc.api.StateEvent;
import org.gatein.pc.api.StatefulPortletContext;
import org.gatein.pc.api.invocation.PortletInvocation;
import org.gatein.pc.api.invocation.response.PortletInvocationResponse;
import org.gatein.pc.api.spi.InstanceContext;
import org.gatein.pc.api.state.AccessMode;
import org.gatein.pc.api.state.DestroyCloneFailure;
import org.gatein.pc.api.state.PropertyChange;
import org.gatein.pc.api.state.PropertyMap;
import org.gatein.pc.portlet.PortletInvokerInterceptor;
import org.gatein.pc.portlet.state.InvalidStateIdException;
import org.gatein.pc.portlet.state.NoSuchStateException;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class ConsumerPortletInvoker extends PortletInvokerInterceptor
{

   /** . */
   private ConsumerPersistenceManager persistenceManager;

   public PortletContext unwrapCCP(String wrappedCCP) throws InvalidPortletIdException
   {
      if (wrappedCCP == null)
      {
         throw new IllegalArgumentException();
      }
      if (!wrappedCCP.startsWith(PortletContext.CONSUMER_CLONE_ID_PREFIX))
      {
         throw new InvalidPortletIdException(wrappedCCP);
      }
      return PortletContext.createPortletContext(wrappedCCP.substring(PortletContext.CONSUMER_CLONE_ID_PREFIX.length()));
   }

   public PortletContext unwrapPOP(String wrappedPOPId) throws InvalidPortletIdException
   {
      if (wrappedPOPId == null)
      {
         throw new IllegalArgumentException();
      }
      return PortletContext.createPortletContext(wrappedPOPId);
   }

   public String wrapCCP(PortletContext ccpCtx) throws InvalidPortletIdException
   {
      if (ccpCtx == null)
      {
         throw new IllegalArgumentException();
      }
      return PortletContext.CONSUMER_CLONE_ID_PREFIX + ccpCtx.getId();
   }

   public String wrapPOP(PortletContext popCtx) throws InvalidPortletIdException
   {
      if (popCtx == null)
      {
         throw new IllegalArgumentException();
      }
      if (popCtx.getId().startsWith(PortletContext.CONSUMER_CLONE_ID_PREFIX))
      {
         throw new IllegalArgumentException("Must not start with " + PortletContext.CONSUMER_CLONE_ID_PREFIX);
      }
      return popCtx.getId();
   }

   public void setPersistenceManager(ConsumerPersistenceManager persistenceManager)
   {
      this.persistenceManager = persistenceManager;
   }


   public Set<Portlet> getPortlets() throws PortletInvokerException
   {
      // We don't need proxies here because we return the list of offered portlets
      return super.getPortlets();
   }

   public Portlet getPortlet(PortletContext portletContext) throws IllegalArgumentException, PortletInvokerException
   {
      return getConsumerContext(portletContext).getPortlet();
   }

   public PortletInvocationResponse invoke(PortletInvocation invocation) throws IllegalArgumentException, PortletInvokerException
   {
      PortletContext portletContext = invocation.getTarget();
      if (portletContext == null)
      {
         throw new IllegalArgumentException();
      }

      //
      ConsumerContext consumerContext = getConsumerContext(portletContext);

      //
      InstanceContext cictx = invocation.getInstanceContext();
      StatefulInstanceContextImpl pictx = new StatefulInstanceContextImpl(cictx);

      try
      {
         invocation.setTarget(consumerContext.producerPortletContext);
         invocation.setInstanceContext(pictx);

         //
         PortletInvocationResponse response = super.invoke(invocation);

         //
         PortletContext clonedContext = pictx.clonedContext;
         if (clonedContext != null)
         {
            if (clonedContext instanceof StatefulPortletContext)
            {
               StatefulPortletContext statefulClonedContext = (StatefulPortletContext)clonedContext;
               Serializable state = statefulClonedContext.getState();
               PortletStateType stateType = statefulClonedContext.getType();

               // Save the clone state
               ConsumerState consumerState = new ConsumerState<Serializable>(clonedContext.getId(), stateType, state);
               String stateId = persistenceManager.createState(consumerState);
               String clonedId = PortletContext.CONSUMER_CLONE_ID_PREFIX + stateId;
               StateEvent event = new StateEvent(PortletContext.createPortletContext(clonedId), StateEvent.Type.PORTLET_CLONED_EVENT);
               cictx.onStateEvent(event);
            }
            else
            {
               StateEvent event = new StateEvent(PortletContext.createPortletContext(clonedContext.getId()), StateEvent.Type.PORTLET_CLONED_EVENT);
               cictx.onStateEvent(event);
            }
         }
         else
         {
            PortletContext modifiedContext = pictx.modifiedContext;
            if (modifiedContext != null)
            {
               // update state if needed
               if (modifiedContext instanceof StatefulPortletContext)
               {
                  StatefulPortletContext statefulClonedContext = (StatefulPortletContext)modifiedContext;
                  Serializable state = statefulClonedContext.getState();
                  PortletStateType stateType = statefulClonedContext.getType();
                  try
                  {
                     ConsumerState consumerState = new ConsumerState<Serializable>(modifiedContext.getId(), stateType, state);
                     persistenceManager.updateState(consumerContext.stateId, consumerState);
                  }
                  catch (NoSuchStateException e)
                  {
                     e.printStackTrace();
                  }
                  catch (InvalidStateIdException e)
                  {
                     e.printStackTrace();
                  }
               }
            }
         }

         //
         return response;
      }
      finally
      {
         invocation.setTarget(portletContext);
         invocation.setInstanceContext(cictx);
      }
   }

   public PortletContext createClone(PortletStateType stateType, PortletContext portletContext) throws IllegalArgumentException, PortletInvokerException, UnsupportedOperationException
   {
      ConsumerContext consumerContext = getConsumerContext(portletContext);

      //
      PortletContext clonedContext = super.createClone(stateType, consumerContext.producerPortletContext);

      if (clonedContext instanceof StatefulPortletContext)
      {
         StatefulPortletContext statefulClonedContext = (StatefulPortletContext)clonedContext;
         ConsumerState consumerState = new ConsumerState<Serializable>(clonedContext.getId(), statefulClonedContext.getType(), statefulClonedContext.getState());
         String id = persistenceManager.createState(consumerState);
         return PortletContext.createPortletContext(PortletContext.CONSUMER_CLONE_ID_PREFIX + id);
      }
      else
      {
         return clonedContext;
      }
   }

   public PortletContext importPortlet(PortletStateType stateType,
                                       PortletContext portletContext) throws PortletInvokerException, IllegalArgumentException
   {
      ConsumerContext consumerContext = getConsumerContext(portletContext);

      PortletContext importContext = super.importPortlet(stateType, consumerContext.producerPortletContext);

      if (importContext instanceof StatefulPortletContext)
      {
         StatefulPortletContext statefulimportContext = (StatefulPortletContext)importContext;
         ConsumerState consumerState = new ConsumerState<Serializable>(importContext.getId(), statefulimportContext.getType(), statefulimportContext.getState());
         String id = persistenceManager.createState(consumerState);
         return PortletContext.createPortletContext(PortletContext.CONSUMER_CLONE_ID_PREFIX + id);
      }
      else
      {
         return importContext;
      }

   }

   public PortletContext exportPortlet(PortletStateType stateType,
                                       PortletContext portletContext) throws PortletInvokerException, IllegalArgumentException
   {
      ConsumerContext consumerContext = getConsumerContext(portletContext);

      //
      return super.exportPortlet(stateType, consumerContext.producerPortletContext);
   }

   public List<DestroyCloneFailure> destroyClones(List<PortletContext> portletContexts) throws IllegalArgumentException, PortletInvokerException, UnsupportedOperationException
   {
      if (portletContexts == null)
      {
         throw new IllegalArgumentException();
      }
      portletContexts = new ArrayList<PortletContext>(portletContexts);
      for (int i = 0; i < portletContexts.size(); i++)
      {
         PortletContext portletContext = portletContexts.get(i);
         ConsumerContext consumerContext = getConsumerContext(portletContext);
         portletContexts.set(i, consumerContext.producerPortletContext);
         if (consumerContext.stateId != null)
         {
            try
            {
               persistenceManager.destroyState(consumerContext.stateId);
            }
            catch (NoSuchStateException e)
            {
               //
            }
            catch (InvalidStateIdException e)
            {
               //
            }
         }
      }

      //
      List<DestroyCloneFailure> failures = super.destroyClones(portletContexts);

      // Probably should wrap the portlet context here ????
//      for (Iterator i = failures.iterator(); i.hasNext();)
//      {
//         DestroyCloneFailure failure = (DestroyCloneFailure)i.next();
//         //
//      }

      //
      return failures;
   }

   public PropertyMap getProperties(PortletContext portletContext, Set<String> keys) throws IllegalArgumentException, PortletInvokerException, UnsupportedOperationException
   {
      ConsumerContext consumerContext = getConsumerContext(portletContext);
      return super.getProperties(consumerContext.producerPortletContext, keys);
   }

   public PropertyMap getProperties(PortletContext portletContext) throws IllegalArgumentException, PortletInvokerException, UnsupportedOperationException
   {
      ConsumerContext consumerContext = getConsumerContext(portletContext);
      return super.getProperties(consumerContext.producerPortletContext);
   }

   public PortletContext setProperties(PortletContext portletContext, PropertyChange[] changes) throws IllegalArgumentException, PortletInvokerException, UnsupportedOperationException
   {
      ConsumerContext consumerContext = getConsumerContext(portletContext);

      //
      PortletContext updatedPortletContext = super.setProperties(consumerContext.producerPortletContext, changes);

      if (updatedPortletContext instanceof StatefulPortletContext)
      {
         StatefulPortletContext statefulUpdatedPortletContext = (StatefulPortletContext)updatedPortletContext;
         Serializable state = statefulUpdatedPortletContext.getState();
         PortletStateType stateType = statefulUpdatedPortletContext.getType();

         //
         if (consumerContext.stateId == null)
         {
            throw new NotYetImplemented();
         }

         //
         ConsumerState consumerState = new ConsumerState<Serializable>(updatedPortletContext.getId(), stateType, state);
         try
         {
            persistenceManager.updateState(consumerContext.stateId, consumerState);
         }
         catch (NoSuchStateException e)
         {
            // What to do ?
         }
         catch (InvalidStateIdException e)
         {
            // What to do ?
         }
      }
      else
      {
         if (consumerContext.stateId != null)
         {
            throw new NotYetImplemented();
         }
      }

      // As the point is to make it constant we can simply return it
      return portletContext;
   }

   private class StatefulInstanceContextImpl implements InstanceContext
   {

      /** . */
      private InstanceContext cictx;

      /** . */
      private PortletContext clonedContext;

      /** . */
      private PortletContext modifiedContext;

      public StatefulInstanceContextImpl(InstanceContext cictx)
      {
         this.cictx = cictx;
      }

      public String getId()
      {
         return cictx.getId();
      }

      public AccessMode getAccessMode()
      {
         return cictx.getAccessMode();
      }

      public void onStateEvent(StateEvent event)
      {
         switch (event.getType())
         {
            case PORTLET_CLONED_EVENT:
               clonedContext = event.getPortletContext();
               break;
            case PORTLET_MODIFIED_EVENT:
               modifiedContext = event.getPortletContext();
               break;
         }
      }

      public PortletStateType<?> getStateType()
      {
         return persistenceManager.getStateType();
      }
   }

   private ConsumerContext getConsumerContext(PortletContext portletContext) throws IllegalArgumentException, InvalidPortletIdException
   {
      if (portletContext == null)
      {
         throw new IllegalArgumentException();
      }

      //
      String portletId = portletContext.getId();

      //
      if (portletId.startsWith(PortletContext.CONSUMER_CLONE_ID_PREFIX))
      {
         String stateId = portletId.substring(PortletContext.CONSUMER_CLONE_ID_PREFIX.length());
         try
         {
            ConsumerStateContext stateCtx = persistenceManager.loadState(stateId);
            StatefulPortletContext<Serializable> blah = StatefulPortletContext.create(
               stateCtx.getPortletId(),
               stateCtx.getStateType(),
               stateCtx.getState());
            return new ConsumerContext(portletContext, blah, stateId);
         }
         catch (NoSuchStateException e)
         {
            throw new NoSuchPortletException(portletId);
         }
         catch (InvalidStateIdException e)
         {
            throw new InvalidPortletIdException(portletId);
         }
      }
      else
      {
         return new ConsumerContext(portletContext, portletContext, null);
      }
   }

   /** A context which defines how the consumer see the producer portlet. */
   private class ConsumerContext
   {

      /** . */
      private final PortletContext consumerPortletContext;

      /** The target portlet context. */
      private final PortletContext producerPortletContext;

      /** The id in the store. */
      private final String stateId;

      /** The lazy created portlet. */
      private Portlet portlet;

      public ConsumerContext(
         PortletContext consumerPortletContext,
         PortletContext producerPortletContext,
         String stateId)
      {
         this.consumerPortletContext = consumerPortletContext;
         this.producerPortletContext = producerPortletContext;
         this.stateId = stateId;
      }

      public Portlet getPortlet() throws PortletInvokerException
      {
         if (portlet == null)
         {
            Portlet producerPortlet = ConsumerPortletInvoker.super.getPortlet(producerPortletContext);

            //
            if (stateId == null)
            {
               portlet = producerPortlet;
            }
            else
            {
               portlet = new ConsumerPortlet(consumerPortletContext, producerPortlet);
            }
         }

         //
         return portlet;
      }
   }
}
