/**
 * 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.cxf.ws.policy;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Resource;
import javax.xml.namespace.QName;

import org.apache.cxf.Bus;
import org.apache.cxf.common.injection.NoJSR250Annotations;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.configuration.ConfiguredBeanLocator;
import org.apache.cxf.extension.BusExtension;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.service.factory.FactoryBeanListener;
import org.apache.cxf.service.factory.FactoryBeanListenerManager;
import org.apache.cxf.service.model.BindingFaultInfo;
import org.apache.cxf.service.model.BindingMessageInfo;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.Destination;
import org.apache.cxf.ws.policy.selector.MinimalAlternativeSelector;
import org.apache.neethi.Assertion;
import org.apache.neethi.Constants;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyComponent;
import org.apache.neethi.PolicyOperator;
import org.apache.neethi.PolicyReference;
import org.apache.neethi.PolicyRegistry;

/**
 * 
 */
@NoJSR250Annotations(unlessNull = "bus")
public class PolicyEngineImpl implements PolicyEngine, BusExtension {
    private static final Logger LOG = LogUtils.getL7dLogger(PolicyEngineImpl.class);
    
    
    private static final String POLICY_INFO_REQUEST_SERVER = "policy-engine-info-serve-request";
    private static final String POLICY_INFO_FAULT_SERVER = "policy-engine-info-serve-fault";
    private static final String POLICY_INFO_RESPONSE_SERVER = "policy-engine-info-serve-response";
    private static final String POLICY_INFO_ENDPOINT_SERVER = "policy-engine-info-serve-rendpoint";
    
    private static final String POLICY_INFO_REQUEST_CLIENT = "policy-engine-info-client-request";
    private static final String POLICY_INFO_FAULT_CLIENT = "policy-engine-info-client-fault";
    private static final String POLICY_INFO_RESPONSE_CLIENT = "policy-engine-info-client-response";
    private static final String POLICY_INFO_ENDPOINT_CLIENT = "policy-engine-info-client-endpoint";
    
    private Bus bus;
    private PolicyRegistry registry;
    private Collection<PolicyProvider> policyProviders;
    private Collection<PolicyProvider> preSetPolicyProviders = new LinkedList<PolicyProvider>();
    private boolean enabled = true;
    private Boolean ignoreUnknownAssertions;
    private boolean addedBusInterceptors;
    private AlternativeSelector alternativeSelector;


    public PolicyEngineImpl() { 
        init();
    }
    public PolicyEngineImpl(boolean en) {
        enabled = en;
        init();
    }
    public PolicyEngineImpl(Bus b) {
        init();
        setBus(b);        
    }

    // configuration

    public boolean isEnabled() {
        return enabled;
    }

    @Resource
    public final void setBus(Bus b) {
        if (this.bus == b) {
            //avoid bus init twice through injection
            return;
        }
        bus = b;
        addBusInterceptors();
        FactoryBeanListenerManager fblm = bus.getExtension(FactoryBeanListenerManager.class);
        if (fblm != null) {
            for (FactoryBeanListener l : fblm.getListeners()) {
                if (l instanceof PolicyAnnotationListener) {
                    return;
                }
            }
            fblm.addListener(new PolicyAnnotationListener(bus));
        }
    }

    public Bus getBus() {
        return bus;
    }

    public void setPolicyProviders(Collection<PolicyProvider> p) {
        policyProviders = new CopyOnWriteArrayList<PolicyProvider>(p);
    }
   
    public synchronized void addPolicyProvider(PolicyProvider p) {
        if (policyProviders != null) {
            policyProviders.add(p);
        } else {
            preSetPolicyProviders.add(p);
        }
    }
    public synchronized Collection<PolicyProvider> getPolicyProviders() {
        if (policyProviders == null) {
            policyProviders = new CopyOnWriteArrayList<PolicyProvider>();
            if (bus != null) {
                ConfiguredBeanLocator loc = bus.getExtension(ConfiguredBeanLocator.class);
                if (loc != null) {
                    loc.getBeansOfType(PolicyProvider.class);
                }
            }
            policyProviders.addAll(preSetPolicyProviders);
            preSetPolicyProviders = null;
        }
        return policyProviders;
    }

