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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import org.apache.cxf.Bus;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.databinding.DataReader;
import org.apache.cxf.databinding.DataWriter;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.interceptor.InterceptorChain;
import org.apache.cxf.interceptor.InterceptorProvider;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageContentsList;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.service.model.FaultInfo;
import org.apache.cxf.service.model.MessageInfo;
import org.apache.cxf.service.model.MessagePartInfo;
import org.apache.cxf.service.model.OperationInfo;
import org.apache.cxf.staxutils.StaxUtils;

public final class ColocUtil {
    private static final Logger LOG = LogUtils.getL7dLogger(ColocUtil.class);

    private ColocUtil() {
        //Completge
    }

    public static void setPhases(SortedSet<Phase> list, String start, String end) {
        Phase startPhase = new Phase(start, 1);
        Phase endPhase = new Phase(end, 2);
        Iterator<Phase> iter = list.iterator();
        boolean remove = true;
        while (iter.hasNext()) {
            Phase p = iter.next();
            if (remove 
                && p.getName().equals(startPhase.getName())) {
                remove = false;
            } else if (p.getName().equals(endPhase.getName())) {
                remove = true;
            } else if (remove) {
                iter.remove();
            }
        }
    }
    
    public static InterceptorChain getOutInterceptorChain(Exchange ex, SortedSet<Phase> phases) {
        Bus bus = ex.get(Bus.class);
        PhaseInterceptorChain chain = new PhaseInterceptorChain(phases);
        
        Endpoint ep = ex.get(Endpoint.class);
        List<Interceptor<? extends Message>> il = ep.getOutInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by endpoint: " + il);
        }
        chain.add(il);
        il = ep.getService().getOutInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by service: " + il);
        }
        chain.add(il);
        il = bus.getOutInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by bus: " + il);
        }
        chain.add(il);
        
        if (ep.getService().getDataBinding() instanceof InterceptorProvider) {
            il = ((InterceptorProvider)ep.getService().getDataBinding()).getOutInterceptors();
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Interceptors contributed by databinding: " + il);
            }
            chain.add(il);
        }
        modifyChain(chain, ex, false);

        return chain;
    }
    
    public static InterceptorChain getInInterceptorChain(Exchange ex, SortedSet<Phase> phases) {
        Bus bus = ex.get(Bus.class);
        PhaseInterceptorChain chain = new PhaseInterceptorChain(phases);
        
        Endpoint ep = ex.get(Endpoint.class);
        List<Interceptor<? extends Message>> il = ep.getInInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by endpoint: " + il);
        }
        chain.add(il);
        il = ep.getService().getInInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by service: " + il);
        }
        chain.add(il);
        il = bus.getInInterceptors();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Interceptors contributed by bus: " + il);
        }
        chain.add(il);
        
        if (ep.getService().getDataBinding() instanceof InterceptorProvider) {
            il = ((InterceptorProvider)ep.getService().getDataBinding()).getInInterceptors();
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Interceptors contributed by databinding: " + il);
            }
            chain.add(il);
        }
        chain.setFaultObserver(new ColocOutFaultObserver(bus));
        modifyChain(chain, ex, true);
        return chain;
    }    
    private static void modifyChain(PhaseInterceptorChain chain, Exchange ex, boolean in) {
        modifyChain(chain, ex.getInMessage(), in);
        modifyChain(chain, ex.getOutMessage(), in);
    }
    private static void modifyChain(PhaseInterceptorChain chain, Message m, boolean in) {
        if (m == null) {
            return;
        }
        Collection<InterceptorProvider> providers 
            = CastUtils.cast((Collection<?>)m.get(Message.INTERCEPTOR_PROVIDERS));
        if (providers != null) {
            for (InterceptorProvider p : providers) {
                if (in) {
                    chain.add(p.getInInterceptors());
                } else {
                    chain.add(p.getOutInterceptors());
                }
            }
        }
        String key = in ? Message.IN_INTERCEPTORS : Message.OUT_INTERCEPTORS;
        Collection<Interceptor<? extends Message>> is 
            = CastUtils.cast((Collection<?>)m.get(key));
        if (is != null) {
            chain.add(is);
        }
    }

    public static boolean isSameOperationInfo(OperationInfo oi1, OperationInfo oi2) {
        return  oi1.getName().equals(oi2.getName())
                && isSameMessageInfo(oi1.getInput(), oi2.getInput())
                && isSameMessageInfo(oi1.getOutput(), oi2.getOutput())
                && isSameFaultInfo(oi1.getFaults(), oi2.getFaults());
    }
    
    public static boolean isCompatibleOperationInfo(OperationInfo oi1, OperationInfo oi2) {
        return isSameOperationInfo(oi1, oi2)
               || isAssignableOperationInfo(oi1, Source.class) 
               || isAssignableOperationInfo(oi2, Source.class);
    }
    
    public static boolean isAssignableOperationInfo(OperationInfo oi, Class<?> cls) {
        MessageInfo mi = oi.getInput();
        List<MessagePartInfo> mpis = mi.getMessageParts();
        return mpis.size() == 1 && cls.isAssignableFrom(mpis.get(0).getTypeClass());
    }
    
    public static boolean isSameMessageInfo(MessageInfo mi1, MessageInfo mi2) {
        if ((mi1 == null && mi2 != null)
            || (mi1 != null && mi2 == null)) {
            return false;
        }
        
        if (mi1 != null && mi2 != null) {
            List<MessagePartInfo> mpil1 = mi1.getMessageParts();
            List<MessagePartInfo> mpil2 = mi2.getMessageParts();
            if (mpil1.size() != mpil2.size()) {
                return false;
            }
            int idx = 0;
            for (MessagePartInfo mpi1 : mpil1) {
                MessagePartInfo mpi2 = mpil2.get(idx);
                if (!mpi1.getTypeClass().equals(mpi2.getTypeClass())) {
                    return false;
                }
                ++idx;
            }
        }
        return true;
    }
    
    public static boolean isSameFaultInfo(Collection<FaultInfo> fil1, 
                                          Collection<FaultInfo> fil2) {
        if ((fil1 == null && fil2 != null)
            || (fil1 != null && fil2 == null)) {
            return false;
        }
        
        if (fil1 != null && fil2 != null) {
            if (fil1.size() != fil2.size()) {
                return false;
            }
            for (FaultInfo fi1 : fil1) {
                Iterator<FaultInfo> iter = fil2.iterator();
                Class<?> fiClass1 = fi1.getProperty(Class.class.getName(), 
                                                    Class.class);
                boolean match = false;
                while (iter.hasNext()) {
                    FaultInfo fi2 = iter.next();
                    Class<?> fiClass2 = fi2.getProperty(Class.class.getName(), 
                                                        Class.class);
                    //Sender/Receiver Service Model not same for faults wr.t message names.
                    //So Compare Exception Class Instance.
                    if (fiClass1.equals(fiClass2)) {
                        match = true;
                        break;
                    }
                }
                if (!match) {
                    return false;        
                }
            }
        }
        return true;
    }
    
    public static void convertSourceToObject(Message message) {
        List<Object> content = CastUtils.cast(message.getContent(List.class));
        if (content == null || content.size() < 1) {
            // nothing to convert
            return;
        }
        // only supporting the wrapped style for now  (one pojo <-> one source)
        Source source = (Source)content.get(0);
        DataReader<XMLStreamReader> reader =
            message.getExchange().getService().getDataBinding().createReader(XMLStreamReader.class);
        MessagePartInfo mpi = getMessageInfo(message).getMessagePart(0);
        XMLStreamReader streamReader = null;
        Object wrappedObject = null;
        try {
            streamReader = StaxUtils.createXMLStreamReader(source);
            wrappedObject = reader.read(mpi, streamReader);
        } finally {
            try {
                StaxUtils.close(streamReader);
            } catch (XMLStreamException e) {
                // Ignore
            }
        }
        MessageContentsList parameters = new MessageContentsList();
        parameters.put(mpi, wrappedObject);

        message.setContent(List.class, parameters);
    }

    public static void convertObjectToSource(Message message) {
        List<Object> content = CastUtils.cast(message.getContent(List.class));
        if (content == null || content.size() < 1) {
            // nothing to convert
            return;
        }
        // only supporting the wrapped style for now  (one pojo <-> one source)
        Object object = content.get(0);
        DataWriter<OutputStream> writer = 
            message.getExchange().getService().getDataBinding().createWriter(OutputStream.class);
        //TODO use a better conversion method to get a Source from a pojo.
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        writer.write(object, bos);

        content.set(0, new StreamSource(new ByteArrayInputStream(bos.toByteArray())));
    }
    
    private static MessageInfo getMessageInfo(Message message) {
        OperationInfo oi = message.getExchange().get(OperationInfo.class);
        if (MessageUtils.isOutbound(message)) {
            return oi.getOutput();
        } else {
            return oi.getInput();
        }
    }
}
