/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.shiro.guice.web;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.name.Names;
import com.google.inject.servlet.ServletModule;
import org.apache.shiro.guice.ShiroModule;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.env.Environment;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.web.env.WebEnvironment;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.filter.authc.*;
import org.apache.shiro.web.filter.authz.*;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;

import javax.servlet.Filter;
import javax.servlet.ServletContext;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Sets up Shiro lifecycles within Guice, enables the injecting of Shiro objects, and binds a default
 * {@link org.apache.shiro.web.mgt.WebSecurityManager}, {@link org.apache.shiro.mgt.SecurityManager} and {@link org.apache.shiro.session.mgt.SessionManager}.  At least one realm must be added by
 * using {@link #bindRealm() bindRealm}.
 * <p/>
 * Also provides for the configuring of filter chains and binds a {@link org.apache.shiro.web.filter.mgt.FilterChainResolver} with that information.
 */
public abstract class ShiroWebModule extends ShiroModule {
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<AnonymousFilter> ANON = Key.get(AnonymousFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<FormAuthenticationFilter> AUTHC = Key.get(FormAuthenticationFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<BasicHttpAuthenticationFilter> AUTHC_BASIC = Key.get(BasicHttpAuthenticationFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<NoSessionCreationFilter> NO_SESSION_CREATION = Key.get(NoSessionCreationFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<LogoutFilter> LOGOUT = Key.get(LogoutFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<PermissionsAuthorizationFilter> PERMS = Key.get(PermissionsAuthorizationFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<PortFilter> PORT = Key.get(PortFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<HttpMethodPermissionFilter> REST = Key.get(HttpMethodPermissionFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<RolesAuthorizationFilter> ROLES = Key.get(RolesAuthorizationFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
    @SuppressWarnings({"UnusedDeclaration"})
    public static final Key<UserFilter> USER = Key.get(UserFilter.class);


    static final String NAME = "SHIRO";

    /**
     * We use a LinkedHashMap here to ensure that iterator order is the same as add order.  This is important, as the
     * FilterChainResolver uses iterator order when searching for a matching chain.
     */
    private final Map<String, Key<? extends Filter>[]> filterChains = new LinkedHashMap<String, Key<? extends Filter>[]>();
    private final ServletContext servletContext;

    public ShiroWebModule(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    public static void bindGuiceFilter(Binder binder) {
        binder.install(guiceFilterModule());
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public static void bindGuiceFilter(final String pattern, Binder binder) {
        binder.install(guiceFilterModule(pattern));
    }

    public static ServletModule guiceFilterModule() {
        return guiceFilterModule("/*");
    }

    public static ServletModule guiceFilterModule(final String pattern) {
        return new ServletModule() {
            @Override
            protected void configureServlets() {
                filter(pattern).through(GuiceShiroFilter.class);
            }
        };
    }

    @Override
    protected final void configureShiro() {
        bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME)));
        bind(Key.get(ServletContext.class, Names.named(NAME))).toInstance(this.servletContext);
        bindWebSecurityManager(bind(WebSecurityManager.class));
        bindWebEnvironment(bind(WebEnvironment.class));
        bind(GuiceShiroFilter.class).asEagerSingleton();
        expose(GuiceShiroFilter.class);

        this.configureShiroWeb();

        setupFilterChainConfigs();

        bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(filterChains));
    }

    private void setupFilterChainConfigs() {
        Table<Key<? extends PathMatchingFilter>, String, String> configs = HashBasedTable.create();

        for (Map.Entry<String, Key<? extends Filter>[]> filterChain : filterChains.entrySet()) {
            for (int i = 0; i < filterChain.getValue().length; i++) {
                Key<? extends Filter> key = filterChain.getValue()[i];
                if (key instanceof FilterConfigKey) {
                    FilterConfigKey<? extends PathMatchingFilter> configKey = (FilterConfigKey<? extends PathMatchingFilter>) key;
                    key = configKey.getKey();
                    filterChain.getValue()[i] = key;
                    if (!PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
                        throw new ConfigurationException("Config information requires a PathMatchingFilter - can't apply to " + key.getTypeLiteral().getRawType());
                    }
                    configs.put(castToPathMatching(key), filterChain.getKey(), configKey.getConfigValue());
                } else if (PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
                    configs.put(castToPathMatching(key), filterChain.getKey(), "");
                }
            }
        }
        for (Key<? extends PathMatchingFilter> filterKey : configs.rowKeySet()) {
            bindPathMatchingFilter(filterKey, configs.row(filterKey));
        }
    }

    private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) {
        bind(filterKey).toProvider(new PathMatchingFilterProvider<T>(filterKey, configs)).asEagerSingleton();
    }

    @SuppressWarnings({"unchecked"})
    private Key<? extends PathMatchingFilter> castToPathMatching(Key<? extends Filter> key) {
        return (Key<? extends PathMatchingFilter>) key;
    }

    protected abstract void configureShiroWeb();

    @SuppressWarnings({"unchecked"})
    @Override
    protected final void bindSecurityManager(AnnotatedBindingBuilder<? super SecurityManager> bind) {
        bindWebSecurityManager(bind);
    }

    /**
     * Binds the security manager.  Override this method in order to provide your own security manager binding.
     * <p/>
     * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton.
     *
     * @param bind
     */
    protected void bindWebSecurityManager(AnnotatedBindingBuilder<? super WebSecurityManager> bind) {
        try {
            bind.toConstructor(DefaultWebSecurityManager.class.getConstructor(Collection.class)).asEagerSingleton();
        } catch (NoSuchMethodException e) {
            throw new ConfigurationException("This really shouldn't happen.  Either something has changed in Shiro, or there's a bug in ShiroModule.", e);
        }
    }

    /**
     * Binds the session manager.  Override this method in order to provide your own session manager binding.
     * <p/>
     * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton.
     *
     * @param bind
     */
    @Override
    protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) {
        bind.to(ServletContainerSessionManager.class).asEagerSingleton();
    }

    @Override
    protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) {
        bindWebEnvironment(bind);
    }

    protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) {
        bind.to(WebGuiceEnvironment.class).asEagerSingleton();
    }

    /**
     * Adds a filter chain to the shiro configuration.
     * <p/>
     * NOTE: If the provided key is for a subclass of {@link org.apache.shiro.web.filter.PathMatchingFilter}, it will be registered with a proper
     * provider.
     *
     * @param pattern
     * @param keys
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected final void addFilterChain(String pattern, Key<? extends Filter>... keys) {
        filterChains.put(pattern, keys);
    }

    protected static <T extends PathMatchingFilter> Key<T> config(Key<T> baseKey, String configValue) {
        return new FilterConfigKey<T>(baseKey, configValue);
    }

    @SuppressWarnings({"UnusedDeclaration"})
    protected static <T extends PathMatchingFilter> Key<T> config(TypeLiteral<T> typeLiteral, String configValue) {
        return config(Key.get(typeLiteral), configValue);
    }

    @SuppressWarnings({"UnusedDeclaration"})
    protected static <T extends PathMatchingFilter> Key<T> config(Class<T> type, String configValue) {
        return config(Key.get(type), configValue);
    }

    private static class FilterConfigKey<T extends PathMatchingFilter> extends Key<T> {
        private Key<T> key;
        private String configValue;

        private FilterConfigKey(Key<T> key, String configValue) {
            super();
            this.key = key;
            this.configValue = configValue;
        }

        public Key<T> getKey() {
            return key;
        }

        public String getConfigValue() {
            return configValue;
        }
    }
}
