/**
 * 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.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.wsdl.extensions.UnknownExtensibilityElement;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import org.xml.sax.InputSource;

import org.apache.cxf.Bus;
import org.apache.cxf.annotations.Policies;
import org.apache.cxf.annotations.Policy;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.configuration.ConfiguredBeanLocator;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.resource.ExtendedURIResolver;
import org.apache.cxf.service.factory.AbstractServiceFactoryBean;
import org.apache.cxf.service.factory.FactoryBeanListener;
import org.apache.cxf.service.model.AbstractPropertiesHolder;
import org.apache.cxf.service.model.BindingFaultInfo;
import org.apache.cxf.service.model.BindingInfo;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.DescriptionInfo;
import org.apache.cxf.service.model.FaultInfo;
import org.apache.cxf.service.model.InterfaceInfo;
import org.apache.cxf.service.model.OperationInfo;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.staxutils.StaxUtils;
import org.apache.neethi.Constants;

/**
 * 
 */
public class PolicyAnnotationListener implements FactoryBeanListener {
    private static final Logger LOG = LogUtils.getL7dLogger(PolicyAnnotationListener.class);
    private static final String EXTRA_POLICIES = PolicyAnnotationListener.class.getName() + ".EXTRA_POLICIES";
    
    private Bus bus;
    
    public PolicyAnnotationListener(Bus bus) {
        this.bus = bus;
    }
    
    public void handleEvent(Event ev, AbstractServiceFactoryBean factory, Object... args) {
        switch (ev) {
        case INTERFACE_CREATED: {
            InterfaceInfo ii = (InterfaceInfo)args[0];
            Class<?> cls = (Class<?>)args[1];
            addPolicies(factory, ii, cls);
            break;
        }
        
        case ENDPOINT_SELECTED: {
            Class<?> cls = (Class<?>)args[2];
            Class<?> implCls = (Class<?>)args[3];
            Endpoint ep = (Endpoint)args[1];
            if (ep.getEndpointInfo().getInterface() != null) {
                addPolicies(factory, ep, cls);
                
                // this will allow us to support annotations in Implementations, but only for
                // class level annotations.  Method level annotations are not currently supported
                // for implementations.  The call has been moved here so that the ServiceInfo
                // policy stuff is loaded before jaxws factory calls the PolicyEngineImpl
                addEndpointImplPolicies(factory, ep, implCls);
            }
            break;
        }
        
        case INTERFACE_OPERATION_BOUND: {
            OperationInfo inf = (OperationInfo)args[0];
            Method m = (Method)args[1];
            addPolicies(factory, inf, m);
            break;
        }
        
        case BINDING_OPERATION_CREATED:
            BindingOperationInfo boi = (BindingOperationInfo) args[1];
            Method m = (Method)args[2];
            addPolicies(factory, boi.getOperationInfo(), m);
            break;
            
        default:
            //ignore
        }
    }

    private void addPolicies(AbstractServiceFactoryBean factory, OperationInfo inf, Method m) {
        if (m == null) {
            return;
        }
        
        Policy p = m.getAnnotation(Policy.class);
        Policies ps = m.getAnnotation(Policies.class);
        if (p != null || ps != null) {
            List<Policy> list = new ArrayList<Policy>();
            if (p != null) {
                list.add(p);
            }
            if (ps != null) {
                list.addAll(Arrays.asList(ps.value()));
            }
            ListIterator<Policy> it = list.listIterator();
            while (it.hasNext()) {
                p = it.next();
                Policy.Placement place = p.placement();
                if (place == Policy.Placement.DEFAULT) {
                    place = Policy.Placement.BINDING_OPERATION;
                }
                ServiceInfo service = inf.getInterface().getService();
                Class<?> cls = m.getDeclaringClass();
                switch (place) {
                case PORT_TYPE_OPERATION:
                    addPolicy(inf, service, p, cls, 
                              inf.getName().getLocalPart() + "PortTypeOpPolicy");
                    it.remove();
                    break;
                case PORT_TYPE_OPERATION_INPUT:
                    addPolicy(inf.getInput(), service, p, cls, 
                              inf.getName().getLocalPart() + "PortTypeOpInputPolicy");
                    it.remove();
                    break;
                case PORT_TYPE_OPERATION_OUTPUT:
                    addPolicy(inf.getOutput(), service, p, cls, 
                              inf.getName().getLocalPart() + "PortTypeOpOutputPolicy");
                    it.remove();
                    break;
                case PORT_TYPE_OPERATION_FAULT: {
                    for (FaultInfo f : inf.getFaults()) {
                        if (p.faultClass().equals(f.getProperty(Class.class.getName()))) {
                            addPolicy(f, service, p, cls, 
                                      f.getName().getLocalPart() + "PortTypeOpFaultPolicy");
                            it.remove();
                        }
                    }
                    break;
                }
                default:
                    //nothing
                }
            }
            
            if (!list.isEmpty()) {
                List<Policy> stuff = CastUtils.cast((List<?>)inf.getProperty(EXTRA_POLICIES));
                if (stuff != null) {
                    stuff.addAll(list);
                } else {
                    inf.setProperty(EXTRA_POLICIES, list);
                }
            }
        }        
    }

