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

import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.continuations.SuspendedInvocationException;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.interceptor.InterceptorChain;
import org.apache.cxf.interceptor.ServiceInvokerInterceptor;
import org.apache.cxf.logging.FaultListener;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.FaultMode;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.service.Service;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.OperationInfo;
import org.apache.cxf.transport.MessageObserver;

/**
 * A PhaseInterceptorChain orders Interceptors according to the phase they
 * participate in and also according to the before & after properties on an
 * Interceptor.
 * <p>
 * A List of phases is supplied to the PhaseInterceptorChain in the constructor.
 * This class is typically instantiated from the PhaseChainCache class in this
 * package. Interceptors that are added to the chain are ordered by phase.
 * Within a phase, interceptors can order themselves. Each PhaseInterceptor
 * has an ID. PhaseInterceptors can supply a Collection of IDs which they
 * should run before or after, supplying fine grained ordering.
 * <p>
 *
 */
public class PhaseInterceptorChain implements InterceptorChain {
    public static final String PREVIOUS_MESSAGE = PhaseInterceptorChain.class.getName() + ".PREVIOUS_MESSAGE";

    private static final Logger LOG = LogUtils.getL7dLogger(PhaseInterceptorChain.class);

    private static final ThreadLocal<Message> CURRENT_MESSAGE = new ThreadLocal<>();

    private final Map<String, Integer> nameMap;
    private final Phase[] phases;

    // heads[phase] refers to the first interceptor of the given phase
    private InterceptorHolder[] heads;
    // tails[phase] refers to the last interceptor of the given phase
    private InterceptorHolder[] tails;
    // hasAfters[phase] indicates that the given phase has already inserted
    // interceptors that may need to be placed after future to-be-inserted
    // interceptors.  This flag is used to activate ordering of interceptors
    // when new ones are added to the list for this phase.
    // Note no hasBefores[] is needed because implementation adds subsequent
    // interceptors to the end of the list by default.
    private boolean[] hasAfters;


    private State state;
    private Message pausedMessage;
    private MessageObserver faultObserver;
    private PhaseInterceptorIterator iterator;
    private final boolean isFineLogging;

    // currently one chain for one request/response, use below as signal
    // to avoid duplicate fault processing on nested calling of
    // doIntercept(), which will throw same fault multi-times
    private boolean faultOccurred;
    private boolean chainReleased;


    private PhaseInterceptorChain(PhaseInterceptorChain src) {
        isFineLogging = LOG.isLoggable(Level.FINE);

        //only used for clone
        state = State.EXECUTING;

        //immutable, just repoint
        nameMap = src.nameMap;
        phases = src.phases;

        int length = phases.length;
        hasAfters = new boolean[length];
        System.arraycopy(src.hasAfters, 0, hasAfters, 0, length);

        heads = new InterceptorHolder[length];
        tails = new InterceptorHolder[length];

        InterceptorHolder last = null;
        for (int x = 0; x < length; x++) {
            InterceptorHolder ih = src.heads[x];
            while (ih != null
                && ih.phaseIdx == x) {
                InterceptorHolder ih2 = new InterceptorHolder(ih);
                ih2.prev = last;
                if (last != null) {
                    last.next = ih2;
                }
                if (heads[x] == null) {
                    heads[x] = ih2;
                }
                tails[x] = ih2;
                last = ih2;
                ih = ih.next;
            }
        }
    }

    public PhaseInterceptorChain(SortedSet<Phase> ps) {
        state = State.EXECUTING;
        isFineLogging = LOG.isLoggable(Level.FINE);

        int numPhases = ps.size();
        phases = new Phase[numPhases];
        nameMap = new HashMap<>();

        heads = new InterceptorHolder[numPhases];
        tails = new InterceptorHolder[numPhases];
        hasAfters = new boolean[numPhases];

        int idx = 0;
        for (Phase phase : ps) {
            phases[idx] = phase;
            nameMap.put(phase.getName(), idx);
            ++idx;
        }
    }

