package org.icefaces.impl.application;

import javax.faces.application.ResourceHandler;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.Logger;

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

class JSFURLWindowScopeTracker implements WindowScopeTracker {
    private static final Logger logger = Logger.getLogger(JSFURLWindowScopeTracker.class.getName());

    public WindowScopeManager.ScopeMap lookupScope(FacesContext context) {
        try {
            final ResourceHandler resourceHandler = context.getApplication().getResourceHandler();
            if (context.getExternalContext().getRequest() != null && resourceHandler.isResourceRequest(context)) {
                return null;
            }

            String id = lookupAssociatedWindowID(context);
            WindowState state = WindowScopeManager.getState(context);
            if (state == null) {
                return null;
            } else {
                Lock synchronizationMonitor = WindowScopeManager.getSynchronizationObject(context);
                synchronizationMonitor.lock();
                try {
                    return (WindowScopeManager.ScopeMap) state.windowScopedMaps.get(id);
                } 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) {
        ExternalContext externalContext = facesContext.getExternalContext();
        try {
            //use reflection when using a ClientWindow to make sure the code runs on JSF 2.1 or lower
            Method getClientWindow = ExternalContext.class.getDeclaredMethod("getClientWindow");
            Object clientWindow = getClientWindow.invoke(externalContext);
            if (clientWindow == null) {
                return null;
            } else {
                Method getId = clientWindow.getClass().getMethod("getId");
                String id = (String) getId.invoke(clientWindow);
                //save the id to be later use by WindowScopeManager.lookupAssociatedWindowID
                externalContext.getRequestMap().put(AssociatedWindowID, id);
                return id;
            }
        } catch (UnsupportedOperationException e) {
            //exception thrown when ExternalContext.getClientWindow() is called during application startup
            return null;
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Cannot use URL based window scope with JSF 2.1 or lower.", e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Cannot use URL based window scope with JSF 2.1 or lower.", e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Cannot use URL based window scope with JSF 2.1 or lower.", e);
        }
    }

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

        synchronizationMonitor.lock();
        try {
            WindowState state = WindowScopeManager.getState(context);
            String id = lookupAssociatedWindowID(context);

            if (!state.windowScopedMaps.containsKey(id)) {
                final SpecificScopeMap scopeMap = new SpecificScopeMap(id, context);
                scopeMap.activate(state);
            }

            return id;
        } finally {
            synchronizationMonitor.unlock();
        }
    }

    public void disposeScope(FacesContext context) {
        //do not dispose scope, a link can still identify the scope even after the corresponding window was closed
    }

    static class SpecificScopeMap extends WindowScopeManager.ScopeMap {
        public SpecificScopeMap(String id, FacesContext facesContext) {
            super(id, facesContext);
        }
    }
}