    private void addPolicies(AbstractServiceFactoryBean factory, Endpoint ep, Class<?> cls) {
        List<Policy> list = CastUtils.cast((List<?>)ep.getEndpointInfo()
                                           .getInterface().removeProperty(EXTRA_POLICIES));
        if (list != null) {
            addPolicies(factory, ep, cls, list, Policy.Placement.BINDING);
        }
        
        ServiceInfo service = ep.getService().getServiceInfos().get(0);
        for (BindingOperationInfo binfo : ep.getBinding().getBindingInfo().getOperations()) {
            List<Policy> later = CastUtils.cast((List<?>)binfo.getOperationInfo()
                                                           .removeProperty(EXTRA_POLICIES));
            if (later != null) {
                for (Policy p : later) {
                    switch (p.placement()) {
                    case DEFAULT:
                    case BINDING_OPERATION:
                        addPolicy(binfo, service, p, cls, 
                                  binfo.getName().getLocalPart() + "BindingOpPolicy");
                        break;
                    case BINDING_OPERATION_INPUT:
                        addPolicy(binfo.getInput(), service, p, cls, 
                                  binfo.getName().getLocalPart() + "BindingOpInputPolicy");
                        break;
                    case BINDING_OPERATION_OUTPUT:
                        addPolicy(binfo.getOutput(), service, p, cls, 
                                  binfo.getName().getLocalPart() + "BindingOpOutputPolicy");
                        break;
                    case BINDING_OPERATION_FAULT: {
                        for (BindingFaultInfo f : binfo.getFaults()) {
                            if (p.faultClass().equals(f.getFaultInfo()
                                                        .getProperty(Class.class.getName()))) {
                                addPolicy(f, service, p, cls, 
                                          f.getFaultInfo().getName().getLocalPart() + "BindingOpFaultPolicy");
                            }
                        }
                        break;
                    }
                    default:
                        //nothing
                    }
                }
            }
        }
    }
    
    private void addEndpointImplPolicies(AbstractServiceFactoryBean factory, Endpoint endpoint, Class<?> cls) {
        List<Policy> list = CastUtils.cast((List<?>)endpoint.getEndpointInfo()
                                           .getInterface().removeProperty(EXTRA_POLICIES));
        if (list != null) {
            addPolicies(factory, endpoint, cls, list, Policy.Placement.BINDING);
        }
        if (cls == null) {
            return;
        }
        
        Policy p = cls.getAnnotation(Policy.class);
        Policies ps = cls.getAnnotation(Policies.class);
        if (p != null || ps != null) {
            list = new ArrayList<Policy>();
            if (p != null) {
                list.add(p);
            }
            if (ps != null) {
                list.addAll(Arrays.asList(ps.value()));
            }
            addPolicies(factory, endpoint, cls, list, Policy.Placement.SERVICE);
        }        
    }

    private void addPolicies(AbstractServiceFactoryBean factory, Endpoint endpoint, Class<?> cls,
                             List<Policy> list, Policy.Placement defaultPlace) {
        ListIterator<Policy> it = list.listIterator();
        InterfaceInfo inf = endpoint.getEndpointInfo().getInterface();
        BindingInfo binf = endpoint.getBinding().getBindingInfo();
        ServiceInfo si = endpoint.getService().getServiceInfos().get(0);
        while (it.hasNext()) {
            Policy p = it.next();
            Policy.Placement place = p.placement();
            if (place == Policy.Placement.DEFAULT) {
                place = defaultPlace;
            }
            switch (place) {
            case PORT_TYPE: {
                addPolicy(inf, si, p, cls, 
                          inf.getName().getLocalPart() + "PortTypePolicy");
                it.remove();
                break;
            }
            case BINDING: {
                addPolicy(binf, si, p, cls, 
                          binf.getName().getLocalPart() + "BindingPolicy");
                it.remove();
                break;
            }
            case SERVICE: {
                addPolicy(si, si, p, cls, 
                          si.getName().getLocalPart() + "ServicePolicy");
                it.remove();
                break;
            }
            case SERVICE_PORT: {
                addPolicy(endpoint.getEndpointInfo(), si, p, cls, 
                          endpoint.getEndpointInfo().getName().getLocalPart() + "PortPolicy");
                it.remove();
                break;
            }
            default:
            }

        }
    }