    public static Message getCurrentMessage() {
        return CURRENT_MESSAGE.get();
    }

    public static boolean setCurrentMessage(PhaseInterceptorChain chain, Message m) {
        if (getCurrentMessage() == m) {
            return false;
        }
        if (chain.iterator.hasPrevious()) {
            chain.iterator.previous();
            if (chain.iterator.next() instanceof ServiceInvokerInterceptor) {
                CURRENT_MESSAGE.set(m);
                return true;
            }
            String error = "Only ServiceInvokerInterceptor can update the current chain message";
            LOG.warning(error);
            throw new IllegalStateException(error);
        }
        return false;

    }

    public synchronized State getState() {
        return state;
    }

    public synchronized void releaseAndAcquireChain() {
        while (!chainReleased) {
            try {
                this.wait();
            } catch (InterruptedException ex) {
                // ignore
            }
        }
        chainReleased = false;
    }

    public synchronized void releaseChain() {
        this.chainReleased = true;
        this.notifyAll();
    }

    public PhaseInterceptorChain cloneChain() {
        return new PhaseInterceptorChain(this);
    }

    private void updateIterator() {
        if (iterator == null) {
            iterator = new PhaseInterceptorIterator(heads);
            outputChainToLog(false);
        }
    }

    public void add(Collection<Interceptor<? extends Message>> newhandlers) {
        add(newhandlers, false);
    }

    public void add(Collection<Interceptor<? extends Message>> newhandlers, boolean force) {
        if (newhandlers == null) {
            return;
        }

        for (Interceptor<? extends Message> handler : newhandlers) {
            add(handler, force);
        }
    }

    public void add(Interceptor<? extends Message> i) {
        add(i, false);
    }

    public void add(Interceptor<? extends Message> i, boolean force) {
        PhaseInterceptor<? extends Message> pi = (PhaseInterceptor<? extends Message>)i;

        String phaseName = pi.getPhase();
        Integer phase = nameMap.get(phaseName);

        if (phase == null) {
            LOG.warning("Skipping interceptor " + i.getClass().getName()
                + ((phaseName == null) ? ": Phase declaration is missing."
                : ": Phase " + phaseName + " specified does not exist."));
        } else {
            if (isFineLogging) {
                LOG.fine("Adding interceptor " + i + " to phase " + phaseName);
            }

            insertInterceptor(phase, pi, force);
        }
        Collection<PhaseInterceptor<? extends Message>> extras
            = pi.getAdditionalInterceptors();
        if (extras != null) {
            for (PhaseInterceptor<? extends Message> p : extras) {
                add(p, force);
            }
        }
    }

    public synchronized void pause() {
        state = State.PAUSED;
        pausedMessage = CURRENT_MESSAGE.get();
    }
    public synchronized void unpause() {
        if (state == State.PAUSED || state == State.SUSPENDED) {
            state = State.EXECUTING;
            pausedMessage = null;
        }
    }

    public synchronized void suspend() {
        state = State.SUSPENDED;
        pausedMessage = CURRENT_MESSAGE.get();
    }

    public synchronized void resume() {
        if (state == State.PAUSED || state == State.SUSPENDED) {
            state = State.EXECUTING;
            Message m = pausedMessage;
            pausedMessage = null;
            doIntercept(m);
        }
    }

