/*
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License).  You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the license at
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Copyright (c) Ericsson AB, 2004-2007. All rights reserved.
 */
package com.ericsson.ssa.sip;

import com.ericsson.ssa.sip.PathNode.Type;
import com.ericsson.ssa.sip.dns.TargetResolver;

import java.io.IOException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.sip.Proxy;
import javax.servlet.sip.ProxyBranch;
import javax.servlet.sip.SipServletRequest;
import javax.servlet.sip.SipURI;
import javax.servlet.sip.URI;


/**
 * The Proxy implementation.
 *
 * @author ehsroha
 */
public class ProxyImpl {
    private static final Logger _log = (Logger) Logger.getLogger("SipContainer");

    // feature of JSR 289 10.2.3 Sending Responses
    private static final URI _virtualProxyBranchURI = new SipURIImpl(false,
            "proxy_uas", "localhost");
    private SipApplicationSessionImpl _sipApplicationSession = null;
    private SipServletRequestImpl _originalRequest = null;
    private boolean _isRecordRoute = false;
    private boolean _isRecurse = true;
    private boolean _isParallel = true;

    // FIXME maybe we should rely on Timer C in the transaction instead?
    // use timer C as default
    private int _proxyTimeout = ProxyBranchImpl.TimerC;

    // set the first 2xx response received or the best
    // 3xx-6xx response that has been forwarded
    private SipServletResponseImpl _bestResponse = null;
    private boolean _is6xx = false;
    private final Collection<TargetSet> _targetSets = new ConcurrentLinkedQueue<TargetSet>();
    private CreatedTargetSet _createdSet = new CreatedTargetSet(this);
    private boolean _isFirst = true;
    private ProxyBranchImpl _virtualProxyBranch = null;

