/*
 * 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.container.SipContainerThreadPool;
import com.ericsson.ssa.sip.timer.GeneralTimer;
import com.ericsson.ssa.sip.timer.GeneralTimerListener;
import com.ericsson.ssa.sip.timer.TimerServiceImpl;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.sip.Proxy;
import javax.servlet.sip.ProxyBranch;
import javax.servlet.sip.ServletParseException;
import javax.servlet.sip.SipServletRequest;
import javax.servlet.sip.SipServletResponse;
import javax.servlet.sip.SipURI;
import javax.servlet.sip.TooManyHopsException;
import javax.servlet.sip.URI;


/**
 * Part of the Proxy implementation.
 *
 * @author ehsroha
 */
public class ProxyBranchImpl extends TargetSet implements ProxyBranch,
    GeneralTimerListener {
    protected static final Logger _log = (Logger) Logger.getLogger(
            "SipContainer");
    static int TimerC = 180; // >3min
    private SipServletResponseImpl _bestResponse = null;
    private SipServletResponse _lastResponse = null;
    private final SipServletRequestImpl _request;
    private GeneralTimer _timer = null;
    private int _proxyBranchTimeout = 0; // important to keep zero
    private SipURI _outboundURI;
    private List<ProxyBranchImpl> _proxyBranches = null;
    private boolean _isCanceled = false;
    private boolean _pendingCancel = false;
    private SipServletResponseImpl _provisional = null;

    /**
     * The constructor of the Proxy Branch implementation.
     *
     * @param request
     *           the request which this branch should proxy to
     */
    public ProxyBranchImpl(ProxyImpl p, SipServletRequestImpl request) {
        super(p);
        _request = request;
    }

    protected List<TargetSet> getTargetSets() {
        if (getRecursedProxyBranchesImpl() == null) {
            return super.getTargetSets();
        }

        return new ArrayList<TargetSet>(getRecursedProxyBranchesImpl());
    }

    public synchronized SipServletResponseImpl findBestResponse() {
        if (_bestResponse != null) {
            // Already 3xx redirection?
            if (_bestResponse.isAlreadyRedirected() &&
                    (getRecursedProxyBranchesImpl() != null)) {
                return super.findBestResponse();
            }
        }

        return _bestResponse;
    }

    /**
     * Returns the best response.
     *
     * @return the best response.
     */
    public synchronized SipServletResponseImpl getBestResponse() {
        return _bestResponse;
    }

    /**
     * Rule 1: Set best response if no response priviously existed for this
     * branch.
     * <p>
     * Rule 2: 2xx response will override any response
     * <p>
     * Rule 3: 6xx response will override any response except 2xx
     * <p>
     * Rule 4: Except rule 1 and 2 a response 3xx-5xx will override a previous
     * set response, which had a higher status code.
     *
     * @param resp
     */
    private synchronized void setBestResponse(SipServletResponseImpl resp) {
        // 700 is one higher than possible
        int bestStatus = (_bestResponse == null) ? 700 : _bestResponse.getStatus();

        if ((bestStatus / 100) != 2) {
            int status = resp.getStatus() / 100;

            if (status == 2) {
                _bestResponse = resp;
            } else if (status == 6) {
                _bestResponse = resp;
            } else if (((status > 2) && (status < 6) &&
                    ((bestStatus / 100) != 6)) &&
                    (resp.getStatus() < bestStatus)) {
                _bestResponse = resp;
            }
        }
    }

    /**
     * Whether this ProxyBranchImpl is matching the URI or not.
     *
     * @param uri
     *           the incoming uri
     * @return this instance if it is matching the URI otherwise null
     */
    public ProxyBranchImpl findBranch(URI uri) {
        if (getRecurse()) {
            ProxyBranchImpl branch = super.findBranch(uri);

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

        return uri.equals(_request.getRequestURI()) ? this : null;
    }

    public void addTopLevelBranch(List<ProxyBranchImpl> branches) {
        branches.add(this);
    }

    private void startTimer(SipServletResponseImpl resp) {
        if (_timer == null) {
            synchronized (this) {
                if (_timer == null) {
                    int time = -1;

                    if (getParallel()) {
                        // In case the proxy is a parallel proxy then
                        // getProxyBranchTimeout()
                        // can only set the timeout value of this branch to a value
                        // lower
                        // than
                        // the value in the proxy Proxy.getProxyTimeout()
                        time = ((getProxyBranchTimeout() > 0) &&
                            (getProxyBranchTimeout() < getProxy()
                                                           .getProxyTimeout()))
                            ? (getProxyBranchTimeout() * 1000)
                            : (getProxy().getProxyTimeout() * 1000);
                    } else {
                        // JSR289, 10.2.4.3 Sequential Search Timeout
                        // 
                        // if ProxyBranchTimeout is set, override ProxyTimeout
                        time = (getProxyBranchTimeout() > 0)
                            ? (getProxyBranchTimeout() * 1000)
                            : (getProxy().getProxyTimeout() * 1000);
                        // if higher than TimerC use TimerC
                        time = (time < (TimerC * 1000)) ? time : (TimerC * 1000);
                    }

                    if (_log.isLoggable(Level.FINE)) {
                        _log.log(Level.FINE,
                            "start timer " + (time / 1000) +
                            " sec for branch = " + _request.getRequestURI());
                    }

                    // needed for CANCEL to reach next hop
                    setProvisionalResponse(resp);

                    // start timer
                    _timer = TimerServiceImpl.getInstance()
                                             .createTimer(this, time, false,
                            null);
                }
            }
        }
    }

    private synchronized void stopTimer() {
        if (_timer != null) {
            _timer.cancel();
            _timer = null;
        }
    }

    /**
     * Response management of the proxy branch
     * @param resp
     * @param pc
     * @return true if it is a virtual proxy branch otherwise false
     */
    public boolean doInitialResponse(SipServletResponseImpl resp,
        ProxyContext pc) {
        setLastResponse(resp);

        int status = resp.getStatus() / 100;

        if (status == 1) {
            if (resp.getMethod().equals("INVITE") && isPendingCancel()) {
                final SipServletRequestImpl cancel;

                synchronized (this) {
                    // needed for cancel to reach next hop-node
                    setProvisionalResponse(resp);

                    if (_log.isLoggable(Level.INFO)) {
                        _log.log(Level.INFO,
                            "CANCEL is pending, lets cancel this branch " +
                            _request.getRequestURI() + " for response " +
                            resp.getStatus());
                    }

                    cancel = cancelIntern();

                    if (cancel != null) {
                        setCanceled();
                    }
                }

                if (cancel != null) {
                    // lets CANCEL this branch outside synchronized block...
                    SipContainerThreadPool.getInstance().execute(new Callable() {
                            // execute in another thread...
                            public Object call() throws Exception {
                                cancel.popDispatcher().dispatch(cancel);
                                return null;
                            }
                        });
                }
            } else {
                // lets start proxy branch timer
                startTimer(resp);
            }

            getProxyImpl().invokeServletAndForward(resp, pc);
        } else {
            // we have a final response
            setBestResponse(resp);

            // need to stop timer
            stopTimer();

            if ((status == 3) && getProxy().getRecurse()) {
                // If recurse is set and this is a 3xx response,
                // fork new requests to list of Contact
                AddressImpl address = null;
                URI uri = null;
                List<URI> contacts = new ArrayList<URI>();

                for (ListIterator<String> contactsIt = resp.getHeaders(
                            Header.CONTACT); contactsIt.hasNext();) {
                    String contact = (String) contactsIt.next();

                    try {
                        address = new AddressImpl(contact);
                        uri = address.getURI();
                        contacts.add(uri);
                    } catch (ServletParseException e) {
                        if (_log.isLoggable(Level.WARNING)) {
                            _log.log(Level.WARNING,
                                "Could not recurse on Contact = " + contact);
                        }
                    }
                }

                if (contacts.size() > 0) {
                    List<ProxyBranchImpl> branches = getProxyImpl()
                                                         .recurseTo(resp.getRequestImpl(),
                            contacts);

                    if (!branches.isEmpty()) {
                        addRecursedProxyBranches(branches);
                    }
                }

                // inform that this 3xx redirect has forked to all its contacts.
                resp.setAlreadyRedirected();
            } else {
                // invoke next branch of parent target set
                if (getParent().hasNext()) {
                    if (_log.isLoggable(Level.FINE)) {
                        _log.log(Level.FINE, "start next branch");
                    }

                    getParent().next();
                }
            }
        }

        return false;
    }

    public Proxy getProxy() {
        try {
            return getProxyImpl().getOriginalRequestImpl().getProxy();
        } catch (TooManyHopsException e) {
            return null;
        }
    }

    public int getProxyBranchTimeout() {
        return _proxyBranchTimeout;
    }

    private synchronized List<ProxyBranchImpl> getRecursedProxyBranchesImpl() {
        return _proxyBranches;
    }

    private synchronized void addRecursedProxyBranches(
        List<ProxyBranchImpl> branches) {
        if (_proxyBranches == null) {
            _proxyBranches = branches;
        } else {
            _proxyBranches.addAll(branches);
        }
    }

    public List<ProxyBranch> getRecursedProxyBranches() {
        List<ProxyBranchImpl> branches = getRecursedProxyBranchesImpl();

        return (branches == null) ? null : new ArrayList<ProxyBranch>(branches);
    }

    private synchronized void setLastResponse(SipServletResponse resp) {
        _lastResponse = resp;
    }

    public synchronized SipServletResponse getResponse() {
        return _lastResponse;
    }

    public SipServletRequest getRequest() {
        return _request;
    }

    public SipServletRequestImpl getRequestImpl() {
        return _request;
    }

    @Override
    public boolean getParallel() {
        return getParent().getParallel();
    }

    @Override
    public boolean isStarted() {
        return getParent().isStarted();
    }

    @Override
    public boolean getRecurse() {
        return getParent().getRecurse();
    }

    public void setOutboundInterface(SipURI uri) {
        _outboundURI = uri;
    }

    public void setProxyBranchTimeout(int seconds) {
        _proxyBranchTimeout = seconds;
    }

    public void timeout(GeneralTimer timer) {
        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE,
                "timer fired for branch = " + _request.getRequestURI());
        }

        if (getBestResponse() == null) {
            // cancel this branch
            cancel();

            // invoke next branch of parent target set
            if (getParent().hasNext()) {
                if (_log.isLoggable(Level.FINE)) {
                    _log.log(Level.FINE, "start next branch");
                }

                getParent().next();
            }
        }
    }

    public void proxyTo() throws IllegalStateException {
        prepareProxyTo();
        
        // the request need to be cloned
        final SipServletRequestImpl clone = (SipServletRequestImpl) getRequestImpl()
                                                                        .clone();
        // lets add the proxy to the transaction path
        clone.pushTransactionDispatcher(getRequestImpl().getProxyContext());
        clone.setTransactionRequest(getRequestImpl());
                
        // push the request to the dispatcher in a new thread
        SipContainerThreadPool.getInstance().execute(new Callable() {
                public Object call() throws Exception {
                    if (_log.isLoggable(Level.FINE)) {
                        _log.log(Level.FINE,
                            "Proxy request " + clone.toDebugString() + ", " +
                            toString());
                    }

                    // push the request to the dispatcher
                    clone.popDispatcher().dispatch(clone);
                    return null;
                }
            });

        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE, "Lets leave...");
        }
    }

    protected void prepareProxyTo() throws IllegalStateException {
        if (getParent() == null) {
            throw new IllegalStateException(
                "Need to add parent target set before calling.");
        }

        // analyze how to add proxy to path...
        if (getProxyImpl().isFirstProxyBranchSetAndTest()) {
            // lets add the proxy context to the application path
            getRequestImpl().getDialog()
                .addToPath(getRequestImpl().getProxyContext());
        } else {
            // need to clone the dialog if this is not the first branch
            DialogFragment clone = (DialogFragment) getRequestImpl().getDialog()
                                                        .cloneFromCallerToCalleeUntil(getRequestImpl()
                                                                                          .getProxyContext(),
                    false);
            // update dialog of request...
            getRequestImpl().setDialog(clone);
            // need to add proper fragment id...
            getRequestImpl().setFragmentId(clone.getFragmentId());

            if (getRequestImpl().isContactIndicated()) {
                // since it's a new fragment id the contact must be replaced...
                Header contact = getRequestImpl().getRawHeader(Header.CONTACT);
                contact.setReadOnly(false);
                contact.removeValues();
                SessionManager.getInstance().addContact(getRequestImpl());
            }

            // need to update transaction path since there are new nodes.
            // lets start by cleaning the top of the stack...
            PathNode p = null;
            int size = 0;

            // need to take it in reverse order since its a stack...
            Iterator<PathNode> i = clone.getCallee2CallerPath();

            if (i.hasNext()) {
                // just skip the first since its this proxy context...
                i.next();
            }

            while (i.hasNext()) {
                p = i.next();

                if (getRequestImpl().getSupervised()) {
                    // this is the same as poping the stack, but poping the
                    // stack on a request will pop application stack
                    size = getRequestImpl()._transactionStack.size();
                    getRequestImpl()._transactionStack.remove(size - 1);
                }
            }

            // ...and replace it with the new nodes.
            i = clone.getCallee2CallerPath();

            if (i.hasNext()) {
                // update the request with the new clone...
                ProxyContext pc = (ProxyContext) i.next();
                getRequestImpl().setProxyContext(pc);
            }

            while (i.hasNext()) {
                // add the rest of the nodes..
                p = i.next();

                if (getRequestImpl().getSupervised()) {
                    getRequestImpl().pushTransactionDispatcher(p);
                }
            }
        }

        if (getProxy().getRecordRoute()) {
            // state Record Route in request to inform
            // that a RecordRoute header must be added
            getRequestImpl().indicateRecordRoute();
        }

        if (getProxy().getAddToPath()) {
            // state Path in request to inform
            // that a Path header shall be added
            getRequestImpl().indicatePath();
        }

        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE,
                getRequestImpl().toDebugString() + ", " + toString());
        }
    }

    private void setProvisionalResponse(SipServletResponseImpl resp) {
        _provisional = resp;
    }

    private SipServletResponseImpl getProvisionalResponse() {
        return _provisional;
    }

    private SipServletRequestImpl createCancel() {
        SipServletRequestImpl cancel = _request.createCancelImpl();

        SipServletResponseImpl provisonal = getProvisionalResponse();

        // add saved top via from provisonal response...
        cancel.setHeader(provisonal.getCancelVia());

        // find next pathNode if any...
        PathNode next = provisonal.getPreviousVisited();

        if (next != null) {
            cancel.pushApplicationDispatcher(next);
        }

        // no need to keep reference to provional anymore, save memory
        setProvisionalResponse(null);

        return cancel;
    }

    public boolean isPendingCancel() {
        return _pendingCancel;
    }

    public void setPendingCancel() {
        _pendingCancel = true;
    }

    private synchronized boolean isCanceled() {
        return _isCanceled;
    }

    private void setCanceled() {
        _isCanceled = true;
    }

    public void cancel() {
        SipServletRequestImpl cancel = null;

        synchronized (this) {
            cancel = cancelIntern();
        }

        if (cancel != null) {
            // lets CANCEL this branch...
            if (_log.isLoggable(Level.FINE)) {
                _log.log(Level.FINE,
                    cancel + " will be sent for branch = " +
                    _request.getRequestURI());
            }

            cancel.popDispatcher().dispatch(cancel);

            synchronized (this) {
                setCanceled();
            }
        }
    }

    private SipServletRequestImpl cancelIntern() {
        // don't need to CANCEL if branch has final
        // response already or it has been canceled
        // before or waiting for provisonal response
        SipServletRequestImpl cancel = null;

        if (!isCanceled()) {
            // already final response?
            SipServletResponseImpl bestResp = getBestResponse();

            if (bestResp == null) {
                // no final response exist for the branch
                if (getProvisionalResponse() == null) {
                    // have not got provisonal response
                    if (_log.isLoggable(Level.FINE)) {
                        _log.log(Level.FINE,
                            "pending CANCEL for branch = " +
                            _request.getRequestURI());
                    }

                    // set pending cancel...
                    setPendingCancel();
                } else {
                    // time to create CANCEL request
                    cancel = createCancel();

                    if (_log.isLoggable(Level.FINE)) {
                        _log.log(Level.FINE, cancel.toDebugString());
                    }
                }
            } else {
                if (_log.isLoggable(Level.FINE)) {
                    _log.log(Level.FINE,
                        "Will not CANCEL, already got final response " +
                        bestResp.getStatus() + " for current branch = " +
                        _request.getRequestURI());
                }
            }
        } else {
            if (_log.isLoggable(Level.FINE)) {
                _log.log(Level.FINE,
                    "CANCEL already for branch = " + _request.getRequestURI());
            }
        }

        return cancel;
    }
}