    /**
     * Intercept a message, invoking each phase's handlers in turn.
     *
     * @param message the message
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public synchronized boolean doIntercept(Message message) {
        updateIterator();

        Message oldMessage = CURRENT_MESSAGE.get();
        try {
            CURRENT_MESSAGE.set(message);
            if (oldMessage != null
                && !message.containsKey(PREVIOUS_MESSAGE)
                && message != oldMessage
                && message.getExchange() != oldMessage.getExchange()) {
                message.put(PREVIOUS_MESSAGE, new WeakReference<Message>(oldMessage));
            }
            while (state == State.EXECUTING && iterator.hasNext()) {
                try {
                    Interceptor<Message> currentInterceptor = (Interceptor<Message>)iterator.next();
                    if (isFineLogging) {
                        LOG.fine("Invoking handleMessage on interceptor " + currentInterceptor);
                    }
                    //System.out.println("-----------" + currentInterceptor);
                    currentInterceptor.handleMessage(message);
                    if (state == State.SUSPENDED) {
                         // throw the exception to make sure thread exit without interrupt
                        throw new SuspendedInvocationException();
                    }

                } catch (SuspendedInvocationException ex) {

                    // Moving the chain iterator to the previous interceptor is needed
                    // for the invocation to be resumed from the same interceptor which
                    // suspended the invocation.
                    // If "suspend.chain.on.current.interceptor" is set to true then
                    // the chain will be resumed from the interceptor which follows
                    // the interceptor which suspended the invocation.
                    Object suspendProp = message.remove("suspend.chain.on.current.interceptor");
                    if ((suspendProp == null || PropertyUtils.isFalse(suspendProp))
                        && iterator.hasPrevious()) {
                        iterator.previous();
                    }
                    pause();
                    throw ex;
                } catch (AbortedInvocationException ex) {
                    abort();
                    throw ex;
                } catch (RuntimeException ex) {
                    if (!faultOccurred) {
                        faultOccurred = true;
                        wrapExceptionAsFault(message, ex);
                    }
                    state = State.ABORTED;
                }
            }
            if (state == State.EXECUTING) {
                state = State.COMPLETE;
            }
            return state == State.COMPLETE;
        } finally {
            CURRENT_MESSAGE.set(oldMessage);
        }
    }

    private void wrapExceptionAsFault(Message message, RuntimeException ex) {
        String description = getServiceInfo(message);

        message.setContent(Exception.class, ex);
        unwind(message);
        Exception ex2 = message.getContent(Exception.class);
        if (ex2 == null) {
            ex2 = ex;
        }

        FaultListener flogger = (FaultListener)
                message.getContextualProperty(FaultListener.class.getName());
        boolean useDefaultLogging = true;
        if (flogger != null) {
            useDefaultLogging = flogger.faultOccurred(ex2, description, message);
        }
        if (useDefaultLogging) {
            doDefaultLogging(message, ex2, description);
        }

        if (message.getExchange() != null && message.getContent(Exception.class) != null) {
            message.getExchange().put(Exception.class, ex2);
        }

        if (faultObserver != null && !isOneWay(message)) {
            // CXF-5629. when exchange is one way and robust, it becomes req-resp in order to
            // send the fault
            message.getExchange().setOneWay(false);
            faultObserver.onMessage(message);
        }
    }

    private String getServiceInfo(Message message) {
        StringBuilder description = new StringBuilder();
        if (message.getExchange() != null) {
            Exchange exchange = message.getExchange();
            Service service = exchange.getService();
            if (service != null) {
                description.append('\'');
                description.append(service.getName());
                BindingOperationInfo boi = exchange.getBindingOperationInfo();
                OperationInfo opInfo = boi != null ? boi.getOperationInfo() : null;
                if (opInfo != null) {
                    description.append('#').append(opInfo.getName());
                }
                description.append("\' ");
            }
        }
        return description.toString();
    }

    private void doDefaultLogging(Message message, Exception ex, String description) {
        FaultMode mode = message.get(FaultMode.class);
        if (mode == FaultMode.CHECKED_APPLICATION_FAULT) {
            if (isFineLogging) {
                LogUtils.log(LOG, Level.FINE,
                             "Application " + description
                             + "has thrown exception, unwinding now", ex);
            } else if (LOG.isLoggable(Level.INFO)) {
                Throwable t = ex;
                if (ex instanceof Fault
                    && ex.getCause() != null) {
                    t = ex.getCause();
                }

                LogUtils.log(LOG, Level.INFO,
                             "Application " + description
                             + "has thrown exception, unwinding now: "
                             + t.getClass().getName()
                             + ": " + ex.getMessage());
            }
        } else if (LOG.isLoggable(Level.WARNING)) {
            if (mode == FaultMode.UNCHECKED_APPLICATION_FAULT) {
                LogUtils.log(LOG, Level.WARNING,
                             "Application " + description
                             + "has thrown exception, unwinding now", ex);
            } else {
                LogUtils.log(LOG, Level.WARNING,
                             "Interceptor for " + description
                             + "has thrown exception, unwinding now", ex);
            }
        }
    }

    private boolean isOneWay(Message message) {
        return (message.getExchange() != null) && message.getExchange().isOneWay() && !isRobustOneWay(message);
    }

    private boolean isRobustOneWay(Message message) {
        return MessageUtils.getContextualBoolean(message, Message.ROBUST_ONEWAY, false);
    }

    /**
     * Intercept a message, invoking each phase's handlers in turn,
     * starting after the specified interceptor.
     *
     * @param message the message
     * @param startingAfterInterceptorID the id of the interceptor
     * @throws Exception
     */
    public synchronized boolean doInterceptStartingAfter(Message message,
                                                         String startingAfterInterceptorID) {
        updateIterator();
        while (state == State.EXECUTING && iterator.hasNext()) {
            PhaseInterceptor<? extends Message> currentInterceptor
                = (PhaseInterceptor<? extends Message>)iterator.next();
            if (currentInterceptor.getId().equals(startingAfterInterceptorID)) {
                break;
            }
        }
        return doIntercept(message);
    }

