package com.github.ruediste.rise.core.scopes;

import java.io.Serializable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;

import com.github.ruediste.rise.core.scopes.HttpScopeManager.SessionScopeManager.SessionScopeData;
import com.github.ruediste.rise.nonReloadable.front.reload.NonReloadable;
import com.github.ruediste.rise.util.GenericEventManager;
import com.github.ruediste.salta.core.Binding;
import com.github.ruediste.salta.core.CoreDependencyKey;
import com.github.ruediste.salta.jsr330.MembersInjector;
import com.github.ruediste.salta.standard.util.SimpleProxyScopeManager;
import com.google.common.base.Preconditions;

import net.sf.cglib.proxy.Dispatcher;
import net.sf.cglib.proxy.Enhancer;

/**
 * Manager for {@link RequestScoped @RequestScoped} and
 * {@link SessionScoped @SessionScoped}.
 * 
 * <p>
 * <b> Life Cycle </b> <br>
 * The session scope manager supports iterating all session scopes (see
 * {@link #runInEachSessionScope(Runnable)}) and destroy events ( see
 * {@link SessionScopeEvents} )
 * <p>
 * <img src="doc-files/sessionScopeLifeCycle.png" alt=
 * "class diagram of the life cycle handling">
 * <p>
 * When a session is destroyed,
 * {@link SessionScopeData#valueUnbound(HttpSessionBindingEvent)} is invoked,
 * which triggers the {@link SessionScopeData#valueUnbound} event. Upon creation
 * of the session scope data, the {@link SessionScopeManager} registers itself
 * with that event. If it is fired, the scope manager invokes
 * {@link SessionScopeEvents#fireDestroyed(HttpSession)}, which causes the
 * session destroyed event to be triggered. The scope manager makes sure the
 * event is fired within the session scope.
 */
public class HttpScopeManager {
    public static final String SESSION_SCOPE_DATA_KEY = "RISE_SESSION_SCOPE_DATA";
    SessionScopeManager sessionScopeManager = new SessionScopeManager();
    SimpleProxyScopeManager requestScopeHandler = new SimpleProxyScopeManager(
            "Request");

    @PostConstruct
    private void postConstruct(
            MembersInjector<SessionScopeManager> managerInjector) {
        managerInjector.injectMembers(sessionScopeManager);
    }

    /**
     * Enter the scopes
     */
    public void enter(HttpServletRequest request,
            HttpServletResponse response) {
        sessionScopeManager.enter(() -> request.getSession());
        requestScopeHandler.setFreshState();
    }

    /**
     * Leave the scope. Always call from a finally block, to make sure scopes
     * are correctly cleaned up
     */
    public void exit() {
        requestScopeHandler.setState(null);
        sessionScopeManager.setState(null);
    }

    public void runInSessionScope(HttpSession session, Runnable run) {
        SessionScopeManager.State old = sessionScopeManager
                .enter(() -> session);
        try {
            run.run();
        } finally {
            sessionScopeManager.setState(old);
        }
    }

    public void runInEachSessionScope(Runnable run) {
        for (SessionScopeData data : sessionScopeManager.dataMap.values()) {
            SessionScopeManager.State old = sessionScopeManager
                    .enterWithData(() -> data);
            try {
                run.run();
            } finally {
                sessionScopeManager.setState(old);
            }
        }
    }

