package org.icefaces.impl.application;

import org.icefaces.bean.AllWindowsClosed;
import org.icefaces.util.EnvUtils;

import javax.faces.application.ResourceHandler;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.PreDestroyCustomScopeEvent;
import javax.faces.event.ScopeContext;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import static org.icefaces.impl.application.WindowScopeManager.*;

class TimeBasedWindowScopeTracker implements WindowScopeTracker {
    private static final Pattern VERIFY_WINDOW_ID = Pattern.compile("\\w*");
    private static final String WindowParameter = "ice.window";
    private static final Logger logger = Logger.getLogger(TimeBasedWindowScopeTracker.class.getName());
    private SharedMapLookupStrategy lookupStrategy;

    public TimeBasedWindowScopeTracker() {
        boolean isLiferay;
        try {
            Object url = FacesContext.getCurrentInstance().getExternalContext().getResource("/WEB-INF/portlet.xml");
            //url not null proves that application is indeed deployed within Liferay (not as webapp deployed along Liferay webapp)
            isLiferay = EnvUtils.isLiferay() && url != null;
        } catch (MalformedURLException e) {
            isLiferay = false;
        }

        try {
            lookupStrategy = isLiferay ? new LiferayOriginalRequestWindowScopeSharing() : new TimeBasedHeuristicWindowScopeSharing();
        } catch (Exception e) {
            lookupStrategy = new TimeBasedHeuristicWindowScopeSharing();
        }
    }

    public ScopeMap lookupScope(FacesContext context) {
        try {
            final ExternalContext externalContext = context.getExternalContext();
            final Map<String, String> parameterMap = externalContext.getRequestParameterMap();
            final ResourceHandler resourceHandler = context.getApplication().getResourceHandler();
            //stop looking up the scope map for resources that do not have 'ice.window' parameter in their URL
            //to avoid creating new window scope maps that will never be used again
            if (externalContext.getRequest() != null && !parameterMap.containsKey(WindowParameter) && resourceHandler.isResourceRequest(context)) {
                return null;
            }

            String id = lookupAssociatedWindowID(context);
            WindowState state = getState(context);
            if (state == null) {
                return null;
            } else {
                Lock synchronizationMonitor = getSynchronizationObject(context);
                //ICE-10174, ICE-10254
                synchronizationMonitor.lock();
                try {
                    ScopeMap map = (ScopeMap) state.windowScopedMaps.get(id);
                    //return the scope map even for requests that arrive after the dispose-window request was received
                    if (map == null) {
                        for (Object scopeMap : state.disposedWindowScopedMaps) {
                            ScopeMap next = (ScopeMap) scopeMap;
                            if (next.id != null && next.id.equals(id)) {
                                return next;
                            }
                        }

                        return null;
                    } else {
                        return map;
                    }
                } finally {
                    synchronizationMonitor.unlock();
                }
            }
        } catch (Exception ex) {
            logger.log(Level.FINE, "Cannot acquire the window corresponding scope map yet.", ex);
            return null;
        }
    }

    private String lookupAssociatedWindowID(FacesContext facesContext) {
        Map requestMap = facesContext.getExternalContext().getRequestMap();
        String id = (String) requestMap.get(WindowScopeManager.class.getName());
        //determine window early on since its needed during component tree creation
        if (id == null) {
            id = setupScope(facesContext);
            requestMap.put(WindowScopeManager.class.getName(), id);
        }
        //save the id to be later use by WindowScopeManager.lookupAssociatedWindowID
        requestMap.put(AssociatedWindowID, id);
        return id;
    }

