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

import com.ericsson.ssa.config.Config;
import com.ericsson.ssa.config.ConfigFactory;
import com.ericsson.ssa.config.Constants;
import com.ericsson.ssa.config.lease.Lease;
import com.ericsson.ssa.config.lease.LeaseExpiredException;
import com.ericsson.ssa.container.SipBindingResolver;
import com.ericsson.ssa.sip.AddressImpl;
import com.ericsson.ssa.sip.Dispatcher;
import com.ericsson.ssa.sip.Header;
import com.ericsson.ssa.sip.Layer;
import com.ericsson.ssa.sip.LayerHelper;
import com.ericsson.ssa.sip.MultiLineHeader;
import com.ericsson.ssa.sip.SipServletRequestImpl;
import com.ericsson.ssa.sip.SipServletResponseImpl;
import com.ericsson.ssa.sip.ViaImpl;
import com.ericsson.ssa.sip.dns.SipTransports;
import com.ericsson.ssa.sip.dns.TargetTuple;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;

// inserted by hockey (automatic)
import java.util.logging.Logger;


/**
 * @author ekrigro TransactionManader is a Layered Dispatcher TODO Clean up in
 *         the transaction Map
 */
public class TransactionManager implements Layer {
    private static final Logger _log = Logger.getLogger("SipContainer");
    private static TransactionManager _tm = null;
    private Layer _nextLayer;
    private Map<String, ClientTransaction> ctMap = new ConcurrentHashMap<String, ClientTransaction>();
    private Map<String, ServerTransaction> stMap = new ConcurrentHashMap<String, ServerTransaction>();
    private ConcurrentHashMap<String, Object> stLockMap = new ConcurrentHashMap<String, Object>();
    private AtomicLong m_EasSipServerTransactions = new AtomicLong();
    private AtomicLong m_EasSipClientTransactions = new AtomicLong();
    private AtomicLong m_EasTotalSipTransactionTime = new AtomicLong();
    private AtomicLong m_EasTotalSipTransactionCount = new AtomicLong();

    /**
     * _timerT1 is defaulted to 500 here, but its actual runtime value is set
     * from user preferences via the setTimerT1 method.
     */
    long _timerT1 = 500;

    /**
     * _timerT2 is defaulted to 4000 here, but its actual runtime value is set
     * from user preferences via the setTimerT2 method.
     */
    long _timerT2 = 4000;

    /**
     * _timerT4 is defaulted to 4000 here, but its actual runtime value is set
     * from user preferences via the setTimerT4 method.
     */
    long _timerT4 = 5000;
    private String myHost = null;
    private MessageDigest md = null;
    private Config _config = ConfigFactory.getConfig();

