/*
 * 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.tomee.catalina.realm;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Realm;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.util.LifecycleBase;
import org.apache.openejb.config.sys.PropertiesAdapter;
import org.apache.tomee.catalina.TomEERuntimeException;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.xbean.recipe.ObjectRecipe;
import org.apache.xbean.recipe.Option;
import org.ietf.jgss.GSSContext;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Properties;
import java.util.Set;

public class LazyRealm extends LifecycleBase implements Realm {
    private String realmClass;
    private String properties;
    private String name;
    private boolean cdi;

    private volatile boolean init;
    private volatile boolean start;
    private volatile Realm delegate;
    private Context container;
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);

    private CreationalContext<Object> creationalContext;

    public void setName(final String name) {
        this.name = name;
    }

    public void setRealmClass(final String realmClass) {
        this.realmClass = realmClass;
    }

    public void setProperties(final String properties) {
        this.properties = properties;
    }

    public void setCdi(final boolean cdi) {
        this.cdi = cdi;
    }

    private Realm instance() {
        if (delegate == null) {
            synchronized (this) {
                if (delegate == null) {
                    final Object instance;

                    ClassLoader cl = Thread.currentThread().getContextClassLoader();
                    if (container != null && container.getLoader() != null && container.getLoader().getClassLoader() != null) {
                        cl = container.getLoader().getClassLoader();
                    }

                    final Class<?> clazz;
                    try {
                        clazz = cl.loadClass(realmClass);
                    } catch (final ClassNotFoundException e) {
                        throw new TomEERuntimeException(e);
                    }

                    if (!cdi) {
                        try {
                            final ObjectRecipe recipe = new ObjectRecipe(clazz);
                            recipe.allow(Option.CASE_INSENSITIVE_PROPERTIES);
                            recipe.allow(Option.IGNORE_MISSING_PROPERTIES);
                            recipe.allow(Option.FIELD_INJECTION);
                            recipe.allow(Option.PRIVATE_PROPERTIES);
                            if (properties != null) {
                                final Properties props = new PropertiesAdapter()
                                        .unmarshal(properties.trim().replaceAll("\\p{Space}*(\\p{Alnum}*)=", "\n$1="));
                                recipe.setAllProperties(props);
                            }
                            instance = recipe.create();
                        } catch (final Exception e) {
                            throw new TomEERuntimeException(e);
                        }
                    } else {
                        final WebBeansContext webBeansContext;
                        try {
                            webBeansContext = WebBeansContext.currentInstance();
                            if (webBeansContext == null) {
                                return null;
                            }
                        } catch (final IllegalStateException ise) {
                            return null; // too early to have a cdi bean, skip these methods - mainly init() but @PostConstruct works then
                        }

                        final BeanManager bm = webBeansContext.getBeanManagerImpl();
                        final Set<Bean<?>> beans = bm.getBeans(clazz);
                        final Bean<?> bean = bm.resolve(beans);
                        if (bean == null) {
                            return null;
                        }
                        creationalContext = bm.createCreationalContext(null);
                        instance = bm.getReference(bean, clazz, creationalContext);
                    }

                    if (instance == null) {
                        throw new TomEERuntimeException("realm can't be retrieved from cdi");
                    }
                    if (instance instanceof Realm) {
                        delegate = (Realm) instance;
                        delegate.setContainer(container);
                        if (Lifecycle.class.isInstance(delegate)) {
                            if (init) {
                                try {
                                    final Lifecycle lifecycle = Lifecycle.class.cast(delegate);
                                    lifecycle.init();
                                    if (start) {
                                        lifecycle.start();
                                    }
                                } catch (final LifecycleException e) {
                                    // no-op
                                }
                            }
                        }
                    } else {
                        delegate = new LowTypedRealm(instance);
                        delegate.setContainer(container);
                    }
                    for (final PropertyChangeListener listener : support.getPropertyChangeListeners()) {
                        delegate.addPropertyChangeListener(listener);
                    }
                }
            }
        }
        return delegate;
    }

    private Class<?> loadClass() {
        if (container != null && container.getLoader() != null && container.getLoader().getClassLoader() != null) {
            try {
                return container.getLoader().getClassLoader().loadClass(realmClass);
            } catch (final ClassNotFoundException e) {
                // no-op
            }
        }
        return null;
    }

    @Override
    protected void initInternal() throws LifecycleException {
        final Class<?> r = loadClass();
        if (r != null && Lifecycle.class.isAssignableFrom(r) && instance() != null) {
            Lifecycle.class.cast(delegate).init();
        } else {
            init = true;
        }
    }

    @Override
    protected void startInternal() throws LifecycleException {
        final Class<?> r = loadClass();
        if (r != null && Lifecycle.class.isAssignableFrom(r) && instance() != null) {
            Lifecycle.class.cast(instance()).start();
        } else {
            start = true;
        }
        setState(LifecycleState.STARTING);
    }

    @Override
    protected void stopInternal() throws LifecycleException {
        final Class<?> r = loadClass();
        if (r != null && Lifecycle.class.isAssignableFrom(r) && instance() != null) {
            Lifecycle.class.cast(delegate).stop();
        }
        setState(LifecycleState.STOPPING);
    }

    @Override
    protected void destroyInternal() throws LifecycleException {
        final Class<?> r = loadClass();
        if (r != null && Lifecycle.class.isAssignableFrom(r) && instance() != null) {
            Lifecycle.class.cast(delegate).destroy();
        }
    }

    @Override
    public Container getContainer() {
        if (delegate != null) {
            return delegate.getContainer();
        }
        return container;
    }

    @Override
    public void setContainer(final Container container) {
        container.addLifecycleListener(new LifecycleListener() {
            @Override
            public void lifecycleEvent(final LifecycleEvent event) {
                if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) {
                    if (creationalContext != null) {
                        creationalContext.release();
                    }
                }
            }
        });

        if (delegate != null) {
            delegate.setContainer(container);
        } else {
            this.container = Context.class.cast(container);
        }
    }

    @Override
    public String getInfo() {
        if (name != null) {
            return name;
        }
        final Realm instance = instance();
        if (instance == null) {
            return getClass().getName() + "/1.0";
        }
        return instance().getInfo();
    }

    @Override
    public void addPropertyChangeListener(final PropertyChangeListener listener) {
        if (delegate != null) {
            delegate.addPropertyChangeListener(listener);
        }
        support.addPropertyChangeListener(listener);
    }

    @Override
    public Principal authenticate(final String s) {
        return instance().authenticate(s);
    }

    @Override
    public Principal authenticate(final String username, final String credentials) {
        return instance().authenticate(username, credentials);
    }

    @Override
    public Principal authenticate(final String username, final String digest, final String nonce, final String nc,
                                  final String cnonce, final String qop, final String realm, final String md5a2) {
        return instance().authenticate(username, digest, nonce, nc, cnonce, qop, realm, md5a2);
    }

    @Override
    public Principal authenticate(final GSSContext gssContext, final boolean storeCreds) {
        return instance().authenticate(gssContext, storeCreds);
    }

    @Override
    public Principal authenticate(final X509Certificate[] certs) {
        return instance().authenticate(certs);
    }

    @Override
    public void backgroundProcess() {
        if (delegate != null) {
            instance().backgroundProcess();
        }
    }

    @Override
    public SecurityConstraint[] findSecurityConstraints(final Request request, final Context context) {
        return instance().findSecurityConstraints(request, context);
    }

    @Override
    public boolean hasResourcePermission(final Request request, final Response response,
                                         final SecurityConstraint[] securityConstraints, final Context context) throws IOException {
        return instance().hasResourcePermission(request, response, securityConstraints, context);
    }

    @Override
    public boolean hasRole(final Wrapper wrapper, final Principal principal, final String role) {
        return instance().hasRole(wrapper, principal, role);
    }

    @Override
    public boolean hasUserDataPermission(final Request request, final Response response,
                                         final SecurityConstraint[] securityConstraints) throws IOException {
        return instance().hasUserDataPermission(request, response, securityConstraints);
    }

    @Override
    public void removePropertyChangeListener(final PropertyChangeListener listener) {
        if (delegate != null) {
            delegate.removePropertyChangeListener(listener);
        }
        support.removePropertyChangeListener(listener);
    }
}