    /**
     * The constructor of the Proxy implementation.
     *
     * @param session
     *           the SipSession associated with this proxy.
     * @param originalRequest
     *           the request received from the upstream caller.
     */
    public ProxyImpl(SipApplicationSessionImpl appSession,
        SipServletRequestImpl originalRequest) {
        _sipApplicationSession = appSession;
        _originalRequest = originalRequest;

        SipSessionBase s = originalRequest.getSessionImpl();
        s.setType(getType());

        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE, "ProxyImpl reference = " + this);
        }
    }

    public Type getType() {
        return Type.Proxy;
    }

    public Collection<TargetSet> getTargetSets() {
        return _targetSets;
    }

    private void setDerivedOrOriginalSession(SipServletRequestImpl req,
        ProxyContext pc) throws IOException, ServletException {
        // dialog creational NOTIFY
        if (req.getMethod().equals("NOTIFY")) {
            // state Record Route in request to inform
            // that a RecordRoute header must be added
            req.indicateRecordRoute();

            // if the context has a session with same to tag as the dialog, use
            // it...
            if (!pc.getSipSession().hasNoToTag() &&
                    (req.getDialog().getToTag() != null) &&
                    req.getDialog().getToTag()
                           .equals(pc.getSipSession().getToTag())) {
                req.setSession(pc.getSipSession());
            } else {
                // ...otherwise fetch or create one
                DialogFragment df = req.getDialog();

                // lets update to-tag of dialog...
                SipSessionBase s = pc.getSipSession()
                                     .getOriginalOrDerivedSessionAndRegisterDialog(req,
                        df);

                if (s.isDerived()) {
                    // lets set the session cuz it is a derived session
                    req.setSession(s);
                    pc.setSipSession(s);
                }
            }
        } else {
            // the response should already have created
            // the dialog for all other methods...
            req.setSession(pc.getSipSession());
        }
    }

    public void dispatch(SipServletRequestImpl req, ProxyContext pc) {
        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE, req.toDebugString());
        }

        // update max forwards
        int maxForwards = req.getMaxForwards();

        if (maxForwards == 0) {
            if (_log.isLoggable(Level.INFO)) {
                _log.log(Level.INFO, "Too Many Hops for request = " + req);
            }

            SipServletResponseImpl response = req.createTerminatingResponse(483);

            if (response != null) {
                response.popDispatcher().dispatch(response);
            }

            return;
        } else if (maxForwards == -1) {
            maxForwards = 70;
        } else {
            maxForwards--;
        }

        req.setMaxForwards(maxForwards);

        if (req.getMethod().equals("CANCEL")) {
            if (!getTargetSets().isEmpty()) {
                cancelIntern();
            } else {
                // no branch found, the initial request has
                // not been proxied jet, respond with 487
                synchronized (this) {
                    if (_bestResponse == null) {
                        // setting best response will stop further
                        // proxyTo/startProxy calls
                        _bestResponse = getOriginalRequestImpl()
                                            .createTerminatingResponse(487);
                    }
                }

                if (_bestResponse != null) {
                    _bestResponse.popDispatcher().dispatch(_bestResponse);
                }
            }

            // lets answer cancel with 200 OK...
            SipServletResponseImpl resp = req.createTerminatingResponse(200);
            resp.setRemote(req.getRemote());
            resp.popDispatcher().dispatch(resp);

            try {
                // notify the servlet about the CANCEL...
                setDerivedOrOriginalSession(req, pc);

                Servlet s = getServlet(pc.getSipSession().getHandler());

                if (s != null) {
                    req.setProxyContext(pc);
                    s.service(req, null);
                } else {
                    if (_log.isLoggable(Level.INFO)) {
                        _log.log(Level.INFO,
                            "Could not find servlet name: " +
                            req.getSessionImpl().getHandler() +
                            " in application: " +
                            req.getSessionImpl().getApplicationSessionImpl()
                               .getName());
                    }
                }
            } catch (Exception e) {
                // problem in servlet, setting best response
                // will stop further proxyTo/startProxy calls
                if (_bestResponse == null) {
                    synchronized (this) {
                        if (_bestResponse == null) {
                            _bestResponse = getOriginalRequestImpl()
                                                .createTerminatingResponse(500);
                        }
                    }

                    if (_bestResponse != null) {
                        _bestResponse.popDispatcher().dispatch(_bestResponse);
                    }
                }
            }
        } else {
            if (getRecordRoute()) {
                try {
                    // support for dialog creational NOTIFY
                    setDerivedOrOriginalSession(req, pc);

                    // if record route is enabled invoke the servlet
                    Servlet s = getServlet(pc.getSipSession().getHandler());

                    if (s != null) {
                        req.setProxyContext(pc);
                        s.service(req, null);

                        if (req.getSupervised()) {
                            // only if the response will visit this proxy
                            // the request need to be cloned
                            SipServletRequestImpl clone = (SipServletRequestImpl) req.clone();
                            clone.pushTransactionDispatcher(pc);
                            clone.setTransactionRequest(req);
                            clone.popDispatcher().dispatch(clone);
                        } else {
                            req.popDispatcher().dispatch(req);
                        }
                    } else {
                        if (_log.isLoggable(Level.INFO)) {
                            _log.log(Level.INFO,
                                "Could not find servlet name: " +
                                req.getSessionImpl().getHandler() +
                                " in application: " +
                                req.getSessionImpl().getApplicationSessionImpl()
                                   .getName());
                        }
                    }
                } catch (Exception e) {
                    if (_log.isLoggable(Level.FINE)) {
                        _log.log(Level.FINE, "Problem in servlet ", e);
                    }

                    // problem in servlet, setting best response
                    // will stop further proxyTo calls
                    synchronized (this) {
                        _bestResponse = req.createTerminatingResponse(500);
                    }

                    if (_bestResponse != null) {
                        _bestResponse.popDispatcher().dispatch(_bestResponse);
                    }
                }
            } else {
                req.popDispatcher().dispatch(req);
            }
        }
    }

    private void invokeServlet(SipServletResponseImpl resp)
        throws IOException, ServletException {
        if (resp.getStatus() != 100) {
            Servlet s = getServlet(resp.getSessionImpl().getHandler());

            if (s != null) {
                s.service(null, resp);
            } else {
                if (_log.isLoggable(Level.INFO)) {
                    _log.log(Level.INFO,
                        "Could not find servlet name: " +
                        resp.getSessionImpl().getHandler() +
                        " in application: " +
                        resp.getSessionImpl().getApplicationSessionImpl()
                            .getName());
                }
            }
        }
    }

    private void dispatchAndClone(SipServletResponseImpl resp) {
        // the request need to be cloned
        SipServletResponseImpl clone = (SipServletResponseImpl) resp.clone();

        // if there is a transaction request, added it to the response...
        SipServletRequestImpl req = resp.getRequestImpl().getTransactionRequest();

        if (req != null) {
            clone.setRequest(req);
            clone.setSession(req.getSessionImpl());
        }

        clone.popDispatcher().dispatch(clone);
    }

    private void assignSession(SipServletResponseImpl resp, ProxyContext pc)
        throws IOException, ServletException {
        if (resp.getStatus() != 100) {
            // if the context has a session with same to tag as the dialog, use
            // it...
            if (!pc.getSipSession().hasNoToTag() &&
                    (resp.getDialog().getToTag() != null) &&
                    resp.getDialog().getToTag()
                            .equals(pc.getSipSession().getToTag())) {
                resp.setSession(pc.getSipSession());
            } else {
                // ...otherwise fetch or create one
                DialogFragment df = resp.getDialog();

                // lets update to-tag of dialog...
                SipSessionBase s = resp.getSessionImpl()
                                       .getOriginalOrDerivedSessionAndRegisterDialog(resp,
                        df);

                if (s.isDerived()) {
                    // lets set the session cuz it is a derived session
                    resp.setSession(s);
                }

                pc.setSipSession(s);
            }
        }
    }

    public void invokeServletAndForward(SipServletResponseImpl resp,
        ProxyContext pc) {
        try {
            assignSession(resp, pc);

            if (resp.getRequestImpl().getSupervised()) {
                // if supervised is enabled invoke the servlet
                invokeServlet(resp);
                dispatchAndClone(resp);
            } else {
                resp.popDispatcher().dispatch(resp);
            }
        } catch (Exception e) {
            // problem in servlet, lets drop response
            if (_log.isLoggable(Level.INFO)) {
                _log.log(Level.INFO, "Problem in servlet.", e);
            }
        }
    }

    private void doInitialResponse(SipServletResponseImpl resp, ProxyContext pc) {
        int status = resp.getStatus() / 100;

        if (status != 1) {
            if (status == 2) {
                // 1. best response is found, stop new proxing
                if (setAndTestBestResponse(resp) == null) {
                    // 2a. cancel all branches
                    cancelIntern();
                    // 3a. invoke servlet if supervised.
                    // 4a. send upstream
                    invokeServletAndForward(resp, pc);
                } else if (resp.getMethod().equals("INVITE")) {
                    // if multiple 2xx response to INVITE
                    // 3b. invoke servlet if supervised
                    // 3c. send upstream
                    invokeServletAndForward(resp, pc);
                }
            } else if (status == 6) {
                // 1. stop new proxing.
                if (!setAndTest6xx()) {
                    // 2. cancel all branches
                    cancelIntern();

                    // 3. find best response since no new branches can be made
                    SipServletResponseImpl bestResp = findBestResponse();

                    if (bestResp != null) {
                        if (setAndTestBestResponse(bestResp) == null) {
                            // 4. if supervised is true invoke servlet with best
                            // response and finally send back best response
                            invokeServletAndForward(bestResp, pc);
                        }
                    }
                }
            } else if ((status > 2) && (status < 6)) {
                // 1. find best response
                SipServletResponseImpl bestResp = findBestResponse();

                if (bestResp != null) {
                    // 2. best response has been found, invoke servlet
                    // if supervised. Servlet might create new branches.
                    try {
                        if (!hasBestResponse()) {
                            assignSession(bestResp, pc);

                            if (bestResp.getRequestImpl().getSupervised()) {
                                // 3a. pass response to servlet
                                invokeServlet(bestResp);

                                // only if this is a 3xx redirect it will inform
                                // that this has already been consumed
                                bestResp.setAlreadyRedirected();

                                // 4. has new proxying occured?
                                bestResp = findBestResponse();

                                if (bestResp != null) {
                                    if (setAndTestBestResponse(bestResp) == null) {
                                        // 5. stop new proxing since best response
                                        // has been found, send it back
                                        bestResp.popDispatcher()
                                                .dispatch(bestResp);
                                    }
                                }
                            } else {
                                if (setAndTestBestResponse(bestResp) == null) {
                                    // 3b. stop new proxing since best response
                                    // has been found, send it back
                                    bestResp.popDispatcher().dispatch(bestResp);
                                }
                            }
                        }
                    } catch (Exception e) {
                        // problem in servlet, lets drop response
                        if (_log.isLoggable(Level.INFO)) {
                            _log.log(Level.INFO, "Problem in servlet.", e);
                        }
                    }
                }
            }
        }
    }

    public void dispatch(SipServletResponseImpl resp, ProxyContext pc) {
        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE, resp.toDebugString());
        }

        // forward to branch only if the response is initial,
        // otherwise invoke servlet if supervised is set by request.
        if (resp.getRequest().isInitial()) {
            if (!hasBestResponse()) {
                ProxyBranchImpl branch = findBranch(resp.getRequest()
                                                        .getRequestURI());

                if (branch != null) {
                    if (branch.doInitialResponse(resp, pc) &&
                            ((findBestResponse() != null) ||
                            ((resp.getStatus() / 100) == 2))) {
                        // this is a response of the virtual proxy branch (proxy as a UAS)
                        // and it was generated in doResponse callback and/or it is a 2xx
                        //
                        // JSR289 10.2.3 Sending Responses
                        // - If it�s a 2xx response, it is sent upstream immediately 
                        // without notifying the application of its own response.
                        // 
                        // - If the best response received was a non-2xx and the application 
                        // generated its own final response in the doResponse callback 
                        // (be it a 2xx or non-2xx), then that response is sent immediately 
                        // without invoking the application again for its own generated response.
                        if (setAndTestBestResponse(resp) == null) {
                            // lets stop retransmissions and subsequent request 
                            // to be sent to servlet of proxy, forward all to ua.
                            resp.getRequestImpl().setSupervised(false);
                            setRecordRoute(false);

                            // stop new proxing since best response has
                            // been found, send it immediately back
                            resp.popDispatcher().dispatch(resp);
                        }
                    } else {
                        doInitialResponse(resp, pc);
                    }
                } else {
                    // no branch found, drop message
                    if (_log.isLoggable(Level.INFO)) {
                        _log.log(Level.INFO,
                            "Could not find ProxyBranch of response: " + resp);
                    }
                }
            } else {
                // best response already exist,
                // only 2xx to INVITE should be forwarded
                if (((resp.getStatus() / 100) == 2) &&
                        resp.getMethod().equals("INVITE")) {
                    invokeServletAndForward(resp, pc);
                }
            }
        } else {
            invokeServletAndForward(resp, pc);
        }
    }

    public SipServletRequest getOriginalRequest() {
        return getOriginalRequestImpl();
    }

    public SipServletRequestImpl getOriginalRequestImpl() {
        return _originalRequest;
    }

    public SipServletRequestImpl cloneOriginalRequest() {
        SipServletRequestImpl clone = (SipServletRequestImpl) _originalRequest.clone();
        // should point to same proxy context instance
        clone.setProxyContext(_originalRequest.getProxyContext());
        // set transaction request
        clone.setTransactionRequest(_originalRequest.getTransactionRequest());

        return clone;
    }

    public void cancel() throws IllegalStateException {
        if (!hasBestResponse() && !has6xx()) {
            cancelIntern();
        } else {
            throw new IllegalStateException("Proxy has already completed");
        }
    }

    public void cancelIntern() {
        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE, "CANCEL proxy");
        }

        // lets clear all created targets
        getCreatedTarget().getTargetSets().clear();

        // should cancel all target sets
        for (TargetSet t : getTargetSets()) {
            t.cancel();
        }
    }

    public boolean getRecurse() {
        return _isRecurse;
    }

    public void setRecurse(boolean recurse) {
        _isRecurse = recurse;
    }

    public boolean getRecordRoute() {
        return _isRecordRoute;
    }

    public void setRecordRoute(boolean recordRoute) {
        _isRecordRoute = recordRoute;
    }

    public boolean getParallel() {
        return _isParallel;
    }

    public void setParallel(boolean parallel) {
        _isParallel = parallel;
    }

    public boolean getStateful() {
        return true;
    }

    @Deprecated
    public void setStateful(boolean stateful) {
    }

    public int getProxyTimeout() {
        return _proxyTimeout;
    }

    @Deprecated
    public int getSequentialSearchTimeout() {
        return _proxyTimeout;
    }

    public void setProxyTimeout(int timeout) {
        _proxyTimeout = timeout;
    }

    @Deprecated
    public void setSequentialSearchTimeout(int timeout) {
        _proxyTimeout = timeout;
    }

    /**
     * Gets the URI
     *
     * @param req
     */
    public SipURI getRecordRouteURI(final SipServletRequestImpl req) {
        return new RecordRouteURI(req);
    }

    public SipApplicationSessionImpl getApplicationSession() {
        return _sipApplicationSession;
    }

    /**
     * This will return the session of the original request or null if not found
     */
    public SipSessionBase getSipSession() {
        if (getOriginalRequestImpl() != null) {
            return getOriginalRequestImpl().getSessionImpl();
        } else {
            return null;
        }
    }

    public Servlet getServlet(String handler) {
        return SipFactoryImpl.getInstance().getServiceHandler()
                             .getHandler(getApplicationSession().getName(),
            handler);
    }

    public String toString() {
        StringBuilder sb = new StringBuilder(super.toString() +
                " isRecordRoute: ");
        sb.append(getRecordRoute());
        sb.append(" isStateful: ");
        sb.append(getStateful());

        return sb.toString();
    }

    private CreatedTargetSet getCreatedTarget() {
        return _createdSet;
    }

    private ProxyBranchImpl createBranch(URI uri, boolean isVirtual) {
        SipServletRequestImpl req = createRequest(uri);

        return isVirtual ? new VirtualProxyBranchImpl(this, req)
                         : new ProxyBranchImpl(this, req);
    }

    private SipServletRequestImpl createRequest(URI uri) {
        TargetResolver.getInstance().updateDefaultTransportParameter(uri);

        SipServletRequestImpl request = cloneOriginalRequest();
        // update the request uri
        request.setRequestURI(uri);

        // update max forwards
        int maxForwards = request.getMaxForwards();

        if (maxForwards == -1) {
            maxForwards = 70;
        } else {
            maxForwards--;
        }

        request.setMaxForwards(maxForwards);

        return request;
    }

    public List<ProxyBranch> getProxyBranches() {
        List<ProxyBranchImpl> branches = new ArrayList<ProxyBranchImpl>();

        for (TargetSet t : getTargetSets()) {
            t.addTopLevelBranch(branches);
        }

        return new ArrayList<ProxyBranch>(branches);
    }

    public ProxyBranch getProxyBranch(URI uri) {
        return findBranch(uri);
    }

    private ProxyBranchImpl findBranch(URI uri) {
        ProxyBranchImpl branch = null;

        for (TargetSet t : getTargetSets()) {
            branch = t.findBranch(uri);

            if (branch != null) {
                return branch;
            }
        }

        return null;
    }

    public synchronized List<ProxyBranch> createProxyBranches(
        List<?extends URI> targets) {
        return createProxyBranchesIntern(targets);
    }

    private List<ProxyBranch> createProxyBranchesIntern(
        List<?extends URI> targets) {
        // make sure that the targets have not been used before
        for (URI uri : targets) {
            if (findBranch(uri) != null) {
                throw new IllegalStateException(
                    "Not allowed to proxy to same destination twice = " + uri);
            }
        }

        List<ProxyBranch> list = new ArrayList<ProxyBranch>(targets.size());
        ProxyBranchImpl branch = null;

        for (URI uri : targets) {
            branch = createBranch(uri, false);
            getCreatedTarget().add(branch);
            list.add(branch);
        }

        return list;
    }

    private List<ProxyBranchImpl> createRecursedProxyBranchesIntern(
        List<?extends URI> targets) {
        List<ProxyBranchImpl> list = new ArrayList<ProxyBranchImpl>(targets.size());
        ProxyBranchImpl branch = null;

        for (URI uri : targets) {
            branch = createBranch(uri, false);
            getCreatedTarget().add(branch);
            list.add(branch);
        }

        return list;
    }

    /**
     * Support of JSR 289 10.2.3 Sending Responses.
     * Creates a ProxyBranch of virtual type (VirtualProxyBranch)
     * to support a Proxy acting as a UAS. It also adds a new request
     * with a virtual request uri (proxy_uas@localhost) pointing to
     * this VirtualProxyBranch
     * @param resp
     */
    public synchronized void setVirtualProxyBranchRequest(
        SipServletResponseImpl resp) {
        if (_virtualProxyBranch == null) {
            _virtualProxyBranch = createBranch(_virtualProxyBranchURI, true);
        }

        resp.setRequest(_virtualProxyBranch.getRequestImpl());
    }

    /**
     * Support of JSR 289 10.2.3 Sending Responses.
     * The created VirtualProxyBranch will be started (proxyTo).
     * @param resp
     */
    public synchronized void startVirtualProxyBranch(
        SipServletResponseImpl resp) {
        // must have created the branch before calling this method
        if (_virtualProxyBranch != null) {
            // guard to not start it twice
            ProxyBranchImpl vpb = findBranch(_virtualProxyBranchURI);

            if (vpb == null) {
                // add this target set to the total set
                getTargetSets().add(_virtualProxyBranch);
                // lets simulate a proxy call
                _virtualProxyBranch.proxyTo();
                // set new dialog
                resp.setDialog(resp.getRequestImpl().getDialog());
            }
        } else {
            throw new IllegalStateException(
                "Must first create VirtualProxyBranch");
        }
    }

    /**
     * Returns the response of 2xx. If no 2xx exist the response of 6xx is
     * returned, otherwise the response of the lowest status number found is
     * returned. If the target set is not finished or not started null is
     * returned.
     *
     * @return the response of 2xx. If no 2xx exist the response of 6xx is
     *         returned, otherwise the response of the lowest status number found
     *         is returned. NOTE: if TargetSet is not finished or not started
     *         null is returned
     */
    private synchronized SipServletResponseImpl findBestResponse() {
        // lets assume target set has finished
        boolean isRunning = false;
        SipServletResponseImpl bestResponse = null;
        SipServletResponseImpl currentResp = null;
        int currentStatus = -1;
        int bestStatus = 700; // one higher than possible

        for (TargetSet t : getTargetSets()) {
            currentResp = t.findBestResponse();

            if (currentResp == null) {
                // haven't got all answers, but 2xx might already be in set.
                isRunning = true;
            } else {
                currentStatus = currentResp.getStatus();

                // 2xx response should always be returned...
                if ((currentStatus / 100) == 2) {
                    // can't be better, lets return...
                    return currentResp;
                }
                // ...otherwise save 6xx response (might find 2xx later)...
                else if ((currentStatus / 100) == 6) {
                    bestStatus = currentStatus;
                    bestResponse = currentResp;
                }
                // ...otherwise save the response of the lowest status number
                else if (((bestStatus / 100) != 6) &&
                        (bestStatus > currentResp.getStatus())) {
                    bestStatus = currentResp.getStatus();
                    bestResponse = currentResp;
                }
            }
        }

        if (isRunning) {
            // haven't got all answers, lets wait for better response context
            return null;
        }

        // have collected all answers
        return bestResponse;
    }

    /**
     * Sets best response.
     *
     * If already set, best response is returned and the proposed best response
     * is left without notice. If not set best response is set and null is
     * returned.
     *
     * @param resp
     * @return any previous best response being set or null if it was empty
     */
    private synchronized SipServletResponseImpl setAndTestBestResponse(
        SipServletResponseImpl resp) {
        if (_bestResponse != null) {
            return _bestResponse;
        }

        _bestResponse = resp;

        return null;
    }

    private synchronized boolean hasBestResponse() {
        return _bestResponse != null;
    }

    /**
     * Indicate that first 6xx has been received.
     *
     * If already set, true is returned. If not set false is returned and true is
     * saved for next invocation.
     *
     * @return any previous set returns true otherwise false
     */
    private synchronized boolean setAndTest6xx() {
        if (_is6xx) {
            return true;
        }

        _is6xx = true;

        return false;
    }

    private synchronized boolean has6xx() {
        return _is6xx;
    }

    private void startProxy(boolean isSaved) {
        if (getCreatedTarget().getTargetSets().isEmpty()) {
            throw new IllegalStateException(
                "Must create branches before startProxy");
        }

        if (hasBestResponse() || has6xx()) {
            throw new IllegalStateException("Proxy has already completed");
        }

        TargetSet t = null;

        synchronized (this) {
            // move created target set to parallel or sequential target set
            // and clear the created one...
            if (getParallel()) {
                t = new ParallelTargetSet(getCreatedTarget(), getRecurse());
            } else {
                t = new SequentialTargetSet(getCreatedTarget(), getRecurse());
            }

            getCreatedTarget().getTargetSets().clear();

            if (isSaved) {
                // add this target set to the total set
                getTargetSets().add(t);
            }
        }

        // time to proxy
        t.proxyTo();
    }

    public void proxyTo(SipServletRequestImpl req, URI uri)
        throws IllegalStateException {
        List<URI> uris = new ArrayList<URI>(1);
        uris.add(uri);
        proxyTo(req, uris);
    }

    public void proxyTo(SipServletRequestImpl req, List<?extends URI> uris)
        throws IllegalStateException {
        createProxyBranchesIntern(uris);
        startProxy(true);
    }

    List<ProxyBranchImpl> recurseTo(SipServletRequestImpl req,
        List<?extends URI> uris) throws IllegalStateException {
        List<ProxyBranchImpl> l = createRecursedProxyBranchesIntern(uris);
        // should be saved in ProxyBranch that started this recursion
        startProxy(false);

        return l;
    }

    /**
     * Test whether this is the first ProxyBranch or not. Returns true for first
     * caller and false otherwise.
     *
     * @return true for first caller and false otherwise
     */
    public synchronized boolean isFirstProxyBranchSetAndTest() {
        if (_isFirst) {
            _isFirst = false;

            return true;
        }

        return false;
    }

    /**
     * Creates a proxy facade, related to the specified request, for this proxy.
     *
     * @param request
     *           the request to relate the proxy facade to
     * @return a proxy facade for this proxy
     */
    public Proxy getFacade(SipServletRequestImpl request) {
        return new ProxyFacade(this, request);
    }

    /**
     * This class is a facade for setting record-route parameters of a request.
     */
    private static final class RecordRouteURI implements SipURI {
        private static final String SET_RR_COMPONENTS_DISALLOWED = "Not allowed to set Record-Route components.";
        private static final String SET_RR_PARAMS_DISALLOWED = "Not allowed to set SIP URI parameters on Record-Route.";
        private final SipServletRequestImpl req;

        private RecordRouteURI(SipServletRequestImpl req) {
            this.req = req;
        }

        public Object clone() {
            throw new UnsupportedOperationException();
        }

        public String getHeader(String name) {
            throw new UnsupportedOperationException();
        }

        public Iterator getHeaderNames() {
            throw new UnsupportedOperationException();
        }

        public String getHost() {
            throw new UnsupportedOperationException();
        }

        public boolean getLrParam() {
            throw new UnsupportedOperationException();
        }

        public String getMAddrParam() {
            throw new UnsupportedOperationException();
        }

        public String getMethodParam() {
            throw new UnsupportedOperationException();
        }

        public String getParameter(String name) {
            return req.getRecordRouteURIParam(name);
        }

        public Iterator getParameterNames() {
            return req.getRecordRouteURIParamNames();
        }

        public int getPort() {
            throw new UnsupportedOperationException();
        }

        public int getTTLParam() {
            throw new UnsupportedOperationException();
        }

        public String getTransportParam() {
            throw new UnsupportedOperationException();
        }

        public String getUser() {
            throw new UnsupportedOperationException();
        }

        public String getUserParam() {
            throw new UnsupportedOperationException();
        }

        public String getUserPassword() {
            throw new UnsupportedOperationException();
        }

        public boolean isSecure() {
            throw new UnsupportedOperationException();
        }

        public void removeParameter(String name) {
            req.removeRecordRouteURIParam(name);
        }

        public void setHeader(String name, String value) {
            throw new UnsupportedOperationException();
        }

        public void setHost(String host) {
            throw new IllegalArgumentException(SET_RR_COMPONENTS_DISALLOWED);
        }

        public void setLrParam(boolean lr) {
            throw new IllegalArgumentException(SET_RR_PARAMS_DISALLOWED);
        }

        public void setMAddrParam(String mAddr) {
            throw new IllegalArgumentException(SET_RR_PARAMS_DISALLOWED);
        }

        public void setMethodParam(String methodParam) {
            throw new IllegalArgumentException(SET_RR_PARAMS_DISALLOWED);
        }

        public void setParameter(String name, String value) {
            req.setRecordRouteURIParam(name, value);
        }

        public void setPort(int port) {
            throw new IllegalArgumentException(SET_RR_COMPONENTS_DISALLOWED);
        }

        public void setSecure(boolean sips) {
            throw new IllegalArgumentException(SET_RR_COMPONENTS_DISALLOWED);
        }

        public void setTTLParam(int ttl) {
            throw new IllegalArgumentException(SET_RR_PARAMS_DISALLOWED);
        }

        public void setTransportParam(String transport) {
            throw new IllegalArgumentException(SET_RR_PARAMS_DISALLOWED);
        }

        public void setUser(String user) {
            throw new UnsupportedOperationException();
        }

        public void setUserParam(String userParam) {
            throw new IllegalArgumentException(SET_RR_PARAMS_DISALLOWED);
        }

        public void setUserPassword(String password) {
            throw new UnsupportedOperationException();
        }

        public String getScheme() {
            throw new UnsupportedOperationException();
        }

        public boolean isSipURI() {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * This class is a facade to a proxy and relates the proxy to a certain
     * request.
     *
     * @author ejoelbi
     *
     */
    private static class ProxyFacade implements Proxy {
        private ProxyImpl delegate;
        private SipServletRequestImpl req;

        public ProxyFacade(ProxyImpl proxy, SipServletRequestImpl reqImpl) {
            this.delegate = proxy;
            this.req = reqImpl;
        }

        public void cancel() {
            delegate.cancel();
        }

        public SipServletRequest getOriginalRequest() {
            return delegate.getOriginalRequest();
        }

        public boolean getParallel() {
            return delegate.getParallel();
        }

        public boolean getRecordRoute() {
            return delegate.getRecordRoute();
        }

        public SipURI getRecordRouteURI() {
            if (!delegate.getRecordRoute()) {
                throw new IllegalStateException("Record-Routing is not enabled");
            }

            return delegate.getRecordRouteURI(req);
        }

        public boolean getRecurse() {
            return delegate.getRecurse();
        }

        public int getSequentialSearchTimeout() {
            return delegate.getSequentialSearchTimeout();
        }

        public boolean getStateful() {
            return delegate.getStateful();
        }

        public boolean getSupervised() {
            return req.getSupervised();
        }

        public void proxyTo(URI uri) {
            delegate.proxyTo(req, uri);
        }

        public void proxyTo(List<?extends URI> uris) {
            delegate.proxyTo(req, uris);
        }

        public void setParallel(boolean parallel) {
            delegate.setParallel(parallel);
        }

        public void setRecordRoute(boolean recordRoute) {
            delegate.setRecordRoute(recordRoute);
        }

        public void setRecurse(boolean recurse) {
            delegate.setRecurse(recurse);
        }

        public void setSequentialSearchTimeout(int seconds) {
            delegate.setSequentialSearchTimeout(seconds);
        }

        public void setStateful(boolean stateful) {
            delegate.setStateful(stateful);
        }

        public void setSupervised(boolean supervised) {
            req.setSupervised(supervised);
        }

        public List<ProxyBranch> createProxyBranches(List<?extends URI> targets) {
            return delegate.createProxyBranches(targets);
        }

        public boolean getAddToPath() {
            // TODO Auto-generated method stub
            return false;
        }

        public SipURI getPathURI() {
            // TODO Auto-generated method stub
            return null;
        }

        public ProxyBranch getProxyBranch(URI uri) {
            return delegate.getProxyBranch(uri);
        }

        public List<ProxyBranch> getProxyBranches() {
            return delegate.getProxyBranches();
        }

        public int getProxyTimeout() {
            return delegate.getProxyTimeout();
        }

        public void setAddToPath(boolean p) {
            // TODO Auto-generated method stub
        }

        public void setOutboundInterface(SipURI uri) {
            // TODO Auto-generated method stub
        }

        public void setProxyTimeout(int seconds) {
            delegate.setProxyTimeout(seconds);
        }

        public void startProxy() {
            delegate.startProxy(true);
        }
    }
}