    static final class SessionScopeManager implements
            com.github.ruediste.salta.standard.ScopeImpl.ScopeHandler {

        @Inject
        SessionScopeEvents scopeEvents;

        ConcurrentHashMap<String, SessionScopeData> dataMap = new ConcurrentHashMap<>();

        private static Object sessionLock = new Object();

        @NonReloadable
        public static class SessionScopeData
                implements Serializable, HttpSessionBindingListener {
            private static final long serialVersionUID = 1L;
            public ConcurrentMap<Binding, Object> sessionDataMap = new ConcurrentHashMap<>();
            public GenericEventManager<HttpSessionBindingEvent> valueUnbound = new GenericEventManager<>();

            @Override
            public void valueBound(HttpSessionBindingEvent event) {

            }

            @Override
            public void valueUnbound(HttpSessionBindingEvent event) {
                valueUnbound.fire(event);
            }

        }

        private static class State {
            Supplier<SessionScopeData> dataSupplier;
            SessionScopeData data;

            SessionScopeData getSessionData() {
                if (data == null) {
                    data = dataSupplier.get();
                }
                return data;
            }
        }

        ThreadLocal<State> currentState = new ThreadLocal<>();

        private SessionScopeData getSessionScopeData(HttpSession session) {
            Object result = session.getAttribute(SESSION_SCOPE_DATA_KEY);
            if (result == null) {
                synchronized (sessionLock) {
                    result = session.getAttribute(SESSION_SCOPE_DATA_KEY);
                    if (result == null) {
                        result = createSessionScopeData(session);
                        session.setAttribute(SESSION_SCOPE_DATA_KEY, result);
                    }
                }
            }
            return (SessionScopeData) result;
        }

        private SessionScopeData createSessionScopeData(HttpSession session) {
            SessionScopeData result = new SessionScopeData();
            dataMap.put(session.getId(), result);
            result.valueUnbound.addListener(e -> {
                dataMap.remove(session.getId());
                if (currentState.get() != null)
                    scopeEvents.fireDestroyed(session);
                else {
                    State old = enterWithData(() -> result);
                    try {
                        scopeEvents.fireDestroyed(session);
                    } finally {
                        setState(old);
                    }
                }
            });
            return result;
        }

        public State enter(Supplier<HttpSession> sessionSupplier) {
            return enterWithData(
                    () -> getSessionScopeData(sessionSupplier.get()));
        }

        private State enterWithData(Supplier<SessionScopeData> dataSupplier) {
            State state = new State();
            state.dataSupplier = dataSupplier;
            return setState(state);
        }

        public State setState(State state) {
            State old = currentState.get();
            if (state == null)
                currentState.remove();
            else
                currentState.set(state);
            return old;
        }

        @Override
        public Supplier<Object> scope(Supplier<Object> supplier,
                Binding binding, CoreDependencyKey<?> requestedKey) {
            // create the proxy right away, such that it can be reused
            // afterwards
            Object proxy = Enhancer.create(requestedKey.getRawType(),
                    new Dispatcher() {

                        @Override
                        public Object loadObject() throws Exception {
                            State state = currentState.get();
                            Preconditions.checkState(state != null,
                                    "Access to session scoped proxy without active session scope");
                            return state.getSessionData().sessionDataMap
                                    .computeIfAbsent(binding,
                                            b -> supplier.get());
                        }
                    });

            return new Supplier<Object>() {

                @Override
                public Object get() {
                    return proxy;
                }

                @Override
                public String toString() {
                    return "SessionScopeProxy[" + requestedKey + "]";
                }
            };
        }
    }

}
@@ -90,33 +90,34 @@
 til.
-concurrent.ConcurrentHash
+HashMap;%0Aimport java.util.
 Map;
@@ -155,16 +155,20 @@
 ncurrent
+Hash
 Map;%0Aimp
@@ -4262,26 +4262,16 @@
  public 
-Concurrent
 Map%3CBind
@@ -4300,34 +4300,24 @@
 taMap = new 
-Concurrent
 HashMap%3C%3E();
@@ -7790,20 +7790,88 @@
     
-return state
+Map%3CBinding, Object%3E sessionDataMap = state%0A                                    
 .get
@@ -7898,16 +7898,17 @@
 nDataMap
+;
 %0A       
@@ -7928,24 +7928,101 @@
         
-        
+synchronized (sessionDataMap) %7B%0A                                return sessionDataMap
 .compute
@@ -8079,20 +8079,16 @@
         
-    
 b -%3E sup
@@ -8097,24 +8097,54 @@
 ier.get());%0A
+                            %7D%0A
             

fix deadlock in HttpScopeManager