    /**
     * Intercept a message, invoking each phase's handlers in turn,
     * starting at the specified interceptor.
     *
     * @param message the message
     * @param startingAtInterceptorID the id of the interceptor
     * @throws Exception
     */
    public synchronized boolean doInterceptStartingAt(Message message,
                                                         String startingAtInterceptorID) {
        updateIterator();
        while (state == State.EXECUTING && iterator.hasNext()) {
            PhaseInterceptor<? extends Message> currentInterceptor
                = (PhaseInterceptor<? extends Message>)iterator.next();
            if (currentInterceptor.getId().equals(startingAtInterceptorID)) {
                iterator.previous();
                break;
            }
        }
        return doIntercept(message);
    }

    public synchronized void reset() {
        updateIterator();
        if (state == State.COMPLETE) {
            state = State.EXECUTING;
            iterator.reset();
        } else {
            iterator.reset();
        }
    }

    @SuppressWarnings("unchecked")
    public void unwind(Message message) {
        while (iterator.hasPrevious()) {
            Interceptor<Message> currentInterceptor = (Interceptor<Message>)iterator.previous();
            if (isFineLogging) {
                LOG.fine("Invoking handleFault on interceptor " + currentInterceptor);
            }
            try {
                currentInterceptor.handleFault(message);
            } catch (RuntimeException e) {
                LOG.log(Level.WARNING, "Exception in handleFault on interceptor " + currentInterceptor, e);
                throw e;
            } catch (Exception e) {
                LOG.log(Level.WARNING, "Exception in handleFault on interceptor " + currentInterceptor, e);
                throw new RuntimeException(e);
            }
        }
    }

    public void remove(Interceptor<? extends Message> i) {
        PhaseInterceptorIterator it = new PhaseInterceptorIterator(heads);
        while (it.hasNext()) {
            InterceptorHolder holder = it.nextInterceptorHolder();
            if (holder.interceptor == i) {
                remove(holder);
                return;
            }
        }
    }

    public synchronized void abort() {
        this.state = InterceptorChain.State.ABORTED;
    }

    public Iterator<Interceptor<? extends Message>> iterator() {
        return getIterator();
    }
    public ListIterator<Interceptor<? extends Message>> getIterator() {
        return new PhaseInterceptorIterator(heads);
    }

