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

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Logger;

import javax.wsdl.Definition;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.UnknownExtensibilityElement;
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.common.util.StringUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.service.model.AbstractDescriptionElement;
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.DescriptionInfo;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.service.model.Extensible;
import org.apache.cxf.service.model.FaultInfo;
import org.apache.cxf.service.model.MessageInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.ws.policy.PolicyConstants;
import org.apache.cxf.ws.policy.PolicyProvider;
import org.apache.cxf.ws.policy.attachment.AbstractPolicyProvider;
import org.apache.cxf.ws.policy.attachment.reference.LocalServiceModelReferenceResolver;
import org.apache.cxf.ws.policy.attachment.reference.ReferenceResolver;
import org.apache.cxf.wsdl11.WSDLServiceBuilder;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyReference;

/**
 * PolicyAttachmentManager provides methods to retrieve element policies and
 * calculate effective policies based on the policy subject's scope.
 * 
 */
@NoJSR250Annotations
public class Wsdl11AttachmentPolicyProvider extends AbstractPolicyProvider 
    implements PolicyProvider {
    private static final Logger LOG = LogUtils.getL7dLogger(Wsdl11AttachmentPolicyProvider.class);

    public Wsdl11AttachmentPolicyProvider() {
        this(null);
    }
    
    public Wsdl11AttachmentPolicyProvider(Bus bus) {
        super(bus);
    }  
    
    public Policy getEffectivePolicy(ServiceInfo si) {
        return getElementPolicy(si);
    }
    
    private Policy mergePolicies(Policy p1, Policy p2) {
        if (p1 == null) {
            return p2;
        } else if (p2 == null) {
            return p1;
        }
        return p1.merge(p2);
    }
    
    /**
     * The effective policy for a WSDL endpoint policy subject includes the element policy of the 
     * wsdl11:port element that defines the endpoint merged with the element policy of the
     * referenced wsdl11:binding element and the element policy of the referenced wsdl11:portType
     * element that defines the interface of the endpoint. 
     * 
     * @param ei the EndpointInfo object identifying the endpoint
     * @return the effective policy
     */
    public Policy getEffectivePolicy(EndpointInfo ei) {
        Policy p = getElementPolicy(ei);
        p = mergePolicies(p, getElementPolicy(ei.getBinding()));
        p = mergePolicies(p, getElementPolicy(ei.getInterface(), true));
        
        return p;
    }

    /**
     * The effective policy for a WSDL operation policy subject is calculated in relation to a 
     * specific port, and includes the element policy of the wsdl11:portType/wsdl11:operation 
     * element that defines the operation merged with that of the corresponding 
     * wsdl11:binding/wsdl11:operation element.
     * 
     * @param bi the BindingOperationInfo identifying the operation in relation to a port
     * @return the effective policy
     */
    public Policy getEffectivePolicy(BindingOperationInfo bi) {
        DescriptionInfo di = bi.getBinding().getDescription();
        Policy p = getElementPolicy(bi, false, di);
        p = mergePolicies(p, getElementPolicy(bi.getOperationInfo(), false, di));
        return p;
    }
    
    /**
     * The effective policy for a specific WSDL message (input or output) is calculated
     * in relation to a specific port, and includes the element policy of the wsdl:message
     * element that defines the message's type merged with the element policy of the 
     * wsdl11:binding and wsdl11:portType message definitions that describe the message.
     * For example, the effective policy of a specific input message for a specific port
     * would be the (element policies of the) wsdl11:message element defining the message type,
     * the wsdl11:portType/wsdl11:operation/wsdl11:input element and the corresponding
     * wsdl11:binding/wsdl11:operation/wsdl11:input element for that message.
     * 
     * @param bmi the BindingMessageInfo identifiying the message
     * @return the effective policy
     */
    public Policy getEffectivePolicy(BindingMessageInfo bmi) {
        ServiceInfo si = bmi.getBindingOperation().getBinding().getService();
        DescriptionInfo di = si.getDescription();
        Policy p = getElementPolicy(bmi, false, di);
        MessageInfo mi = bmi.getMessageInfo();
        p = mergePolicies(p, getElementPolicy(mi, true, di));
        Extensible ex = getMessageTypeInfo(mi.getName(), di);
        p = mergePolicies(p, getElementPolicy(ex, false, di));

        return p;
    }
    

    
    public Policy getEffectivePolicy(BindingFaultInfo bfi) {
        ServiceInfo si = bfi.getBindingOperation().getBinding().getService();
        DescriptionInfo di = si.getDescription();

        Policy p = getElementPolicy(bfi, false, di);
        FaultInfo fi = bfi.getFaultInfo();
        p = mergePolicies(p, getElementPolicy(fi, true, di));
        Extensible ex = getMessageTypeInfo(fi.getName(), di);
        p = mergePolicies(p, getElementPolicy(ex, false, di));

        return p;
    }
    
    Policy getElementPolicy(AbstractDescriptionElement adh) {
        return getElementPolicy(adh, false);
    }
    
    Policy getElementPolicy(AbstractDescriptionElement adh, boolean includeAttributes) {
        if (adh == null) {
            return null;
        }
        return getElementPolicy(adh, includeAttributes, adh.getDescription());
    }
  
    Policy getElementPolicy(Extensible ex, boolean includeAttributes, DescriptionInfo di) {
        if (null == ex || null == di) {
            return null;
        }
        
        if (di.getProperty("registeredPolicy") == null) {
            List<UnknownExtensibilityElement> diext = 
                di.getExtensors(UnknownExtensibilityElement.class);
            if (diext != null) {
                for (UnknownExtensibilityElement e : diext) {
                    String uri = e.getElement().getAttributeNS(PolicyConstants.WSU_NAMESPACE_URI,
                                                  PolicyConstants.WSU_ID_ATTR_NAME);
                    
                    if (PolicyConstants.isPolicyElem(e.getElementType())
                        && !StringUtils.isEmpty(uri)) {
                        try {
                            Policy policy = builder.getPolicy(e.getElement());
                            String fragement = "#" + uri;
                            registry.register(fragement, policy);
                            registry.register(di.getBaseURI() + fragement, policy);
                        } catch (Exception policyEx) {
                            //ignore the policy can not be built
                            LOG.warning("Failed to build the policy '" + uri + "':" + policyEx.getMessage());
                        }
                    }
                }
            }
            di.setProperty("registeredPolicy", true);
        }
        
        Policy elementPolicy = null;
        List<UnknownExtensibilityElement> extensions = 
            ex.getExtensors(UnknownExtensibilityElement.class);
        
        if (null != extensions) {
            for (UnknownExtensibilityElement e : extensions) {
                Policy p = null;
                if (PolicyConstants.isPolicyElem(e.getElementType())) {
                    p = builder.getPolicy(e.getElement());                    

                } else if (PolicyConstants.isPolicyRefElem(e.getElementType())) {
                    PolicyReference ref = builder.getPolicyReference(e.getElement());
                    if (null != ref) {
                        p = resolveReference(ref, di);
                    }
                }
                if (null != p) {
                    if (elementPolicy == null) {
                        elementPolicy = new Policy();
                    }
                    elementPolicy = elementPolicy.merge(p);
                }
            }
        }
        
        if (includeAttributes && ex.getExtensionAttributes() != null) {
            for (Map.Entry<QName, Object> ent : ex.getExtensionAttributes().entrySet()) {
                if (PolicyConstants.isPolicyURIsAttr(ent.getKey())) {
                    Object attr = ent.getValue();
                    // can be of type a String, a QName, a list of Srings or a list of QNames
                    String uris = null;
                    if (attr instanceof QName) {
                        uris = ((QName)attr).getLocalPart();
                    } else if (attr instanceof String) {
                        uris = (String)attr;
                    }
                    if (null != uris) {
                        StringTokenizer st = new StringTokenizer(uris);
                        while (st.hasMoreTokens()) {
                            String uri = st.nextToken();
                            PolicyReference ref = new PolicyReference();
                            ref.setURI(uri);
                            Policy p = resolveReference(ref, di);
                            if (null != p) {
                                elementPolicy = elementPolicy == null 
                                    ? new Policy().merge(p) : elementPolicy.merge(p);
                            }
                        }
                    }
                }
            }
        }

        return elementPolicy;
    }
    
    Policy resolveReference(PolicyReference ref, DescriptionInfo di) {
        Policy p = null;
        if (isExternal(ref)) {
            p = resolveExternal(ref, di.getBaseURI());
        } else {
            p = resolveLocal(ref, di);
        }
        checkResolved(ref, p);
        return p;
    }
    
    Policy resolveLocal(PolicyReference ref, DescriptionInfo di) {
        String uri = ref.getURI().substring(1);
        String absoluteURI = di.getBaseURI() + ref.getURI();
        Policy resolved = registry.lookup(absoluteURI);
        if (null != resolved) {
            return resolved;
        }
        
        ReferenceResolver resolver = new LocalServiceModelReferenceResolver(di, builder);
        resolved = resolver.resolveReference(uri);
        if (null != resolved) {
            ref.setURI(absoluteURI);
            registry.register(absoluteURI, resolved);
        }
        return resolved;
    }
    
     
    private Extensible getMessageTypeInfo(QName name, DescriptionInfo di) {
        if (null == di) {
            return null;
        }
        Definition def = (Definition)di.getProperty(WSDLServiceBuilder.WSDL_DEFINITION);
        if (null == def) {
            return null;
        }
        
        javax.wsdl.Message m = def.getMessage(name);
        if (null != m) {
            List<ExtensibilityElement> extensors = 
                CastUtils.cast(m.getExtensibilityElements(), ExtensibilityElement.class);
            if (null != extensors) {
                return new ExtensibleInfo(extensors);
            }
        }
        return null;
    }
     
    private class ExtensibleInfo implements Extensible {
        private List<ExtensibilityElement> extensors;
        ExtensibleInfo(List<ExtensibilityElement> e) {
            extensors = e;
        }
        
        public <T> T getExtensor(Class<T> cls) {
            for (ExtensibilityElement e : extensors) {
                if (cls.isInstance(e)) {
                    return cls.cast(e);
                }
            }
            return null;
        }
        public <T> List<T> getExtensors(Class<T> cls) {
            if (null == extensors) {
                return null;
            }
            
            List<T> list = new ArrayList<T>(extensors.size());
            for (ExtensibilityElement e : extensors) {
                if (cls.isInstance(e)) {
                    list.add(cls.cast(e));
                }
            }
            return list;
        }

        public void addExtensionAttribute(QName arg0, Object arg1) {    
        }

        public void addExtensor(Object arg0) {   
        }

        public Object getExtensionAttribute(QName arg0) {
            return null;
        }

        public Map<QName, Object> getExtensionAttributes() {
            return null;
        }

        public void setExtensionAttributes(Map<QName, Object> arg0) {  
        }
        
        
        
    } 
   

}