    private void addPolicies(AbstractServiceFactoryBean factory, InterfaceInfo ii, Class<?> cls) {
        if (cls == null) {
            return;
        }
        Policy p = cls.getAnnotation(Policy.class);
        Policies ps = cls.getAnnotation(Policies.class);
        if (p != null || ps != null) {
            List<Policy> list = new ArrayList<Policy>();
            if (p != null) {
                list.add(p);
            }
            if (ps != null) {
                list.addAll(Arrays.asList(ps.value()));
            }
            ListIterator<Policy> it = list.listIterator();
            while (it.hasNext()) {
                p = it.next();
                Policy.Placement place = p.placement();
                if (place == Policy.Placement.DEFAULT) {
                    place = Policy.Placement.BINDING;
                }
                switch (place) {
                case PORT_TYPE: {
                    addPolicy(ii, ii.getService(), p, cls, 
                              ii.getName().getLocalPart() + "PortTypePolicy");
                    it.remove();
                    break;
                }
                case SERVICE: {
                    addPolicy(ii.getService(),
                              ii.getService(),
                              p, cls, 
                              ii.getService().getName().getLocalPart() + "ServicePolicy");
                    it.remove();
                    break;
                }
                default:
                }
            }
            
            if (!list.isEmpty()) {
                List<Policy> stuff = CastUtils.cast((List<?>)ii.getProperty(EXTRA_POLICIES));
                if (stuff != null) {
                    stuff.addAll(list);
                } else {
                    ii.setProperty(EXTRA_POLICIES, list);
                }
            }
        }
    }

    private void addPolicy(AbstractPropertiesHolder place,
                           ServiceInfo service, 
                           Policy p,
                           Class<?> cls,
                           String defName) {
        Element el = addPolicy(service, p, cls, defName);
        if (el != null) {
            UnknownExtensibilityElement uee = new UnknownExtensibilityElement();
            uee.setElement(el);
            uee.setRequired(true);
            uee.setElementType(DOMUtils.getElementQName(el));
            place.addExtensor(uee);
        }
    }
    private Element addPolicy(ServiceInfo service, Policy p, Class<?> cls, String defName) {
        String uri = p.uri();
        String ns = Constants.URI_POLICY_NS;
        
        if (p.includeInWSDL()) {
            Element element = loadPolicy(uri, defName);
            if (element == null) {
                return null;
            }
            
            // might have been updated on load policy
            uri = getPolicyId(element);
            ns = element.getNamespaceURI();
            
            if (service.getDescription() == null && cls != null) {
                service.setDescription(new DescriptionInfo());
                URL u = cls.getResource("/");
                if (u != null) {
                    service.getDescription().setBaseURI(u.toString());
                }
            }

            // if not already added to service add it, otherwise ignore 
            // and just create the policy reference.
            if (!isExistsPolicy(service, uri)) {
                UnknownExtensibilityElement uee = new UnknownExtensibilityElement();
                uee.setElement(element);
                uee.setRequired(true);
                uee.setElementType(DOMUtils.getElementQName(element));
                service.getDescription().addExtensor(uee);
            }
            
            uri = "#" + uri;
        }
        
        Document doc = DOMUtils.createDocument();
        Element el = doc.createElementNS(ns, "wsp:" + Constants.ELEM_POLICY_REF);
        Attr att = doc.createAttributeNS(null, "URI");
        att.setValue(uri);
        el.setAttributeNodeNS(att);
        return el;
    }

    private String getPolicyId(Element element) {
        return element.getAttributeNS(PolicyConstants.WSU_NAMESPACE_URI,
                                     PolicyConstants.WSU_ID_ATTR_NAME);
    }
    
    private boolean isExistsPolicy(ServiceInfo service, String uri) {
        Object exts[] = service.getDescription().getExtensors().get();
        exts = exts == null ? new Object[0] : exts;
        for (Object o : exts) {
            if (o instanceof UnknownExtensibilityElement) {
                UnknownExtensibilityElement uee = (UnknownExtensibilityElement)o;
                String uri2 = getPolicyId(uee.getElement());
                if (uri.equals(uri2)) {
                    return true;
                }
            }
        }
        return false;
    }

    private Element loadPolicy(String uri, String defName) {
        if (!uri.startsWith("#")) {
            return loadRemotePolicy(uri, defName);
        } else {
            return loadLocalPolicy(uri);
        }
    }
        
    private Element loadRemotePolicy(String uri, String defName) {
        ExtendedURIResolver resolver = new ExtendedURIResolver();
        InputSource src = resolver.resolve(uri, "classpath:");
        
        if (null == src) {
            return null;
        }
        XMLStreamReader reader = null;
        try {
            reader = StaxUtils.createXMLStreamReader(src);
            Document doc = StaxUtils.read(reader);
            uri = getPolicyId(doc.getDocumentElement());
            if (StringUtils.isEmpty(uri)) {
                Attr att = doc.createAttributeNS(PolicyConstants.WSU_NAMESPACE_URI,
                                                 "wsu:" + PolicyConstants.WSU_ID_ATTR_NAME);
                att.setNodeValue(defName);
                doc.getDocumentElement().setAttributeNodeNS(att);
            }
            
            return doc.getDocumentElement();
        } catch (XMLStreamException e) {
            LOG.log(Level.WARNING, e.getMessage());
            return null;
        } finally {
            StaxUtils.close(reader);
        }
    }
    
    private Element loadLocalPolicy(String uri) {
        PolicyBean pb = bus.getExtension(ConfiguredBeanLocator.class)
            .getBeanOfType(uri.substring(1), PolicyBean.class);
        if (null != pb) {
            return pb.getElement(); 
        } else {
            return null;
        }
    }
}