    private void remove(InterceptorHolder i) {
        if (i.prev != null) {
            i.prev.next = i.next;
        }
        if (i.next != null) {
            i.next.prev = i.prev;
        }
        int ph = i.phaseIdx;
        if (heads[ph] == i) {
            if (i.next != null
                && i.next.phaseIdx == ph) {
                heads[ph] = i.next;
            } else {
                heads[ph] = null;
                tails[ph] = null;
            }
        }
        if (tails[ph] == i) {
            if (i.prev != null
                && i.prev.phaseIdx == ph) {
                tails[ph] = i.prev;
            } else {
                heads[ph] = null;
                tails[ph] = null;
            }
        }
    }

    private void insertInterceptor(int phase, PhaseInterceptor<? extends Message> interc, boolean force) {
        InterceptorHolder ih = new InterceptorHolder(interc, phase);
        if (heads[phase] == null) {
            // no interceptors yet in this phase
            heads[phase] = ih;
            tails[phase] = ih;
            hasAfters[phase] = !interc.getAfter().isEmpty();

            int idx = phase - 1;
            while (idx >= 0) {
                if (tails[idx] != null) {
                    break;
                }
                --idx;
            }
            if (idx >= 0) {
                //found something before us, in an earlier phase
                ih.prev = tails[idx];
                ih.next = tails[idx].next;
                if (ih.next != null) {
                    ih.next.prev = ih;
                }
                tails[idx].next = ih;
            } else {
                //did not find something before us, try after
                idx = phase + 1;
                while (idx < heads.length) {
                    if (heads[idx] != null) {
                        break;
                    }
                    ++idx;
                }

                if (idx != heads.length) {
                    //found something after us
                    ih.next = heads[idx];
                    heads[idx].prev = ih;
                }
            }
        } else { // this phase already has interceptors attached

            // list of interceptors that the new interceptor should precede
            Set<String> beforeList = interc.getBefore();

            // list of interceptors that the new interceptor should be after
            Set<String> afterList = interc.getAfter();

            // firstBefore will hold the first interceptor of a given phase
            // that the interceptor to be added must precede
            InterceptorHolder firstBefore = null;

            // lastAfter will hold the last interceptor of a given phase
            // that the interceptor to be added must come after
            InterceptorHolder lastAfter = null;

            String id = interc.getId();
            if (hasAfters[phase] || !beforeList.isEmpty()) {

                InterceptorHolder ih2 = heads[phase];
                while (ih2 != tails[phase].next) {
                    PhaseInterceptor<? extends Message> cmp = ih2.interceptor;
                    String cmpId = cmp.getId();
                    if (cmpId != null && firstBefore == null
                        && (beforeList.contains(cmpId)
                            || cmp.getAfter().contains(id))) {
                        firstBefore = ih2;
                    }
                    if (cmp.getBefore().contains(id)
                        || (cmpId != null && afterList.contains(cmpId))) {
                        lastAfter = ih2;
                    }
                    if (!force && cmpId.equals(id)) {
                        // interceptor is already in chain
                        return;
                    }
                    ih2 = ih2.next;
                }
                if (lastAfter == null && beforeList.contains("*")) {
                    firstBefore = heads[phase];
                }
                
            } else if (!force) {
                // skip interceptor if already in chain
                InterceptorHolder ih2 = heads[phase];
                while (ih2 != tails[phase].next) {
                    if (ih2.interceptor.getId().equals(id)) {
                        return;
                    }
                    ih2 = ih2.next;
                }

            }
            hasAfters[phase] |= !afterList.isEmpty();

            if (firstBefore == null
                && lastAfter == null
                && !beforeList.isEmpty()
                && afterList.isEmpty()) {
                //if this interceptor has stuff it MUST be before,
                //but nothing it must be after, just
                //stick it at the beginning
                firstBefore = heads[phase];
            }

            if (firstBefore == null) {
                //just add new interceptor at the end
                ih.prev = tails[phase];
                ih.next = tails[phase].next;
                tails[phase].next = ih;

                if (ih.next != null) {
                    ih.next.prev = ih;
                }
                tails[phase] = ih;
            } else {
                ih.prev = firstBefore.prev;
                if (ih.prev != null) {
                    ih.prev.next = ih;
                }
                ih.next = firstBefore;
                firstBefore.prev = ih;

                if (heads[phase] == firstBefore) {
                    heads[phase] = ih;
                }
            }
        }
        if (iterator != null) {
            outputChainToLog(true);
        }
    }

