/*
 * 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 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;

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;

/**
 * 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);
        resetOrientation(reqCtx, 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) {
    }

    /**
     * @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);
        }
    }
}
