/**
 * 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.impl.jsr168.api;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.gatein.common.NotYetImplemented;
import org.gatein.common.util.Tools;
import org.gatein.pc.api.ParametersStateString;
import org.gatein.pc.api.invocation.PortletInvocation;
import org.gatein.pc.api.invocation.response.HTTPRedirectionResponse;
import org.gatein.pc.api.invocation.response.PortletInvocationResponse;
import org.gatein.pc.api.invocation.response.UpdateNavigationalStateResponse;
import org.gatein.pc.portlet.impl.info.ContainerEventInfo;
import org.gatein.pc.portlet.impl.info.ContainerPortletApplicationInfo;
import org.gatein.pc.portlet.impl.info.ContainerTypeInfo;
import org.gatein.pc.portlet.impl.jsr168.PortletApplicationImpl;
import org.gatein.pc.portlet.impl.jsr168.PortletParameterMap;

import javax.portlet.PortletMode;
import javax.portlet.PortletModeException;
import javax.portlet.StateAwareResponse;
import javax.portlet.WindowState;
import javax.portlet.WindowStateException;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.namespace.QName;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

public abstract class StateAwareResponseImpl extends PortletResponseImpl implements StateAwareResponse
{

   /** . */
   private static final Log                           log = ExoLogger.getLogger(ActionResponseImpl.class);

   /** . */
   protected WantUpdate wantUpdate;

   /** . */
   protected WantRedirect wantRedirect;

   /** . */
   private List<UpdateNavigationalStateResponse.Event> events;

   public StateAwareResponseImpl(PortletInvocation invocation, PortletRequestImpl preq)
   {
      super(invocation, preq);
   }

   public PortletInvocationResponse getResponse()
   {
      Decision decision = wantRedirect;

      //
      if (wantRedirect == null)
      {
         decision = wantUpdate;
      }

      //
      if (decision == null)
      {
         decision = new WantUpdate();
      }

      //
      return decision.getResponse();
   }

   protected WantUpdate requireUpdate(String errorMsg)
   {
      if (wantRedirect != null)
      {
         throw new IllegalStateException(errorMsg);
      }
      else if (wantUpdate == null)
      {
         wantUpdate = new WantUpdate();
      }
      return wantUpdate;
   }

   protected void checkRedirect(String errorMsg)
   {
      if (wantUpdate != null)
      {
         throw new IllegalStateException(errorMsg);
      }
   }

   protected WantRedirect requireRedirect()
   {
      if (wantUpdate != null)
      {
         throw new IllegalStateException();
      }
      else if (wantRedirect == null)
      {
         wantRedirect = new WantRedirect();
      }
      return wantRedirect;
   }

   protected WantUpdate wantUpdate()
   {
      if (wantUpdate == null)
      {
         wantUpdate = new WantUpdate();
      }
      return wantUpdate;
   }

   public void setWindowState(WindowState windowState) throws WindowStateException
   {
      WantUpdate update = requireUpdate("Window state cannot be set after redirect");

      //
      if (!preq.isWindowStateAllowed(windowState))
      {
         throw new WindowStateException("Not supported", windowState);
      }

      //
      update.windowState = org.gatein.pc.api.WindowState.create(windowState.toString());
   }

   public void setPortletMode(PortletMode portletMode) throws PortletModeException
   {
      WantUpdate update = requireUpdate("Portlet mode cannot be set after redirect");

      //
      if (portletMode == null)
      {
         // The spec does not define that case
         // we just issue a warn
         log.warn("Set null portlet mode");
      }
      else
      {
         if (!preq.isPortletModeAllowed(portletMode))
         {
            throw new PortletModeException("Not supported", portletMode);
         }

         //
         update.mode = org.gatein.pc.api.Mode.create(portletMode.toString());
      }
   }

   public void setRenderParameters(Map<String, String[]> map)
   {
      WantUpdate update = requireUpdate("setRenderParameters cannot be called after redirect");

      //
      update.navigationalState.setMap(map);
   }

   public void setRenderParameter(String name, String value)
   {
      WantUpdate update = requireUpdate("setRenderParameter cannot be called after redirect");

      //
      update.navigationalState.setParameterValue(name, value);
   }

   public void setRenderParameter(String name, String[] values)
   {
      WantUpdate update = requireUpdate("setRenderParameter cannot be called after redirect");

      //
      update.navigationalState.setParameterValues(name, values);
   }

   public Map<String, String[]> getRenderParameterMap()
   {
      WantUpdate update = wantUpdate();

      //
      return update.navigationalState.getMap();
   }

   public void removePublicRenderParameter(String name)
   {
      WantUpdate update = wantUpdate();

      //
      update.navigationalState.removePublicParameterValue(name);
   }

   private static final Set<? extends Class<? extends Serializable>> acceptedFinalClasses = Tools.toSet(
      Boolean.class, Integer.class, Byte.class, Long.class, Float.class, Double.class, String.class, URI.class, UUID.class
   );

   /**
    * Returns true if the class requires a check of its JAXB annotation.
    *
    * @param clazz the class to check
    * @return true if the class requires the JAXB annotation
    */
   private boolean requiresJAXBAnnotation(Class<? extends Serializable> clazz)
   {
      // Since they are final a equals should be enough
      if (acceptedFinalClasses.contains(clazz))
      {
         return false;
      }

      //
      if (clazz.getName().startsWith("java"))
      {
         if (BigInteger.class.isAssignableFrom(clazz))
         {
            return false;
         }
         else if (BigDecimal.class.isAssignableFrom(clazz))
         {
            return false;
         }
         else if (Calendar.class.isAssignableFrom(clazz))
         {
            return false;
         }
         else if (Date.class.isAssignableFrom(clazz))
         {
            return false;
         }
         else if (QName.class.isAssignableFrom(clazz))
         {
            return false;
         }
      }

      //
      return true;
   }

   public void setEvent(QName name, Serializable value)
   {
      if (name == null)
      {
         throw new IllegalArgumentException("The portlet must specify a name when producing an event");
      }

      //
      if (value != null)
      {
         Class<? extends Serializable> valueType = value.getClass();

         // Check jaxb annotation
         if (requiresJAXBAnnotation(valueType))
         {
            boolean b = valueType.isAnnotationPresent(XmlRootElement.class);
            if (!b)
            {
               throw new IllegalArgumentException("The provided event value type " + value.getClass().getName() +
                  " does not have a valid jaxb annotation");
            }
         }

         // Check type
         ContainerEventInfo eventInfo = preq.container.getInfo().getEventing().getProducedEvents().get(name);

         // If we have event info it means we need to check the type validity otherwise it is a non declared event
         // and it is up to the controller to manage the payload.
         if (eventInfo != null)
         {
            ContainerTypeInfo typeInfo = eventInfo.getType();
            if (typeInfo != null)
            {
               Class expectedType = typeInfo.getType();

               //
               if (!expectedType.isInstance(value))
               {
                  throw new IllegalArgumentException("The provided event value type " + valueType.getName() +
                     " does not match the declared event type " + expectedType.getName());
               }
            }
         }
      }

      //
      queueEvent(name, value);
   }

   public void setEvent(String localName, Serializable value)
   {
      if (localName == null)
      {
         throw new IllegalArgumentException("The portlet must specify a local name when producing an event");
      }

      //
      ContainerPortletApplicationInfo info = ((PortletApplicationImpl)preq.container.getPortletApplication()).getInfo();
      QName name = new QName(info.getDefaultNamespace(), localName);
      setEvent(name, value);
   }

   public PortletMode getPortletMode()
   {
      throw new NotYetImplemented();
   }

   public WindowState getWindowState()
   {
      throw new NotYetImplemented();
   }

   protected final void queueEvent(QName name, Serializable value)
   {
      if (events == null)
      {
         events = new LinkedList<UpdateNavigationalStateResponse.Event>();
      }
      events.add(new UpdateNavigationalStateResponse.Event(name, value));
   }

   private abstract class Decision
   {

      protected abstract PortletInvocationResponse getResponse();

   }

   protected class WantUpdate extends Decision
   {

      /** The navigational state returned. */
      protected PortletParameterMap navigationalState = new PortletParameterMap(preq.navigationInfo);

      /** The new window state requested. */
      protected org.gatein.pc.api.WindowState windowState = org.gatein.pc.api.WindowState.create(preq.getWindowState().toString());

      /** The new mode requested. */
      protected org.gatein.pc.api.Mode mode = org.gatein.pc.api.Mode.create(preq.getPortletMode().toString());

      protected PortletInvocationResponse getResponse()
      {
         UpdateNavigationalStateResponse response = new UpdateNavigationalStateResponse();

         //
         response.setProperties(getProperties(false));
         response.setAttributes(preq.attributes.getAttributeMap());

         //
         response.setMode(mode);
         response.setWindowState(windowState);
         response.setPublicNavigationalStateUpdates(navigationalState.getPublicMapSnapshot());
         response.setNavigationalState(ParametersStateString.create(navigationalState.getPrivateMapSnapshot()));

         //
         if (events != null)
         {
            for (UpdateNavigationalStateResponse.Event event : events)
            {
               response.queueEvent(event);
            }
         }

         //
         return response;
      }
   }

   protected class WantRedirect extends Decision
   {

      /** . */
      protected String location;

      protected PortletInvocationResponse getResponse()
      {
         return new HTTPRedirectionResponse(location);
      }
   }
}