    public String toString() {
        return toString("");
    }
    private String toString(String message) {
        StringBuilder chain = new StringBuilder(128);

        chain.append("Chain ")
            .append(super.toString())
            .append(message)
            .append(". Current flow:\n");

        for (int x = 0; x < phases.length; x++) {
            if (heads[x] != null) {
                chain.append("  ");
                printPhase(x, chain);
            }
        }
        return chain.toString();
    }
    private void printPhase(int ph, StringBuilder chain) {

        chain.append(phases[ph].getName())
            .append(" [");
        InterceptorHolder i = heads[ph];
        boolean first = true;
        while (i != tails[ph].next) {
            if (first) {
                first = false;
            } else {
                chain.append(", ");
            }
            String nm = i.interceptor.getClass().getSimpleName();
            if (StringUtils.isEmpty(nm)) {
                nm = i.interceptor.getId();
            }
            chain.append(nm);
            i = i.next;
        }
        chain.append("]\n");
    }

    private void outputChainToLog(boolean modified) {
        if (isFineLogging) {
            if (modified) {
                LOG.fine(toString(" was modified"));
            } else {
                LOG.fine(toString(" was created"));
            }
        }
    }

    public MessageObserver getFaultObserver() {
        return faultObserver;
    }

    public void setFaultObserver(MessageObserver faultObserver) {
        this.faultObserver = faultObserver;
    }

    static final class PhaseInterceptorIterator implements ListIterator<Interceptor<? extends Message>> {
        InterceptorHolder[] heads;
        InterceptorHolder prev;
        InterceptorHolder first;

        PhaseInterceptorIterator(InterceptorHolder[] h) {
            heads = h;
            first = findFirst();
        }

        public void reset() {
            prev = null;
            first = findFirst();
        }

        private InterceptorHolder findFirst() {
            for (int x = 0; x < heads.length; x++) {
                if (heads[x] != null) {
                    return heads[x];
                }
            }
            return null;
        }


        public boolean hasNext() {
            if (prev == null) {
                return first != null;
            }
            return prev.next != null;
        }

        public Interceptor<? extends Message> next() {
            if (prev == null) {
                if (first == null) {
                    throw new NoSuchElementException();
                }
                prev = first;
            } else {
                if (prev.next == null) {
                    throw new NoSuchElementException();
                }
                prev = prev.next;
            }
            return prev.interceptor;
        }
        public InterceptorHolder nextInterceptorHolder() {
            if (prev == null) {
                if (first == null) {
                    throw new NoSuchElementException();
                }
                prev = first;
            } else {
                if (prev.next == null) {
                    throw new NoSuchElementException();
                }
                prev = prev.next;
            }
            return prev;
        }

        public boolean hasPrevious() {
            return prev != null;
        }
        public Interceptor<? extends Message> previous() {
            if (prev == null) {
                throw new NoSuchElementException();
            }
            InterceptorHolder tmp = prev;
            prev = prev.prev;
            return tmp.interceptor;
        }

        public int nextIndex() {
            throw new UnsupportedOperationException();
        }
        public int previousIndex() {
            throw new UnsupportedOperationException();
        }
        public void add(Interceptor<? extends Message> o) {
            throw new UnsupportedOperationException();
        }
        public void set(Interceptor<? extends Message> o) {
            throw new UnsupportedOperationException();
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }


    static final class InterceptorHolder {
        PhaseInterceptor<? extends Message> interceptor;
        InterceptorHolder next;
        InterceptorHolder prev;
        int phaseIdx;

        InterceptorHolder(PhaseInterceptor<? extends Message> i, int p) {
            interceptor = i;
            phaseIdx = p;
        }
        InterceptorHolder(InterceptorHolder p) {
            interceptor = p.interceptor;
            phaseIdx = p.phaseIdx;
        }
    }

}