    public String setupScope(FacesContext context) {
        Lock synchronizationMonitor = getSynchronizationObject(context);
        if (synchronizationMonitor == null) return null;

        ExternalContext externalContext = context.getExternalContext();
        Map requestMap = externalContext.getRequestMap();
        String presetID = WindowScopeManager.getAssociatedWindowID(requestMap);

        //avoid running scope map cleanup more than once per request -- the activated map in the first method call will be discarded in the second call
        //avoid scope map lookup in case this method is invoked multiple time for a given request -- a new map will be extracted off 'disposedWindowScopedMaps' for the same request
        if (presetID != null) {
            return presetID;
        }

        synchronizationMonitor.lock();
        try {
            WindowState state = getState(context);
            String id = externalContext.getRequestParameterMap().get(WindowParameter);
            try {
                for (Object scopeMap : new ArrayList(state.windowScopedMaps.values())) {
                    SpecificScopeMap map = (SpecificScopeMap) scopeMap;
                    if (!map.getId().equals(id)) {
                        map.disactivateIfUnused(context);
                    }
                }
            } catch (Throwable e) {
                logger.log(Level.FINE, "Failed to remove window scope map", e);
            }
            try {
                for (Object scopeMap : new ArrayList(state.disposedWindowScopedMaps)) {
                    SpecificScopeMap map = (SpecificScopeMap) scopeMap;
                    if (!map.getId().equals(id)) {
                        map.discardIfExpired(context);
                    }
                }
            } catch (Throwable e) {
                logger.log(Level.FINE, "Failed to remove window scope map", e);
            }


            if (id == null) {
                SpecificScopeMap scopeMap = lookupStrategy.lookup(context);
                if (scopeMap == null) {
                    long delay = EnvUtils.getWindowScopeCreationDelay(context);
                    if (delay > 0) {
                        //temporarily unlock to allow the handling of a late 'dispose-window' request
                        synchronizationMonitor.unlock();
                        try {
                            Thread.sleep(delay);
                        } catch (InterruptedException e) {
                            //ignore and continue
                        }
                        synchronizationMonitor.lock();
                        //resume synchronized work
                    }
                    if (state.disposedWindowScopedMaps.isEmpty()) {
                        scopeMap = new SpecificScopeMap(context);
                    } else {
                        scopeMap = (SpecificScopeMap) state.disposedWindowScopedMaps.removeFirst();
                    }
                    scopeMap.activate(state);
                }

                WindowScopeManager.associateWindowID(scopeMap.getId(), requestMap);
                return scopeMap.getId();
            } else {
                if (state.windowScopedMaps.containsKey(id)) {
                    WindowScopeManager.associateWindowID(id, requestMap);
                    return id;
                } else {
                    //this must be a postback request while the window is reloading or redirecting
                    for (Object disposedScopeMap : new ArrayList(state.disposedWindowScopedMaps)) {
                        SpecificScopeMap scopeMap = (SpecificScopeMap) disposedScopeMap;
                        if (scopeMap.getId().equals(id)) {
                            scopeMap.activate(state);
                            WindowScopeManager.associateWindowID(id, requestMap);
                            return id;
                        }
                    }

                    //unknown window scope, corresponding SpecificScopeMap might have been erased when not used (no beans stored in it)
                    //most probably a postback received after the server or application was restarted
                    final SpecificScopeMap scopeMap;
                    //verify if this unknown ID is actually a rogue window ID
                    if (VERIFY_WINDOW_ID.matcher(id).matches()) {
                        scopeMap = new SpecificScopeMap(id, context);
                    } else {
                        scopeMap = new SpecificScopeMap(context);
                    }
                    scopeMap.activate(state);
                    WindowScopeManager.associateWindowID(scopeMap.getId(), requestMap);
                    return scopeMap.getId();
                }
            }
        } finally {
            synchronizationMonitor.unlock();
        }
    }

    public void disposeScope(FacesContext context) {
        Lock synchronizationMonitor = getSynchronizationObject(context);
        synchronizationMonitor.lock();
        try {
            final WindowState state = getState(context);
            String id = lookupAssociatedWindowID(context);
            SpecificScopeMap scopeMap = (SpecificScopeMap) state.windowScopedMaps.get(id);
            //verify if the ScopeMap is present
            //it's possible to have dispose-window request arriving after an application restart or re-deploy
            if (scopeMap != null) {
                scopeMap.disactivate(state);
                scopeMap.discardIfExpired(context);
            }

            //notify annotated scope beans that all windows were closed
            if (state.windowScopedMaps.isEmpty()) {
                long windowScopeExpiration = EnvUtils.getWindowScopeExpiration(context);
                //copy session beans before they're cleared on FacesContext.release()
                final Map session = new HashMap(context.getExternalContext().getSessionMap());
                Timer timer = SetupTimer.get(context);
                timer.schedule(new AllWindowsClosedNotifier(state, session), windowScopeExpiration * 2);
            }

            WindowScopeManager.disposeViewScopeBeans(context);
        } finally {
            synchronizationMonitor.unlock();
        }
    }

    public class SpecificScopeMap extends ScopeMap {
        private long activateTimestamp = System.currentTimeMillis();
        private long deactivateTimestamp = -1;

        public SpecificScopeMap(String id, FacesContext facesContext) {
            super(id, facesContext);
            lookupStrategy.associate(facesContext, id);
        }

        public SpecificScopeMap(FacesContext facesContext) {
            super(facesContext);
            lookupStrategy.associate(facesContext, id);
        }

        public void activate(WindowState state) {
            state.windowScopedMaps.put(getId(), this);
            activateTimestamp = System.currentTimeMillis();
        }

        public void disactivate(WindowState state) {
            deactivateTimestamp = System.currentTimeMillis();
            state.disposedWindowScopedMaps.addLast(state.windowScopedMaps.remove(getId()));
        }

        public void disactivateIfUnused(FacesContext facesContext) {
            final UIViewRoot viewRoot = facesContext.getViewRoot();
            if (EnvUtils.isLazyWindowScope(facesContext) && !EnvUtils.containsBeans(this) && (viewRoot == null || !EnvUtils.containsDisposedBeans(viewRoot.getViewMap()))) {
                //the map *does not* contain objects (most probably beans) other than the ones inserted by the framework
                disactivate(getState(facesContext));
            }
        }

