/**
 * 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.ConcurrentHashMap;

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

import org.apache.cxf.Bus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.endpoint.ServerLifeCycleListener;
import org.apache.cxf.endpoint.ServerLifeCycleManager;
import org.apache.cxf.extension.BusExtension;
import org.apache.cxf.helpers.CastUtils;
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;

/**
 * 
 */
public class PolicyEngineImpl implements PolicyEngine, BusExtension, ServerLifeCycleListener {

    private Bus bus;
    private PolicyRegistry registry;
    private Collection<PolicyProvider> policyProviders;
    private boolean enabled;
    private boolean ignoreUnknownAssertions;
    private boolean addedBusInterceptors;
    private AlternativeSelector alternativeSelector;

    private Map<BindingOperation, EffectivePolicy> clientRequestInfo;
    private Map<BindingOperation, EffectivePolicy> clientResponseInfo;
    private Map<BindingFault, EffectivePolicy> clientFaultInfo;
    private Map<BindingOperation, EffectivePolicy> serverRequestInfo;
    private Map<BindingOperation, EffectivePolicy> serverResponseInfo;
    private Map<BindingFault, EffectivePolicy> serverFaultInfo;
    private Map<EndpointInfo, EndpointPolicy> serverEndpointInfo;
    private Map<EndpointInfo, EndpointPolicy> clientEndpointInfo;

    public PolicyEngineImpl() { 
        init();
    }

    // configuration

    public boolean isEnabled() {
        return enabled;
    }

    public void setBus(Bus b) {
        bus = b;
    }

    public Bus getBus() {
        return bus;
    }

    public void setPolicyProviders(Collection<PolicyProvider> p) {
        policyProviders = p;
    }
   
    public Collection<PolicyProvider> getPolicyProviders() {
        return policyProviders;
    }

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

    public PolicyRegistry getRegistry() {
        return registry;
    }

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

    public AlternativeSelector getAlternativeSelector() {    
        return alternativeSelector;
    }

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

    public boolean isIgnoreUnknownAssertions() {
        return 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) {
        BindingOperation bo = new BindingOperation(ei, boi);
        EffectivePolicy effectivePolicy = clientRequestInfo.get(bo);
        if (null == effectivePolicy) {
            EffectivePolicyImpl epi = createOutPolicyInfo();
            Assertor assertor = null;
            if (c instanceof Assertor) {
                assertor = (Assertor)c;
            }
            epi.initialise(ei, bo.getBindingOperation(), this, assertor, true);
            clientRequestInfo.put(bo, epi);
            effectivePolicy = epi;
        }
        return effectivePolicy;
    }

    public void setEffectiveClientRequestPolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                EffectivePolicy ep) {
        BindingOperation bo = new BindingOperation(ei, boi);
        clientRequestInfo.put(bo, ep);
    }

    public EffectivePolicy getEffectiveServerResponsePolicy(EndpointInfo ei, BindingOperationInfo boi,
                                                            Destination d) {
        BindingOperation bo = new BindingOperation(ei, boi);
        EffectivePolicy effectivePolicy = serverResponseInfo.get(bo);
        if (null == effectivePolicy) {
            EffectivePolicyImpl epi = createOutPolicyInfo();
            Assertor assertor = null;
            if (d instanceof Assertor) {
                assertor = (Assertor)d;
            }
            epi.initialise(ei, bo.getBindingOperation(), this, assertor, false);
            serverResponseInfo.put(bo, epi);
            effectivePolicy = epi;
        }
        return effectivePolicy;
    }

    public void setEffectiveServerResponsePolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                 EffectivePolicy ep) {
        BindingOperation bo = new BindingOperation(ei, boi);
        serverResponseInfo.put(bo, ep);
    }
  
    public EffectivePolicy getEffectiveServerFaultPolicy(EndpointInfo ei, BindingFaultInfo bfi, 
                                                         Destination d) {
        BindingFault bf = new BindingFault(ei, bfi);
        EffectivePolicy effectivePolicy = serverFaultInfo.get(bf);
        if (null == effectivePolicy) {
            EffectivePolicyImpl epi = createOutPolicyInfo();
            Assertor assertor = null;
            if (d instanceof Assertor) {
                assertor = (Assertor)d;
            }
            epi.initialise(ei, bfi, this, assertor);
            serverFaultInfo.put(bf, epi);
            effectivePolicy = epi;
        }
        return effectivePolicy;
    }

    public void setEffectiveServerFaultPolicy(EndpointInfo ei, BindingFaultInfo bfi, EffectivePolicy ep) {
        BindingFault bf = new BindingFault(ei, bfi);
        serverFaultInfo.put(bf, ep);
    }

    public EndpointPolicy getClientEndpointPolicy(EndpointInfo ei, Conduit conduit) {
        Assertor assertor = conduit instanceof Assertor ? (Assertor)conduit : null;
        return getEndpointPolicy(ei, clientEndpointInfo.get(ei), true, assertor);
    }
   
    public EndpointPolicy getServerEndpointPolicy(EndpointInfo ei, Destination destination) {
    
        Assertor assertor = destination instanceof Assertor ? (Assertor)destination : null;
        return getEndpointPolicy(ei, serverEndpointInfo.get(ei), false, assertor);
    }

    private EndpointPolicy getEndpointPolicy(
        EndpointInfo ei, EndpointPolicy ep, boolean isRequestor, Assertor assertor) {
        if (null != ep) {
            return ep; 
        }
        return createEndpointPolicyInfo(ei, isRequestor, assertor);
    }

    public void setClientEndpointPolicy(EndpointInfo ei, EndpointPolicy ep) {
        clientEndpointInfo.put(ei, ep);
    }

    public void setServerEndpointPolicy(EndpointInfo ei, EndpointPolicy ep) {
        serverEndpointInfo.put(ei, ep);
    }

    public EffectivePolicy getEffectiveServerRequestPolicy(EndpointInfo ei, BindingOperationInfo boi) {
        BindingOperation bo = new BindingOperation(ei, boi);
        EffectivePolicy effectivePolicy = serverRequestInfo.get(bo);
        if (null == effectivePolicy) {
            EffectivePolicyImpl epi = createOutPolicyInfo();
            epi.initialisePolicy(ei, bo.getBindingOperation(), this, false);
            serverRequestInfo.put(bo, epi);
            effectivePolicy = epi;
        }
        return effectivePolicy;
    }

    public void setEffectiveServerRequestPolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                EffectivePolicy ep) {
        BindingOperation bo = new BindingOperation(ei, boi);
        serverRequestInfo.put(bo, ep);
    }

    public EffectivePolicy getEffectiveClientResponsePolicy(EndpointInfo ei, BindingOperationInfo boi) {
        BindingOperation bo = new BindingOperation(ei, boi);
        EffectivePolicy effectivePolicy = clientResponseInfo.get(bo);
        if (null == effectivePolicy) {
            EffectivePolicyImpl epi = createOutPolicyInfo();
            epi.initialisePolicy(ei, bo.getBindingOperation(), this, true);        
            clientResponseInfo.put(bo, epi);
            effectivePolicy = epi;
        }
        return effectivePolicy;
    }

    public void setEffectiveClientResponsePolicy(EndpointInfo ei, BindingOperationInfo boi, 
                                                 EffectivePolicy ep) {
        BindingOperation bo = new BindingOperation(ei, boi);
        clientResponseInfo.put(bo, ep);
    }

    public EffectivePolicy getEffectiveClientFaultPolicy(EndpointInfo ei, BindingFaultInfo bfi) {
        BindingFault bf = new BindingFault(ei, bfi);
        EffectivePolicy effectivePolicy = clientFaultInfo.get(bf);
        if (null == effectivePolicy) {
            EffectivePolicyImpl epi = createOutPolicyInfo();
            epi.initialisePolicy(ei, bfi, this);
            clientFaultInfo.put(bf, epi);
            effectivePolicy = epi;
        }
        return effectivePolicy;
    }

    public void setEffectiveClientFaultPolicy(EndpointInfo ei, BindingFaultInfo bfi, EffectivePolicy ep) {
        BindingFault bf = new BindingFault(ei, bfi);
        clientFaultInfo.put(bf, ep);
    }

    // implementation

    protected final void init() {
    
        registry = new PolicyRegistryImpl();
    
        clientRequestInfo 
            = new ConcurrentHashMap<BindingOperation, EffectivePolicy>();

        clientResponseInfo 
            = new ConcurrentHashMap<BindingOperation, EffectivePolicy>();

        clientFaultInfo 
            = new ConcurrentHashMap<BindingFault, EffectivePolicy>();

        serverEndpointInfo 
            = new EndpointPolicyMap();
        clientEndpointInfo 
            = new EndpointPolicyMap();

        serverRequestInfo 
            = new ConcurrentHashMap<BindingOperation, EffectivePolicy>();

        serverResponseInfo 
            = new ConcurrentHashMap<BindingOperation, EffectivePolicy>();

        serverFaultInfo 
            = new ConcurrentHashMap<BindingFault, EffectivePolicy>();    
    }



    @PostConstruct
    public void addBusInterceptors() {
    
        if (null == alternativeSelector) {
            alternativeSelector = new MinimalAlternativeSelector();
        }
    
        if (null == bus || !enabled) {
            return;
        }
    
        AssertionBuilderRegistry abr = bus.getExtension(AssertionBuilderRegistry.class);
        if (null != abr) {
            abr.setIgnoreUnknownAssertions(ignoreUnknownAssertions);
        }

        ClientPolicyOutInterceptor clientOut = new ClientPolicyOutInterceptor();
        clientOut.setBus(bus);
        bus.getOutInterceptors().add(clientOut);
        ClientPolicyInInterceptor clientIn = new ClientPolicyInInterceptor();
        clientIn.setBus(bus);
        bus.getInInterceptors().add(clientIn);
        ClientPolicyInFaultInterceptor clientInFault = new ClientPolicyInFaultInterceptor();
        clientInFault.setBus(bus);
        bus.getInFaultInterceptors().add(clientInFault);
    
        ServerPolicyInInterceptor serverIn = new ServerPolicyInInterceptor();
        serverIn.setBus(bus);
        bus.getInInterceptors().add(serverIn);
        ServerPolicyOutInterceptor serverOut = new ServerPolicyOutInterceptor();
        serverOut.setBus(bus);
        bus.getOutInterceptors().add(serverOut);
        ServerPolicyOutFaultInterceptor serverOutFault = new ServerPolicyOutFaultInterceptor();
        serverOutFault.setBus(bus);
        bus.getOutFaultInterceptors().add(serverOutFault);
    
        PolicyVerificationOutInterceptor verifyOut = new PolicyVerificationOutInterceptor();
        verifyOut.setBus(bus);
        bus.getOutInterceptors().add(verifyOut);
        PolicyVerificationInInterceptor verifyIn = new PolicyVerificationInInterceptor();
        verifyIn.setBus(bus);
        bus.getInInterceptors().add(verifyIn);
        PolicyVerificationInFaultInterceptor verifyInFault = new PolicyVerificationInFaultInterceptor();
        verifyInFault.setBus(bus);
        bus.getInFaultInterceptors().add(verifyInFault);
    
        addedBusInterceptors = true;
    }  

    @PostConstruct
    public void registerListener() {
        ServerLifeCycleManager slm = bus.getExtension(ServerLifeCycleManager.class);
        if (slm != null) {
            slm.registerListener(this);
        }
    }

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

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

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

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

    Policy getAggregatedFaultPolicy(BindingFaultInfo bfi) {
        Policy aggregated = null;
        for (PolicyProvider pp : getPolicyProviders()) {
            Policy p = pp.getEffectivePolicy(bfi);
            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;
    }

    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;
    } 

    EndpointPolicyImpl createEndpointPolicyInfo(EndpointInfo ei, boolean isRequestor, Assertor assertor) {
        EndpointPolicyImpl epi = new EndpointPolicyImpl(ei, this, isRequestor, assertor);
        epi.initialize();
    
        Map<EndpointInfo, EndpointPolicy> map = isRequestor ? clientEndpointInfo : serverEndpointInfo;
        map.put(ei, epi);

        return epi;
    }


    /**
     * 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<Assertion> alternative, Assertor assertor) {
        PolicyInterceptorProviderRegistry pipr = bus.getExtension(PolicyInterceptorProviderRegistry.class);
        for (Assertion a : alternative) {
            if (!(a.isOptional() 
                || (null != pipr.get(a.getName())) 
                || (null != assertor && assertor.canAssert(a.getName())))) {
                return false;
            }
        }
        return true;
    }

    public void startServer(Server server) {
        // empty
    }

    /**
     * Callback recieved while the server side endpoint is being undeployed.
     *
     * @param server
     */
    public void stopServer(Server server) {
        EndpointInfo ei = server.getEndpoint().getEndpointInfo();
        serverEndpointInfo.remove(ei);

        /**
         * While cleaning up the entries of requestInfo's, responseInfo's and faultInfo's map, we create
         * a temperory Set with all the keys. Later we iterate over the keys of this temp set,
         * and if it's same as that of the endpoint being undeployed, we remove the corresponding
         * entries from the client and server maps.
         */

        cleanupBindingOperations(ei, new HashSet<BindingOperation>(clientRequestInfo.keySet()),
                clientRequestInfo);
        cleanupBindingOperations(ei, new HashSet<BindingOperation>(clientResponseInfo.keySet()),
                clientResponseInfo);

        cleanupBindingOperations(ei, new HashSet<BindingOperation>(serverRequestInfo.keySet()),
                serverRequestInfo);
        cleanupBindingOperations(ei, new HashSet<BindingOperation>(serverResponseInfo.keySet()),
                serverResponseInfo);

        cleanupBindingFaults(ei, new HashSet<BindingFault>(clientFaultInfo.keySet()),
                clientFaultInfo);
        cleanupBindingFaults(ei, new HashSet<BindingFault>(serverFaultInfo.keySet()),
                serverFaultInfo);
    }

    private void cleanupBindingOperations(EndpointInfo ei,
                                          Set<BindingOperation> bindingOperations,
                                          Map<BindingOperation, EffectivePolicy> originalMap) {
        
        Iterator<BindingOperation> bindingOpsItr = bindingOperations.iterator();
        while (bindingOpsItr.hasNext()) {
            BindingOperation bindingOperation = bindingOpsItr.next();
            if (ei.isSameAs(bindingOperation.getEndpoint())) {
                originalMap.remove(bindingOperation);
            }
        }
    }

    private void cleanupBindingFaults(EndpointInfo ei, Set <BindingFault> bindingFaults,
                                      Map<BindingFault, EffectivePolicy> originalMap) {
        Iterator<BindingFault> bindingFaultsItr = bindingFaults.iterator();
        while (bindingFaultsItr.hasNext()) {
            BindingFault bindingFault = bindingFaultsItr.next();
            if (ei.isSameAs(bindingFault.getEndpoint())) {
                originalMap.remove(bindingFault);
            }
        }
    }
    /**
     * Class used as key in the client request policy and server response policy maps.
     */
    class BindingOperation {
        private EndpointInfo ei;
        private BindingOperationInfo boi;
    
        BindingOperation(EndpointInfo e, BindingOperationInfo b) {
            ei = e;
            boi = b.isUnwrapped() ? b.getWrappedOperation() : b;
        }
    
        EndpointInfo getEndpoint() {
            return ei;
        }
    
        BindingOperationInfo getBindingOperation() {
            return boi;
        }

        @Override
        public int hashCode() {
            return boi.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            BindingOperation other = (BindingOperation)obj;
            return boi.equals(other.boi) && ei.equals(other.ei);
        }
    
        @Override
        public String toString() {
            return ei.getName().toString() + "." + boi.getName().toString();
        }
    
    
    }

    /**
     * Class used as key in the server fault policy map.
     */
    class BindingFault {
        private EndpointInfo ei;
        private BindingFaultInfo bfi;
    
        BindingFault(EndpointInfo e, BindingFaultInfo b) {
            ei = e;
            bfi = b;
        }
    
        EndpointInfo getEndpoint() {
            return ei;
        }
    
        BindingFaultInfo getBindingFault() {
            return bfi;
        }
    
        @Override
        public int hashCode() {
            return bfi.hashCode();
        }
    
        @Override
        public boolean equals(Object obj) {
            BindingFault other = (BindingFault)obj;
            return bfi.equals(other.bfi) && ei.equals(other.ei);
        }
    
        @Override
        public String toString() {
            return ei.getName().toString() + "." + bfi.getFaultInfo().toString();
        }
    }

    // for test

    EffectivePolicyImpl createOutPolicyInfo() {
        return new EffectivePolicyImpl();
    }
    
    private class EndpointPolicyMap extends ConcurrentHashMap<EndpointInfo, EndpointPolicy> {
        public EndpointPolicy remove(Object key) {
            EndpointInfo toRemove = (EndpointInfo) key;
            EndpointPolicy pol = null;
            for (EndpointInfo info : keySet()) {
                if (info.isSameAs(toRemove)) {
                    pol = super.remove(info);
                }
            }
            return pol;
        }
    }


}