    private TransactionManager() {
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException ignore) {
        }
    }

    private ServerTransaction getServerTransaction(String id) {
        ServerTransaction st = null;
        Object mutex = stLockMap.get(id);

        if (mutex != null) {
            synchronized (mutex) {
                st = stMap.get(id);
            }
        }

        return st;
    }

    private void invokeCreatedOrFetchedServerTransaction(
        SipServletRequestImpl req, String id) {
        boolean isTransactionCreated = false;
        ServerTransaction st = null;
        ServerTransaction stCancel = null;
        SipServletResponseImpl resp = null;

        Object newMutex = new Object();
        Object mutex = stLockMap.putIfAbsent(id, newMutex);

        if (mutex == null) {
            mutex = newMutex;
        }

        synchronized (mutex) {
            st = stMap.get(id);

            if (req.getMethod().equals("CANCEL")) {
                if (st != null) {
                    // lets create the cancel transaction.
                    // to distinguish cancel from original transaction
                    // add method to branch id...
                    id = id + req.getMethod();
                    stCancel = new NonInviteServerTransaction(id, req);
                    req.pushTransactionDispatcher(stCancel); // Push the new
                                                             // ST

                    req.pushApplicationDispatcher(this);
                    stMap.put(id, stCancel);
                } else {
                    // orginal transaction to cancel is gone...
                    resp = req.createTerminatingResponse(481);
                    resp.setRemote(req.getRemote());
                }
            } else if (st == null) {
                st = req.getMethod().equals("INVITE")
                    ? new InviteServerTransaction(id, req)
                    : new NonInviteServerTransaction(id, req);
                req.pushTransactionDispatcher(st); // Push the new ST
                req.pushApplicationDispatcher(this);
                stMap.put(id, st);
                isTransactionCreated = true;
            }
        }

        // invoke outside synchronization block...
        if (resp != null) {
            // orginal transaction to cancel is gone, lets reply...
            resp.popDispatcher().dispatch(resp);
        } else if (stCancel != null) {
            // lets inform the original transaction
            // that a cancel request is pending...
            st.handleCancel(stCancel);
        } else if (isTransactionCreated) {
            LayerHelper.next(req, this, _nextLayer);
        } else {
            st.handle(req);
        }
    }

    /*
     * Initial method for incomming ST transactions
     */
    public void next(SipServletRequestImpl req) {
        // Find existing or new server transaction
        // TODO - make lazy creation for stateless
        String method = req.getMethod();
        ServerTransaction st = null;
        String id = getBranchId(req);

        // Get the branch from the top VIA
        if (id != null) {
            // Got the branchId now see if there is a match
            // TODO check also METHOD & sent by value
            //
            // Must separate ACK for 2xx and non-2xx:
            // ACK for non-2xx has same branch id as INVITE
            // and must be sent to transaction. If transaction
            // was not found forward it, then its assumed to be
            // ACK for 2xx...
            try {
                if (method.equals("ACK")) {
                    st = getServerTransaction(id);

                    if (st == null) {
                        LayerHelper.next(req, this, _nextLayer);
                    } else {
                        st.handle(req);
                    }
                } else {
                    invokeCreatedOrFetchedServerTransaction(req, id);
                }
            } catch (RuntimeException re) {
                removeServerTransaction(id);
                throw (re);
            }
        } else {
            // No branch in via
            if (_log.isLoggable(Level.FINE)) {
                _log.log(Level.FINE, "No Branch in Via header " +
                    req.toString());
            }

            SipServletResponseImpl resp = (SipServletResponseImpl) req.createResponse(400,
                    "No Branch in Via header");
            Dispatcher d = resp.popDispatcher();

            if (d != null) {
                d.dispatch(resp);
            }
        }
    }

    /*
     * Initial method for incomming CT transactions
     */
    public void next(SipServletResponseImpl resp) {
        boolean stopLayer = false;

        // Pass it on to the client transaction if there is one
        String id = getBranchId(resp);

        if (resp.getMethod().equals("CANCEL")) {
            id = id + resp.getMethod();
        }

        ClientTransaction ct = ctMap.get(id);

        if (ct != null) {
            stopLayer = ct.handle(resp);
        } else {
            if (_log.isLoggable(Level.FINE)) {
                _log.log(Level.SEVERE,
                    "Can't find matching transaction - Terminating");
            }

            return;
        }

        // POP via
        Header via = resp.getRawHeader(Header.VIA);
        via.setReadOnly(false);

        ListIterator<String> li = via.getValues();
        String topVia = li.next();

        if ((topVia != null) && resp.getMethod().equals("INVITE")) {
            // An INVITE response need to save the topVia, which is used
            // by CANCEL to find it's way to the correct transaction...
            Header viaOfCancel = new MultiLineHeader(Header.VIA, true);
            ViaImpl v = new ViaImpl(topVia);
            viaOfCancel.setValue(v.toString(), true);
            resp.setCancelVia(viaOfCancel);
        }

        // TODO could match to see that it's the right host'n port
        li.remove();
        via.setReadOnly(true);

        if (_log.isLoggable(Level.FINE)) {
            _log.log(Level.FINE, "Removing via = " + topVia);
        }

        if (!stopLayer) {
            try {
                LayerHelper.next(resp, this, _nextLayer);
            } catch (RuntimeException re) {
                removeClientTransaction(id);
                throw (re);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see com.ericsson.ssa.sip.Layer#registerNext(com.ericsson.ssa.sip.Layer)
     */
    public void registerNext(Layer layer) {
        _nextLayer = layer;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.ericsson.ssa.sip.Dispatcher#dispatch(com.ericsson.ssa.sip.SipServletRequestImpl)
     */
    public void dispatch(SipServletRequestImpl req) {
        // Sending out request UAC or Proxy
        // Slightly special handling of ACK and CANCEL
        String method = req.getMethod();

        if (!method.equals("CANCEL")) {
            Header via = req.getRawHeader(Header.VIA);

            if (via == null) {
                via = new MultiLineHeader(Header.VIA, true);
            }

            int port = -1;

            if (Boolean.getBoolean("sip.module.use_new_config")) {
                try {
                    Lease<TargetTuple[]> lease = SipBindingResolver.instance()
                                                                   .lease(SipBindingResolver.PUBLIC_BINDING_CTX);
                    System.out.println("TransactionManager: \n" +
                        lease.getResource()[0]);
                    myHost = lease.getResource()[0].getIP();
                    port = lease.getResource()[0].getPort();
                } catch (LeaseExpiredException e) {
                    //TODO logging
                    e.printStackTrace();
                }
            } else {
                port = Integer.parseInt(_config.get(Constants.SIP_PUBLIC_PORT));

                //If outgoing request is to be sent over TLS
                if (req.getRemote().getProtocol().ordinal() == SipTransports.TLS) {
                    port++; //Might later change to public TLS port config value
                }

                if (myHost == null) {
                    myHost = _config.get(Constants.SIP_PUBLIC_HOST);
                }
            }

            ViaImpl v = new ViaImpl(req.getProtocol(),
                    req.getTransport().toUpperCase(), myHost, port);
            String id = Transaction.generateBranch();
            v.setParameter(ViaImpl.PARAM_BRANCH, id);

            if (_log.isLoggable(Level.FINE)) {
                _log.log(Level.FINE, "Adding via = " + v);
            }

            via.setValue(v.toString(), true);
            req.setHeader(via);

            if (!method.equals("ACK")) {
                ClientTransaction ct = method.equals("INVITE")
                    ? new InviteClientTransaction(id, req)
                    : new NonInviteClientTransaction(id, req);
                ctMap.put(id, ct);
            }
        } else {
            // CANCEL
            String id = getBranchId(req) + req.getMethod();
            ClientTransaction ct = new NonInviteClientTransaction(id, req);
            ctMap.put(id, ct);
        }

        Dispatcher d = req.popDispatcher();

        if (d != null) {
            d.dispatch(req);
        }
    }

    /*
     * Nothing todo here more then pop. Should only get her for stateless proxy
     * Responses should go on ServerTransaction
     */
    public void dispatch(SipServletResponseImpl resp) {
        Dispatcher d = resp.popDispatcher();

        if (d != null) {
            d.dispatch(resp);
        }
    }

    public synchronized static TransactionManager getInstance() {
        if (_tm != null) {
            return _tm;
        }

        return _tm = new TransactionManager();
    }

    private String getBranchId(SipServletResponseImpl resp) {
        ViaImpl via = null;

        // Get the branch from the top VIA
        String vstr = resp.getHeader(Header.VIA);

        if ((vstr != null) && ((via = new ViaImpl(vstr)) != null)) {
            String id = null;

            // TODO Add a check of the magic cockie to the parsing of ViaImpl
            if ((id = via.getParameter(ViaImpl.PARAM_BRANCH)) != null) {
                return id;
            }
        }

        return null;
    }

    private String getBranchId(SipServletRequestImpl req) {
        ViaImpl via = null;

        // Get the branch from the top VIA
        String vstr = req.getHeader(Header.VIA);

        if ((vstr != null) && ((via = new ViaImpl(vstr)) != null)) {
            String id = null;

            // TODO Add a check of the magic cockie to the parsing of ViaImpl
            if ((id = via.getParameter(ViaImpl.PARAM_BRANCH)) != null) {
                return id;
            } else// Calculate an id according to rules from rfc2543
             {
                // RFC3261 chapter 17.2.3
                StringBuilder sb = new StringBuilder();
                sb.append(req.getRequestURI().toString());
                sb.append(req.getFrom().getParameter(AddressImpl.TAG_PARAM));
                // sb.append(req.getTo().getParameter(AddressImpl.TAG_PARAM));
                sb.append(req.getCallId());
                sb.append(req.getCSeqNumber());
                sb.append(via.toString());

                if (_log.isLoggable(Level.FINE)) {
                    _log.log(Level.FINE,
                        "Before hash, input is : " + sb.toString());
                }

                byte[] hash = md.digest(sb.toString().getBytes());

                sb = new StringBuilder();

                for (int i = 0; i < hash.length; i++) {
                    String d = Integer.toHexString(new Byte(hash[i]).intValue() &
                            0xFF);

                    if (d.length() == 1) {
                        sb.append('0');
                    }

                    sb.append(d);
                }

                if (_log.isLoggable(Level.FINE)) {
                    _log.log(Level.FINE,
                        "Generated id, hash is : " + sb.toString());
                }

                _log.log(Level.WARNING,
                    "Using generated transactionID rfc2543 style : " +
                    sb.toString() + via);

                return sb.toString();
            }
        }

        return null;
    }

    public void remove(ClientTransaction t) {
        String id = t.getTransactionId();
        removeClientTransaction(id);
    }

    private void removeClientTransaction(String id) {
        if (id != null) {
            ctMap.remove(id);
        }
    }

    public void remove(ServerTransaction t) {
        String id = t.getTransactionId();
        removeServerTransaction(id);
    }

    private void removeServerTransaction(String id) {
        if (id != null) {
            Object mutex = stLockMap.get(id);

            if (mutex == null) {
                _log.warning("Failed get read mutex");
            } else {
                synchronized (mutex) {
                    stMap.remove(id);
                }
            }

            stLockMap.remove(id);
        }
    }

    /*
     * CLIENT TRANSACTION 1. If the response has the same value of the branch
     * parameter in the top Via header field as the branch parameter in the top
     * Via header field of the request that created the transaction. 2. If the
     * method parameter in the CSeq header field matches the method of the
     * request that created the transaction. The method is needed since a CANCEL
     * request constitutes a different transaction, but shares the same value of
     * the branch parameter.
     */

    /*
     * SERVER TRANSACTION 1. the branch parameter in the request is equal to the
     * one in the top Via header field of the request that created the
     * transaction, and 2. the sent-by value in the top Via of the request is
     * equal to the one in the request that created the transaction, and 3. the
     * method of the request matches the one that created the transaction, except
     * for ACK, where the method of the request that created the transaction is
     * INVITE.
     */
    public void setTimerT1(Integer t) {
        _timerT1 = t;
    }

    public long getTimerT1() {
        return _timerT1;
    }

    public void setTimerT2(Integer t) {
        _timerT2 = t;
    }

    public long getTimerT2() {
        return _timerT2;
    }

    public void setTimerT4(Integer t) {
        _timerT4 = t;
    }

    public long getTimerT4() {
        return _timerT4;
    }

    /* Performance Counters */
    public long getEasSipClientTransactions() {
        return m_EasSipClientTransactions.longValue();
    }

    public long getEasSipServerTransactions() {
        return m_EasSipServerTransactions.longValue();
    }

    public long getEasTotalSipTransactionTime() {
        return m_EasTotalSipTransactionTime.longValue();
    }

    public long getEasTotalSipTransactionCount() {
        return m_EasTotalSipTransactionCount.longValue();
    }

    // package access
    void recordTransactionTime(long transactionTime) {
        m_EasTotalSipTransactionTime.addAndGet(transactionTime);
        m_EasTotalSipTransactionCount.incrementAndGet();
    }

    // package access
    void incrEasSipClientTransactions() {
        m_EasSipClientTransactions.incrementAndGet();
    }

    // package access
    void incrEasSipServerTransactions() {
        m_EasSipServerTransactions.incrementAndGet();
    }
}