        public void discardIfExpired(FacesContext facesContext) {
            WindowState state = getState(facesContext);
            if (System.currentTimeMillis() > (deactivateTimestamp + state.expirationPeriod)) {
                try {
                    if (!isPreDestroyInvoked()) {
                        boolean processingEvents = facesContext.isProcessingEvents();
                        try {
                            facesContext.setProcessingEvents(true);
                            ScopeContext context = new ScopeContext(WindowScopeManager.ScopeName, this);
                            facesContext.getApplication().publishEvent(facesContext, PreDestroyCustomScopeEvent.class, context);
                        } finally {
                            preDestroyInvoked();
                            facesContext.setProcessingEvents(processingEvents);
                        }
                    }
                } finally {
                    state.disposedWindowScopedMaps.remove(this);
                }
            }
        }
    }

    static class AllWindowsClosedNotifier extends TimerTask {
        private final WindowState state;
        private final Map session;

        public AllWindowsClosedNotifier(WindowState state, Map session) {
            this.state = state;
            this.session = session;
        }

        public void run() {
            //re-verify that all windows are still closed
            if (state.windowScopedMaps.isEmpty()) {
                ArrayList expiredMaps = new ArrayList();
                //collect expired maps
                Iterator disposedMaps = state.disposedWindowScopedMaps.iterator();
                while (disposedMaps.hasNext()) {
                    SpecificScopeMap scopeMap = (SpecificScopeMap) disposedMaps.next();
                    //todo: see if scopes can be expired generically
                    if (System.currentTimeMillis() > (scopeMap.deactivateTimestamp + state.expirationPeriod)) {
                        expiredMaps.add(scopeMap);
                    }
                }
                //notify @PreDestroy on window scoped beans
                WindowScopeManager.notifyPreDestroyForAll(expiredMaps);
                //notify @AllWindowsClosed on session scoped beans
                Iterator objects = session.values().iterator();
                while (objects.hasNext()) {
                    Object object = objects.next();
                    WindowScopeManager.callAnnotatedMethod(object, AllWindowsClosed.class);
                }
            }
        }
    }

    private interface SharedMapLookupStrategy {
        SpecificScopeMap lookup(FacesContext context);

        void associate(FacesContext context, String windowID);
    }

    private static class TimeBasedHeuristicWindowScopeSharing implements SharedMapLookupStrategy {
        private long sameWindowMaxDelay = -1;

        public SpecificScopeMap lookup(FacesContext context) {
            if (sameWindowMaxDelay == -1) {
                sameWindowMaxDelay = EnvUtils.getWindowScopeExpiration(context);
            }

            WindowState state = getState(context);
            Iterator i = state.windowScopedMaps.values().iterator();
            while (i.hasNext()) {
                SpecificScopeMap sm = (SpecificScopeMap) i.next();
                if (sm.activateTimestamp + sameWindowMaxDelay > System.currentTimeMillis()) {
                    return sm;
                }
            }

            return null;
        }

        public void associate(FacesContext context, String windowID) {
            //cannot share the windowID
        }
    }

    private static class LiferayOriginalRequestWindowScopeSharing implements SharedMapLookupStrategy {
        private Class PortalUtilClass;
        private Method GetHttpServletRequest;
        private Method GetOriginalServletRequest;

        private LiferayOriginalRequestWindowScopeSharing() throws ClassNotFoundException, NoSuchMethodException {
            PortalUtilClass = Class.forName("com.liferay.portal.util.PortalUtil");
            GetHttpServletRequest = PortalUtilClass.getDeclaredMethod("getHttpServletRequest", javax.portlet.PortletRequest.class);
            GetOriginalServletRequest = PortalUtilClass.getDeclaredMethod("getOriginalServletRequest", HttpServletRequest.class);
        }

        public SpecificScopeMap lookup(FacesContext context) {
            WindowState state = getState(context);
            ExternalContext externalContext = context.getExternalContext();
            HttpServletRequest originalRequest = getOriginalServletRequest(externalContext);
            String sharedWindowID = (String) originalRequest.getAttribute(WindowScopeManager.class.getName());
            return sharedWindowID == null ? null : (SpecificScopeMap) state.windowScopedMaps.get(sharedWindowID);
        }

        public void associate(FacesContext context, String windowID) {
            ExternalContext externalContext = context.getExternalContext();
            HttpServletRequest originalRequest = getOriginalServletRequest(externalContext);
            originalRequest.setAttribute(WindowScopeManager.class.getName(), windowID);

        }

        private HttpServletRequest getOriginalServletRequest(ExternalContext externalContext) {
            try {
                javax.portlet.PortletRequest portletRequest = (javax.portlet.PortletRequest) externalContext.getRequest();
                HttpServletRequest httpPortletRequest = (HttpServletRequest) GetHttpServletRequest.invoke(PortalUtilClass, portletRequest);
                HttpServletRequest originalRequest = (HttpServletRequest) GetOriginalServletRequest.invoke(PortalUtilClass, httpPortletRequest);
                return originalRequest;
            } catch (InvocationTargetException e) {
                return null;
            } catch (IllegalAccessException e) {
                return null;
            }
        }
    }
}