    public void setRegistry(PolicyRegistry r) {
        registry = r;
    }

    public PolicyRegistry getRegistry() {
        return registry;
    }

    public synchronized void setEnabled(boolean e) {
        enabled = e;
        if (enabled && !addedBusInterceptors) {
            addBusInterceptors();
        } else if (!enabled && addedBusInterceptors) {
            removeBusInterceptors();
        }
    }

    public synchronized AlternativeSelector getAlternativeSelector() {  
        if (alternativeSelector == null && enabled) {
            alternativeSelector = new MinimalAlternativeSelector();
        }
        return alternativeSelector;
    }

    public void setAlternativeSelector(AlternativeSelector as) {
        alternativeSelector = as;
    }

    public boolean isIgnoreUnknownAssertions() {
        return ignoreUnknownAssertions == null ? true : ignoreUnknownAssertions;
    }

    public void setIgnoreUnknownAssertions(boolean ignore) {
        ignoreUnknownAssertions = ignore;
    }

    // BusExtension interface


    public Class<?> getRegistrationType() {
        return PolicyEngine.class;
    }

    // PolicyEngine interface

    public EffectivePolicy getEffectiveClientRequestPolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                           Conduit c, Message m) {
        EffectivePolicy effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_REQUEST_CLIENT);
        if (effectivePolicy == null) {
            synchronized (ei) {
                effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_REQUEST_CLIENT);
                if (null == effectivePolicy) {
                    EffectivePolicyImpl epi = createOutPolicyInfo();
                    Assertor assertor = PolicyUtils.createAsserter(c);
                    epi.initialise(ei, boi, this, assertor, true, true, m);
                    if (m != null) {
                        boi.setProperty(POLICY_INFO_REQUEST_CLIENT, epi);
                    }
                    effectivePolicy = epi;
                }
            }
        }
        return effectivePolicy;
    }

    public void setEffectiveClientRequestPolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                EffectivePolicy ep) {
        boi.setProperty(POLICY_INFO_REQUEST_CLIENT, ep);
    }

    public EffectivePolicy getEffectiveServerResponsePolicy(EndpointInfo ei,
                                                            BindingOperationInfo boi,
                                                            Destination d,
                                                            List<List<Assertion>> incoming, 
                                                            Message m) {
        if (incoming == null) {
            EffectivePolicy effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_RESPONSE_SERVER);
            if (effectivePolicy == null) {
                synchronized (ei) {
                    effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_RESPONSE_SERVER);
                    if (null == effectivePolicy) {
                        EffectivePolicyImpl epi = createOutPolicyInfo();
                        Assertor assertor = PolicyUtils.createAsserter(d);
                        epi.initialise(ei, boi, this, assertor, false, false, null);
                        if (m != null) {
                            boi.setProperty(POLICY_INFO_RESPONSE_SERVER, epi);
                        }
                        effectivePolicy = epi;
                    }
                }
            }
            return effectivePolicy;
        }
        EffectivePolicyImpl epi = createOutPolicyInfo();
        Assertor assertor = PolicyUtils.createAsserter(d);
        epi.initialise(ei, boi, this, assertor, incoming, m);
        return epi;
    }

    public void setEffectiveServerResponsePolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                 EffectivePolicy ep) {
        boi.setProperty(POLICY_INFO_RESPONSE_SERVER, ep);
    }
  
    public EffectivePolicy getEffectiveServerFaultPolicy(EndpointInfo ei,
                                                         BindingOperationInfo boi,
                                                         BindingFaultInfo bfi, 
                                                         Destination d,
                                                         Message m) {

        if (bfi == null) {
            EffectivePolicyImpl epi = createOutPolicyInfo();
            Assertor assertor = PolicyUtils.createAsserter(d);
            epi.initialise(ei, boi, null, this, assertor, m);
            return epi;
        }
        bfi = mapToWrappedBindingFaultInfo(bfi);
        EffectivePolicy effectivePolicy = (EffectivePolicy)bfi.getProperty(POLICY_INFO_FAULT_SERVER);
        if (effectivePolicy == null) {
            synchronized (ei) {
                effectivePolicy = (EffectivePolicy)bfi.getProperty(POLICY_INFO_FAULT_SERVER);
                if (null == effectivePolicy) {
                    EffectivePolicyImpl epi = createOutPolicyInfo();
                    Assertor assertor = PolicyUtils.createAsserter(d);
                    epi.initialise(ei, boi, bfi, this, assertor, m);
                    if (m != null) {
                        bfi.setProperty(POLICY_INFO_FAULT_SERVER, epi);
                    }
                    effectivePolicy = epi;
                }
            }
        }
        return effectivePolicy;
    }

    private BindingFaultInfo mapToWrappedBindingFaultInfo(BindingFaultInfo bfi) {
        BindingOperationInfo boi = bfi.getBindingOperation();
        if (boi != null && boi.isUnwrapped()) {
            boi = boi.getWrappedOperation();
            for (BindingFaultInfo bf2 : boi.getFaults()) {
                if (bf2.getFaultInfo().getName().equals(bfi.getFaultInfo().getName())) {
                    return bf2;
                }
            }
        }
        return bfi;
    }
    public void setEffectiveServerFaultPolicy(EndpointInfo ei, BindingFaultInfo bfi, EffectivePolicy ep) {
        bfi.setProperty(POLICY_INFO_FAULT_SERVER, ep);
    }

    public EndpointPolicy getClientEndpointPolicy(EndpointInfo ei, Conduit conduit, Message m) {
        Assertor assertor = PolicyUtils.createAsserter(conduit);
        return getEndpointPolicy(ei, true, assertor, m);
    }
   
    public EndpointPolicy getServerEndpointPolicy(EndpointInfo ei, Destination destination, Message m) {
        Assertor assertor = PolicyUtils.createAsserter(destination);
        return getEndpointPolicy(ei, false, assertor, m);
    }

    private EndpointPolicy getEndpointPolicy(//NOPMD
        EndpointInfo ei, 
        boolean isRequestor,
        Assertor assertor,
        Message m) {
        return createEndpointPolicyInfo(ei, isRequestor, assertor, m);
    }

    public void setClientEndpointPolicy(EndpointInfo ei, EndpointPolicy ep) {
        ei.setProperty(POLICY_INFO_ENDPOINT_CLIENT, ep);
    }

    public void setServerEndpointPolicy(EndpointInfo ei, EndpointPolicy ep) {
        ei.setProperty(POLICY_INFO_ENDPOINT_SERVER, ep);
    }

    public EffectivePolicy getEffectiveServerRequestPolicy(EndpointInfo ei, 
                                                           BindingOperationInfo boi, 
                                                           Message m) {
        EffectivePolicy effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_REQUEST_SERVER);
        if (effectivePolicy == null) {
            synchronized (ei) {
                effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_REQUEST_SERVER);
                if (null == effectivePolicy) {
                    EffectivePolicyImpl epi = createOutPolicyInfo();
                    epi.initialise(ei, boi, this, false, true, m);
                    if (m != null) {
                        boi.setProperty(POLICY_INFO_REQUEST_SERVER, epi);
                    }
                    effectivePolicy = epi;
                }
            }
        }
        return effectivePolicy;
    }

    public void setEffectiveServerRequestPolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                EffectivePolicy ep) {
        boi.setProperty(POLICY_INFO_REQUEST_SERVER, ep);        
    }

    public EffectivePolicy getEffectiveClientResponsePolicy(EndpointInfo ei,
                                                            BindingOperationInfo boi,
                                                            Message m) {
        EffectivePolicy effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_RESPONSE_CLIENT);
        if (effectivePolicy == null) {
            synchronized (ei) {
                effectivePolicy = (EffectivePolicy)boi.getProperty(POLICY_INFO_RESPONSE_CLIENT);
                if (null == effectivePolicy) {
                    EffectivePolicyImpl epi = createOutPolicyInfo();
                    epi.initialise(ei, boi, this, true, false, m);
                    if (m != null) {
                        boi.setProperty(POLICY_INFO_RESPONSE_CLIENT, epi);
                    }
                    effectivePolicy = epi;
                }
            }
        }
        return effectivePolicy;
    }

    public void setEffectiveClientResponsePolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                 EffectivePolicy ep) {
        boi.setProperty(POLICY_INFO_RESPONSE_CLIENT, ep);
    }

    public EffectivePolicy getEffectiveClientFaultPolicy(EndpointInfo ei,
                                                         BindingOperationInfo boi,
                                                         BindingFaultInfo bfi,
                                                         Message m) {
        EffectivePolicy effectivePolicy = null;
        if (bfi != null) {
            effectivePolicy = (EffectivePolicy)bfi.getProperty(POLICY_INFO_FAULT_CLIENT);
        }
        if (effectivePolicy == null) {
            synchronized (ei) {
                if (bfi != null) {
                    effectivePolicy = (EffectivePolicy)bfi.getProperty(POLICY_INFO_FAULT_CLIENT);
                }
                if (null == effectivePolicy) {
                    EffectivePolicyImpl epi = createOutPolicyInfo();
                    epi.initialisePolicy(ei, boi, bfi, this, m);
                    if (bfi != null) {
                        bfi.setProperty(POLICY_INFO_FAULT_CLIENT, epi);
                    }
                    effectivePolicy = epi;
                }
            }
        }
        return effectivePolicy;
    }

    public void setEffectiveClientFaultPolicy(EndpointInfo ei, BindingFaultInfo bfi, EffectivePolicy ep) {
        bfi.setProperty(POLICY_INFO_FAULT_CLIENT, ep);
    }

    // implementation

    protected final void init() {
        registry = new PolicyRegistryImpl();
    }
    


    public synchronized void removeBusInterceptors() {
        bus.getInInterceptors().remove(PolicyInInterceptor.INSTANCE);
        bus.getOutInterceptors().remove(PolicyOutInterceptor.INSTANCE);
        bus.getInFaultInterceptors().remove(ClientPolicyInFaultInterceptor.INSTANCE);
        bus.getOutFaultInterceptors().remove(ServerPolicyOutFaultInterceptor.INSTANCE);
        bus.getInFaultInterceptors().remove(PolicyVerificationInFaultInterceptor.INSTANCE);
        addedBusInterceptors = false;
    }

    public final synchronized void addBusInterceptors() {
        if (null == bus || !enabled) {
            return;
        }
    
        if (ignoreUnknownAssertions != null) {
            AssertionBuilderRegistry abr = bus.getExtension(AssertionBuilderRegistry.class);
            if (null != abr) {
                abr.setIgnoreUnknownAssertions(ignoreUnknownAssertions);
            }
        }

        bus.getInInterceptors().add(PolicyInInterceptor.INSTANCE);
        bus.getOutInterceptors().add(PolicyOutInterceptor.INSTANCE);
        bus.getInFaultInterceptors().add(ClientPolicyInFaultInterceptor.INSTANCE);
        bus.getOutFaultInterceptors().add(ServerPolicyOutFaultInterceptor.INSTANCE);
        bus.getInFaultInterceptors().add(PolicyVerificationInFaultInterceptor.INSTANCE);
    
        addedBusInterceptors = true;
    }  

    Policy getAggregatedServicePolicy(ServiceInfo si, Message m) {
        if (si == null) {
            return new Policy();
        }
        Policy aggregated = null;
        for (PolicyProvider pp : getPolicyProviders()) {
            Policy p = pp.getEffectivePolicy(si, m);
            if (null == aggregated) {
                aggregated = p;
            } else if (p != null) {
                aggregated = aggregated.merge(p);
            }
        }
        return aggregated == null ? new Policy() : aggregated;
    }

    Policy getAggregatedEndpointPolicy(EndpointInfo ei, Message m) {
        Policy aggregated = null;
        for (PolicyProvider pp : getPolicyProviders()) {
            Policy p = pp.getEffectivePolicy(ei, m);
            if (null == aggregated) {
                aggregated = p;
            } else if (p != null) {
                aggregated = aggregated.merge(p);
            }
        }
        return aggregated == null ? new Policy() : aggregated;
    }

    Policy getAggregatedOperationPolicy(BindingOperationInfo boi, Message m) {
        Policy aggregated = null;
        for (PolicyProvider pp : getPolicyProviders()) {
            Policy p = pp.getEffectivePolicy(boi, m);
            if (null == aggregated) {
                aggregated = p;
            } else if (p != null) {
                aggregated = aggregated.merge(p);
            }
        }
        return aggregated == null ? new Policy() : aggregated;
    }

    Policy getAggregatedMessagePolicy(BindingMessageInfo bmi, Message m) {
        Policy aggregated = null;
        for (PolicyProvider pp : getPolicyProviders()) {
            Policy p = pp.getEffectivePolicy(bmi, m);
            if (null == aggregated) {
                aggregated = p;
            } else if (p != null) {
                aggregated = aggregated.merge(p);
            }
        }
        return aggregated == null ? new Policy() : aggregated;
    }

    Policy getAggregatedFaultPolicy(BindingFaultInfo bfi, Message m) {
        Policy aggregated = null;
        for (PolicyProvider pp : getPolicyProviders()) {
            Policy p = pp.getEffectivePolicy(bfi, m);
            if (null == aggregated) {
                aggregated = p;
            } else if (p != null) {
                aggregated = aggregated.merge(p);
            }
        }
        return aggregated == null ? new Policy() : aggregated;
    }

    /**
     * Return a collection of all assertions used in the given policy component,
     * optionally including optional assertions.
     * The policy need not be normalised, so any policy references will have to be resolved.
     * @param pc the policy component
     * @param includeOptional flag indicating if optional assertions should be included
     * @return the assertions
     */
    Collection<Assertion> getAssertions(PolicyComponent pc, boolean includeOptional) {
    
        Collection<Assertion> assertions = new ArrayList<Assertion>();
    
        if (Constants.TYPE_ASSERTION == pc.getType()) {
            Assertion a = (Assertion)pc;
            if (includeOptional || !a.isOptional()) {
                assertions.add(a);
            }
        } else {   
            addAssertions(pc, includeOptional, assertions);
        }
        return assertions;
    }
    Collection<Assertion> getAssertions(EffectivePolicy pc, boolean includeOptional) {
        if (pc == null || pc.getChosenAlternative() == null) {
            return null;
        }
        Collection<Assertion> assertions = new ArrayList<Assertion>();
        for (Assertion assertion : pc.getChosenAlternative()) {
            if (Constants.TYPE_ASSERTION == assertion.getType()) {
                if (includeOptional || !assertion.isOptional()) {
                    assertions.add(assertion);
                }
            } else {   
                addAssertions(assertion, includeOptional, assertions);
            }
        }
        return assertions;
    }

    void addAssertions(PolicyComponent pc, boolean includeOptional, 
                       Collection<Assertion> assertions) {
   
        if (Constants.TYPE_ASSERTION == pc.getType()) {
            Assertion a = (Assertion)pc;
            if (includeOptional || !a.isOptional()) {
                assertions.add((Assertion)pc);            
            }
            return;
        } 
    
        if (Constants.TYPE_POLICY_REF == pc.getType()) {
            PolicyReference pr = (PolicyReference)pc;
            pc = pr.normalize(registry, false);
        }

        PolicyOperator po = (PolicyOperator)pc;

        List<PolicyComponent> pcs = CastUtils.cast(po.getPolicyComponents(), PolicyComponent.class);
        for (PolicyComponent child : pcs) {
            addAssertions(child, includeOptional, assertions);
        }
    }

    /**
     * Return the vocabulary of a policy component, i.e. the set of QNames of
     * the assertions used in the componente, duplicates removed.
     * @param pc the policy component
     * @param includeOptional flag indicating if optional assertions should be included
     * @return the vocabulary
     */
    Set<QName> getVocabulary(PolicyComponent pc, boolean includeOptional) {
        Collection<Assertion> assertions = getAssertions(pc, includeOptional);
        Set<QName> vocabulary = new HashSet<QName>();
        for (Assertion a : assertions) {
            vocabulary.add(a.getName());
        }
        return vocabulary;
    } 

    EndpointPolicy createEndpointPolicyInfo(EndpointInfo ei,
                                                boolean isRequestor,
                                                Assertor assertor,
                                                Message m) {
        EndpointPolicy ep = (EndpointPolicy)ei.getProperty(isRequestor
                                                           ? POLICY_INFO_ENDPOINT_CLIENT : POLICY_INFO_ENDPOINT_SERVER);
        if (ep == null) {
            synchronized (ei) {
                ep = (EndpointPolicy)ei.getProperty(isRequestor
                    ? POLICY_INFO_ENDPOINT_CLIENT : POLICY_INFO_ENDPOINT_SERVER);
                if (ep == null) {
                    EndpointPolicyImpl epi = new EndpointPolicyImpl(ei, this, isRequestor, assertor);
                    epi.initialize(m);
                    if (m != null) {
                        ei.setProperty(isRequestor ? POLICY_INFO_ENDPOINT_CLIENT : POLICY_INFO_ENDPOINT_SERVER, epi);
                    }
                    ep = epi;
                }
            }
        }
        return ep;
    }


    /**
     * Check if a given list of assertions can potentially be supported by
     * interceptors or by an already installed assertor (a conduit or transport
     * that implements the Assertor interface).
     * 
     * @param alternative the policy alternative
     * @param Assertor the assertor
     * @return true iff the alternative can be supported
     */
    public boolean supportsAlternative(Collection<? extends PolicyComponent> alternative, 
                                       Assertor assertor,
                                       Message m) {
        PolicyInterceptorProviderRegistry pipr = 
            bus.getExtension(PolicyInterceptorProviderRegistry.class);
        final boolean doLog = LOG.isLoggable(Level.FINE);
        for (PolicyComponent pc : alternative) {
            if (pc instanceof Assertion) {
                Assertion a = (Assertion)pc;
                if (!a.isOptional()) {
                    if (null != assertor && assertor.canAssert(a.getName())) {
                        continue;
                    }
                    Set<PolicyInterceptorProvider> s = pipr.get(a.getName());
                    if (s.isEmpty()) {
                        if (doLog) {
                            LOG.fine("Alternative " + a.getName() + " is not supported");
                        }
                        return false;
                    }
                    for (PolicyInterceptorProvider p : s) {
                        if (!p.configurationPresent(m, a)) {
                            if (doLog) {
                                LOG.fine("Alternative " + a.getName() + " is not supported");
                            }
                            return false;
                        }
                    }
                }
            } else {
                return false;
            }
        }
        return true;
    }


    // for test
    EffectivePolicyImpl createOutPolicyInfo() {
        return new EffectivePolicyImpl();
    }
    

}
