/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.portal.application.localization;

import org.exoplatform.container.ExoContainer;
import org.exoplatform.portal.Constants;
import org.exoplatform.portal.application.PortalRequestContext;
import org.exoplatform.portal.application.UserProfileLifecycle;
import org.exoplatform.portal.config.DataStorage;
import org.exoplatform.portal.config.model.PortalConfig;
import org.exoplatform.portal.mop.SiteType;
import org.exoplatform.portal.webui.workspace.UIPortalApplication;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.UserProfile;
import org.exoplatform.services.resources.LocaleConfig;
import org.exoplatform.services.resources.LocaleConfigService;
import org.exoplatform.services.resources.LocaleContextInfo;
import org.exoplatform.services.resources.LocalePolicy;
import org.exoplatform.web.application.Application;
import org.exoplatform.web.application.ApplicationRequestPhaseLifecycle;
import org.exoplatform.web.application.Phase;
import org.exoplatform.web.application.RequestFailure;
import org.exoplatform.webui.application.WebuiRequestContext;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * This class takes care of loading / initializing / saving the current Locale.
 * Current Locale is used to create properly localized response to current request.
 *
 * At the beginning of request {@link LocalePolicy} is used to determine the
 * initial Locale to be used for processing the request.
 *
 * This Locale is then set on current {@link org.exoplatform.portal.application.PortalRequestContext} (it's presumed that current
 * {@link org.exoplatform.web.application.RequestContext} is of type PortalRequestContext) by calling
 * {@link org.exoplatform.portal.application.PortalRequestContext#setLocale}.
 *
 * During request processing {@link org.exoplatform.portal.application.PortalRequestContext#getLocale} is the ultimate reference consulted by any
 * rendering code that needs to know about current Locale.
 *
 * When this Locale is changed during action processing, the new Locale choice is saved
 * into user's profile or into browser's cookie in order to be used by future requests.
 *
 * This Lifecycle depends on UserProfileLifecycle being registered before this one, as it relies on it
 * for loading the user profile. See WEB-INF/webui-configuration.xml in web/portal module.
 *
 * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
 */
public class LocalizationLifecycle implements ApplicationRequestPhaseLifecycle<WebuiRequestContext>
{
   private static final String LOCALE_COOKIE = "LOCALE";

   private static final String LOCALE_SESSION_ATTR = "org.gatein.LOCALE";

   private static final String PREV_LOCALE_SESSION_ATTR = "org.gatein.LAST_LOCALE";

   private static final ThreadLocal<Locale> calculatedLocale = new ThreadLocal<Locale>();

   private static Log log = ExoLogger.getLogger("portal:LocalizationLifecycle");

   /**
    * @see org.exoplatform.web.application.ApplicationLifecycle#onInit
    */
   public void onInit(Application app) throws Exception
   {
   }

   /**
    * Initialize Locale to be used for the processing of current request
    *  
    * @see org.exoplatform.web.application.ApplicationLifecycle#onStartRequest
    */
   public void onStartRequest(Application app, WebuiRequestContext context) throws Exception
   {
      if (context instanceof PortalRequestContext == false)
         throw new IllegalArgumentException("Expected PortalRequestContext, but got: " + context);

      PortalRequestContext reqCtx = (PortalRequestContext) context;
      ExoContainer container = app.getApplicationServiceContainer();

      LocaleConfigService localeConfigService = (LocaleConfigService)
            container.getComponentInstanceOfType(LocaleConfigService.class);
      LocalePolicy localePolicy = (LocalePolicy) container.getComponentInstanceOfType(LocalePolicy.class);

      LocaleContextInfo localeCtx = new LocaleContextInfo();

      Set<Locale> supportedLocales = new HashSet<Locale>();
      for (LocaleConfig lc: localeConfigService.getLocalConfigs())
      {
         supportedLocales.add(lc.getLocale());
      }
      localeCtx.setSupportedLocales(supportedLocales);

      HttpServletRequest request = HttpServletRequest.class.cast(context.getRequest());
      localeCtx.setBrowserLocales(Collections.list(request.getLocales()));
      localeCtx.setCookieLocales(getCookieLocales(request));
      localeCtx.setSessionLocale(getSessionLocale(request));
      localeCtx.setUserProfileLocale(getUserProfileLocale(reqCtx));
      localeCtx.setRemoteUser(reqCtx.getRemoteUser());

      DataStorage dataStorage = (DataStorage)container.getComponentInstanceOfType(DataStorage.class);
      PortalConfig pConfig = null;
      try
      {
         pConfig = dataStorage.getPortalConfig(SiteType.PORTAL.getName(), reqCtx.getPortalOwner());
         if (pConfig == null)
            log.warn("No UserPortalConfig available! Portal locale set to 'en'");
      }
      catch(Exception ignored)
      {
         if (log.isDebugEnabled())
            log.debug("IGNORED: Failed to load UserPortalConfig: ", ignored);
      }

      String portalLocaleName = "en";
      if (pConfig != null)
         portalLocaleName = pConfig.getLocale();

      Locale portalLocale = LocaleContextInfo.getLocale(portalLocaleName);
      localeCtx.setPortalLocale(portalLocale);

      localeCtx.setRequestLocale(reqCtx.getRequestLocale());

      Locale locale = localePolicy.determineLocale(localeCtx);
      boolean supported = supportedLocales.contains(locale);

      if (!supported && !"".equals(locale.getCountry()))
      {
         locale = new Locale(locale.getLanguage());
         supported = supportedLocales.contains(locale);
      }
      if (!supported)
      {
         if (log.isWarnEnabled())
            log.warn("Unsupported locale returned by LocalePolicy: " + localePolicy + ". Falling back to 'en'.");
         locale = Locale.ENGLISH;
      }
      reqCtx.setLocale(locale);
      calculatedLocale.set(locale);
   }

   /**
    * @see org.exoplatform.web.application.ApplicationRequestPhaseLifecycle#onStartRequestPhase
    */
   public void onStartRequestPhase(Application app, WebuiRequestContext context, Phase phase)
   {
   }

   /**
    * Save any locale change - to cookie for anonymous users, to profile for logged-in users
    *
    * @see org.exoplatform.web.application.ApplicationRequestPhaseLifecycle#onEndRequestPhase
    */
   public void onEndRequestPhase(Application app, WebuiRequestContext context, Phase phase)
   {
      if (phase == Phase.ACTION)
      {
         // if onStartRequest survived the cast, this one should as well - no check necessary
         PortalRequestContext reqCtx = (PortalRequestContext) context;
         Locale loc = reqCtx.getLocale();
         Locale remembered = calculatedLocale.get();
         calculatedLocale.remove();

         boolean refreshNeeded = false;

         // if locale changed since previous request
         Locale sessLocale = getPreviousLocale(reqCtx.getRequest());
         if (loc != null && sessLocale != null && !loc.equals(sessLocale))
         {
            refreshNeeded = true;
         }
         // if locale changed during this request's processing
         if (loc != null && (remembered == null || !loc.equals(remembered)))
         {
            refreshNeeded = true;
            saveLocale(reqCtx, loc);
         }

         if (refreshNeeded)
         {
            resetOrientation(reqCtx, loc);
         }

         savePreviousLocale(reqCtx, loc);
      }
   }

   /**
    * @see org.exoplatform.web.application.ApplicationLifecycle#onEndRequest
    */
   public void onEndRequest(Application app, WebuiRequestContext context) throws Exception
   {
   }

   /**
    * @see org.exoplatform.web.application.ApplicationLifecycle#onFailRequest
    */
   public void onFailRequest(Application app, WebuiRequestContext context, RequestFailure failureType) throws Exception
   {
   }

   /**
    * @see org.exoplatform.web.application.ApplicationLifecycle#onDestroy
    */
   public void onDestroy(Application app) throws Exception
   {
   }

   /**
    * Use {@link UserProfile} already loaded by {@link org.exoplatform.portal.application.UserProfileLifecycle} or load one ourselves.
    * @param context current PortalRequestContext
    * @return Locale from user's profile or null
    */
   private Locale getUserProfileLocale(PortalRequestContext context)
   {
      String lang = null;

      UserProfile userProfile = getLoadedProfile(context);
      lang = userProfile == null ? null : userProfile.getUserInfoMap().get(Constants.USER_LANGUAGE);
      return (lang != null) ? LocaleContextInfo.getLocale(lang) : null;
   }

   private UserProfile loadUserProfile(ExoContainer container, PortalRequestContext context)
   {
      UserProfile userProfile = null;
      OrganizationService svc = (OrganizationService)
            container.getComponentInstanceOfType(OrganizationService.class);

      String user = context.getRemoteUser();
      if (user != null)
      {
         try
         {
            userProfile = svc.getUserProfileHandler().findUserProfileByName(user);
         }
         catch (Exception ignored)
         {
            log.error("IGNORED: Failed to load UserProfile for username: " + user, ignored);
         }

         if (userProfile == null && log.isWarnEnabled())
            log.warn("Could not load user profile for " + user + ". Using default portal locale.");
      }
      return userProfile;
   }

   private UserProfile getLoadedProfile(PortalRequestContext context)
   {
      return (UserProfile) context.getAttribute(UserProfileLifecycle.USER_PROFILE_ATTRIBUTE_NAME);
   }

   public static List<Locale> getCookieLocales(HttpServletRequest request)
   {
      Cookie [] cookies = request.getCookies();
      if (cookies != null)
      {
         for (Cookie cookie: cookies)
         {
            if (LOCALE_COOKIE.equals(cookie.getName()))
            {
               List<Locale> locales = new ArrayList<Locale>();
               locales.add(LocaleContextInfo.getLocale(cookie.getValue()));
               return locales;
            }
         }
      }
      return Collections.emptyList();
   }

   public static Locale getSessionLocale(HttpServletRequest request)
   {
      return getLocaleFromSession(request, LOCALE_SESSION_ATTR);
   }

   public static Locale getPreviousLocale(HttpServletRequest request)
   {
      return getLocaleFromSession(request, PREV_LOCALE_SESSION_ATTR);
   }

   private static Locale getLocaleFromSession(HttpServletRequest request, String attrName)
   {
      String lang = null;
      HttpSession session = request.getSession(false);
      if (session != null)
         lang = (String) session.getAttribute(attrName);
      return (lang != null) ? LocaleContextInfo.getLocale(lang) : null;
   }

   private void saveLocale(PortalRequestContext context, Locale loc)
   {
      String user = context.getRemoteUser();
      if (user != null)
      {
         saveLocaleToUserProfile(context, loc, user);
      }
      else
      {
         saveLocaleToCookie(context, loc);
      }

      saveSessionLocale(context, loc);
   }

   private void resetOrientation(PortalRequestContext context, Locale loc)
   {
      ExoContainer container = context.getApplication().getApplicationServiceContainer();
      LocaleConfigService localeConfigService = (LocaleConfigService)
            container.getComponentInstanceOfType(LocaleConfigService.class);
      LocaleConfig localeConfig = localeConfigService.getLocaleConfig(LocaleContextInfo.getLocaleAsString(loc));
      if (localeConfig == null)
      {
         if (log.isWarnEnabled())
            log.warn("Locale changed to unsupported Locale during request processing: " + loc);
         return;
      }
      // we presume PortalRequestContext, and UIPortalApplication
      ((UIPortalApplication) context.getUIApplication()).setOrientation(localeConfig.getOrientation());
   }

   private void saveSessionLocale(PortalRequestContext context, Locale loc)
   {
      saveLocaleToSession(context, LOCALE_SESSION_ATTR, loc);
   }

   private void savePreviousLocale(PortalRequestContext context, Locale loc)
   {
      saveLocaleToSession(context, PREV_LOCALE_SESSION_ATTR, loc);
   }

   private void saveLocaleToSession(PortalRequestContext context, String attrName, Locale loc)
   {
      HttpServletRequest res = context.getRequest();
      HttpSession session = res.getSession(false);
      if (session != null)
         session.setAttribute(attrName, LocaleContextInfo.getLocaleAsString(loc));
   }

   private void saveLocaleToCookie(PortalRequestContext context, Locale loc)
   {
      HttpServletResponse res = context.getResponse();
      Cookie cookie = new Cookie(LOCALE_COOKIE, LocaleContextInfo.getLocaleAsString(loc));
      cookie.setMaxAge(Integer.MAX_VALUE);
      cookie.setPath("/");
      res.addCookie(cookie);
   }

   private void saveLocaleToUserProfile(PortalRequestContext context, Locale loc, String user)
   {
      ExoContainer container = context.getApplication().getApplicationServiceContainer();
      OrganizationService svc = (OrganizationService)
            container.getComponentInstanceOfType(OrganizationService.class);

      // Don't rely on UserProfileLifecycle loaded UserProfile when doing
      // an update to avoid a potential overwrite of other changes
      UserProfile userProfile = loadUserProfile(container, context);
      if (userProfile != null)
      {
         userProfile.getUserInfoMap().put(Constants.USER_LANGUAGE, LocaleContextInfo.getLocaleAsString(loc));
         try
         {
            svc.getUserProfileHandler().saveUserProfile(userProfile, false);
         }
         catch (Exception ignored)
         {
            log.error("IGNORED: Failed to save profile for user: " + user, ignored);
            userProfile = null;
         }
      }

      if (userProfile == null)
      {
         if (log.isWarnEnabled())
            log.warn("Unable to save locale into profile for user: " + user);
      }
   }
}
