/*
 * 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 java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.UnsupportedEncodingException;

import java.util.Collections;
import java.util.Iterator;

import javax.servlet.sip.ServletParseException;
import javax.servlet.sip.SipURI;


/**
* @author ekrigro
* @reviewed qmigkra 2006-nov-17
 */
public class SipURIImpl extends URIImpl implements SipURI, Externalizable {
    /**
     *
     */
    private static final long serialVersionUID = 3616450094292218936L;
    static String DUPLICATE_PARAM = "The parsed SIP URI has two parameter with the same name : ";
    public static final String LOOSE_ROUTE_PARAM = "lr";
    public static final String MADDR_PARAM = "maddr";
    public static final String METHOD_PARAM = "method";
    public static final String TTL_PARAM = "ttl";
    public static final String TRANSPORT_PARAM = "transport";
    public static final String USER_PARAM = "user";
    public static final String FRAGID_PARAM = "fid";
    private static SipURIDecoder _uriDecoder = new SipURIDecoder();
    private static SipURIEncoder _uriEncoder = new SipURIEncoder();
    private boolean _secure = false;
    private Ascii7String _user = null;
    private Ascii7String _password = null;
    private Ascii7String _host = null;
    private int _port = -1;

    // private ParameterByteMap     _parameters       = null;
    private ParameterByteMap _headers = null;

    public SipURIImpl() {
    } // used only in parser and serialization

    public SipURIImpl(boolean secure, String user, String host) {
        _secure = secure;
        _user = new Ascii7String(user);
        _host = new Ascii7String(host);
    }

    public SipURIImpl(String protocol, byte[] bytes, int offset)
        throws ServletParseException {
        if (protocol.equals(SipFactoryImpl.SIPS_URI_PROTOCOL)) {
            _secure = true;
        }

        try {
            parse(bytes, offset, bytes.length);
        } catch (RuntimeException e) {
            throw new ServletParseException(
                "Unexpected exception while parsing URI: " + e);
        }
    }

    // For RMI serialization
    public void writeExternal(ObjectOutput output) throws IOException {
        try {
            // _secure, _user, _password, _host, _port, _parameters, _headers
            output.writeBoolean(_secure);

            if (_user != null) { // Special treatment since it can be null and writeUTF can't be used
                output.writeInt(_user.length());
                output.write(_user.getBytes());
            } else {
                output.writeInt(0);
            }

            if (_password != null) {
                output.writeInt(_password.length());
                output.write(_password.getBytes());
            } else {
                output.writeInt(0);
            }

            output.writeUTF(_host.toString());
            output.writeInt(_port);

            if (_parameters != null && _parameters.toArray() != null) {
                byte[] parameter_data = _parameters.toArray();
                output.writeInt(parameter_data.length); // write #bytes
                output.write(parameter_data);
            } else {
                output.writeInt(0);
            }

            if (_headers != null && _headers.toArray() != null) {
                byte[] headers_data = _headers.toArray();
                output.writeInt(headers_data.length); // write #bytes
                output.write(headers_data);
            } else {
                output.writeInt(0);
            }
        } catch (Exception ignore) {
        }
    }

    public void readExternal(ObjectInput input) throws IOException {
        try {
            _secure = input.readBoolean();

            int len = input.readInt();

            if (len > 0) {
                byte[] b = new byte[len];
                input.read(b);
                _user = new Ascii7String(b);
            }

            len = input.readInt();

            if (len > 0) {
                byte[] b = new byte[len];
                input.read(b);
                _password = new Ascii7String(b);
            }

            _host = new Ascii7String(input.readUTF());
            _port = input.readInt();

            len = input.readInt(); // read parameter #bytes
            if (len > 0) {
                byte[] parameter_data = new byte[len];
                int readBytes = input.read(parameter_data, 0, len);

                if (readBytes != len) {
                throw new IOException("failed to read parameter section len:" +
                    len);
                }
                _parameters = new ParameterByteMap(parameter_data, ';');
            }
            
            len = input.readInt(); // read headers #bytes
            if (len > 0) {
                byte[] headers_data = new byte[len];
                int readBytes = input.read(headers_data, 0, len);
    
                if (readBytes != len) {
                    throw new IOException("failed to read headers section len:" +
                        len);
                }
    
                _headers = new ParameterByteMap(headers_data, ';');
            }
        } catch (Exception ignore) {
        }
    }

    private void parse(byte[] bytes, int offset, int length)
        throws ServletParseException {
        int passwdIndex = -1; // colon
        int portIndex = -1; // colon
        int semiIndex = -1;
        int qIndex = -1;
        int atIndex = -1;
        int endHostIndex = length;
        int endParamIndex = length;

        for (int i = offset; i < length; i++) {
            // Control character should not appear in the url.
            if (((bytes[i] >= 0x00) && (bytes[i] < 0x20)) ||
                    (bytes[i] == 0x7F)) {
                throw new ServletParseException(
                    "The URI contains a control character.");
            }

            if (bytes[i] == ':') {
                if (atIndex > 0) {
                    portIndex = i;
                } else {
                    passwdIndex = i;
                }
            } else if ((atIndex == -1) && (bytes[i] == '@')) {
                atIndex = i;
            } else if ((semiIndex == -1) && (bytes[i] == ';')) {
                semiIndex = i;
                endHostIndex = i;
            } else if (bytes[i] == '?') {
                qIndex = i;

                if (semiIndex > 0) {
                    endParamIndex = i;
                } else {
                    endHostIndex = i;
                }

                break;
            }
        }

        if ((passwdIndex > 0) && (atIndex < 0)) {
            portIndex = passwdIndex;
        }

        /*
         * INFO: The user part and parameter parts of the url, could contains
         * escaped characters, so we need to unecape them.
         */
        try {
            // Check user
            try {
                if (atIndex > 0) {
                    if (passwdIndex > 0) {
                        _user = new Ascii7String(new String(bytes, offset,
                                    passwdIndex - offset,
                                    SipFactoryImpl.SIP_CHARSET));
                        _password = new Ascii7String(new String(bytes,
                                    passwdIndex + 1,
                                    atIndex - (passwdIndex + 1),
                                    SipFactoryImpl.SIP_CHARSET));
                    } else {
                        _user = new Ascii7String(new String(bytes, offset,
                                    atIndex - offset, SipFactoryImpl.SIP_CHARSET));
                    }

                    offset = atIndex + 1;
                }

                // Now the hostpart that is mandatory
                if (portIndex > 0) { // Host n port
                    _host = new Ascii7String(bytes, offset, portIndex - offset,
                            SipFactoryImpl.SIP_CHARSET);
                    _port = Integer.parseInt(new String(bytes, portIndex + 1,
                                endHostIndex - (portIndex + 1),
                                SipFactoryImpl.SIP_CHARSET));
                } else { // no port only host
                    _host = new Ascii7String(bytes, offset,
                            endHostIndex - offset, SipFactoryImpl.SIP_CHARSET);
                }
            } catch (IllegalStateException e) {
                throw new ServletParseException("CharacterCodingException");
            }

            offset = endHostIndex + 1;

            // Time for the parameters
            if (semiIndex > 0) {
                int numBytes = endParamIndex - semiIndex;
                _parameters = new ParameterByteMap(bytes, semiIndex, numBytes,
                        ';');
            }

            // And last and least the headers
            if (qIndex > 0) {
                int endIndex = length - 1;
                int numBytes = endIndex - qIndex;
                _headers = new ParameterByteMap(bytes, qIndex, numBytes + 1, '&');
            }
        } catch (UnsupportedEncodingException uee) {
        } // C.C.L.
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getUser()
     */
    public String getUser() {
        try {
            if (_user == null) {
                return null;
            }

            return _uriDecoder.decode(_user.toString());
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setUser(java.lang.String)
     */
    public void setUser(String user) {
        if (user != null) {
            _user = new Ascii7String(_uriEncoder.encodeUserPart(user));
        } else {
            _user = null;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getUserPassword()
     */
    public String getUserPassword() {
        try {
            if (_password == null) {
                return null;
            }

            return _uriDecoder.decode(_password.toString());
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setUserPassword(java.lang.String)
     */
    public void setUserPassword(String passwd) {
        if (_password != null) {
            _password = new Ascii7String(_uriEncoder.encodePasswordPart(passwd));
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getHost()
     */
    public String getHost() {
        return _host.toString();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setHost(java.lang.String)
     */
    public void setHost(String host) {
        _host = new Ascii7String(host);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getPort()
     */
    public int getPort() {
        return _port;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setPort(int)
     */
    public void setPort(int port) {
        if (port < 0) {
            _port = -1;
        } else {
            _port = port;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#isSecure()
     */
    public boolean isSecure() {
        return _secure;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setSecure(boolean)
     */
    public void setSecure(boolean sec) {
        _secure = sec;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getTransportParam()
     */
    public String getTransportParam() {
        return getParameter(TRANSPORT_PARAM);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setTransportParam(java.lang.String)
     */
    public void setTransportParam(String value) {
        setParameter(TRANSPORT_PARAM, value);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getMAddrParam()
     */
    public String getMAddrParam() {
        return getParameter(MADDR_PARAM);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setMAddrParam(java.lang.String)
     */
    public void setMAddrParam(String value) {
        setParameter(MADDR_PARAM, value);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getMethodParam()
     */
    public String getMethodParam() {
        return getParameter(METHOD_PARAM);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setMethodParam(java.lang.String)
     */
    public void setMethodParam(String value) {
        setParameter(METHOD_PARAM, value);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getTTLParam()
     */
    public int getTTLParam() {
        int ttl = -1;
        String value = getParameter(TTL_PARAM);

        if (value != null) {
            try {
                ttl = Integer.parseInt(value);
            } catch (Exception e) {
            } // Bad luck :-)
        }

        return ttl;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setTTLParam(int)
     */
    public void setTTLParam(int value) {
        setParameter(TTL_PARAM, String.valueOf(value));
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getUserParam()
     */
    public String getUserParam() {
        return getParameter(USER_PARAM);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setUserParam(java.lang.String)
     */
    public void setUserParam(String value) {
        setParameter(USER_PARAM, value);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setLrParam(boolean)
     */
    public void setLrParam(boolean lr) {
        if (lr) {
            setParameter(LOOSE_ROUTE_PARAM, "");
        } else {
            removeParameter(LOOSE_ROUTE_PARAM);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getHeader(java.lang.String)
     */
    public String getHeader(String header) {
        if (_headers == null) {
            return null;
        }

        return _headers.get(header);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#setHeader(java.lang.String,
     *      java.lang.String)
     */
    public void setHeader(String header, String value) {
        if (_headers == null) {
            _headers = new ParameterByteMap('&');
        }

        _headers.put(header, value);
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.SipURI#getHeaderNames()
     */
    public Iterator<String> getHeaderNames() {
        if (_headers == null) {
            return Collections.EMPTY_LIST.iterator();
        }

        return _headers.getKeys();
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.URI#getScheme()
     */
    public String getScheme() {
        return _secure ? SipFactoryImpl.SIPS_URI_PROTOCOL
                       : SipFactoryImpl.SIP_URI_PROTOCOL;
    }

    /*
     * (non-Javadoc)
     *
     * @see javax.servlet.sip.URI#isSipURI()
     */
    public boolean isSipURI() {
        return true;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#clone()
     */
    public Object clone() {
        SipURIImpl newUri = new SipURIImpl();
        newUri._secure = _secure;
        // avoid String(String) due to memory overhead when a explicit copy is
        // created
        // copies is Unnecesary because String are immutable!!
        // if (_user != null) newUri._user = new String(_user);
        // if (_password != null) newUri._password = new String(_password);
        // if (_host != null) newUri._host = new String(_host);
        newUri._user = _user;
        newUri._password = _password;
        newUri._host = _host;
        newUri._port = _port;

        if (_parameters != null) {
            newUri._parameters = (ParameterByteMap) _parameters.clone();
            // remove tag Due to strange requirement on Address to not clone tag
            newUri._parameters.remove(AddressImpl.TAG_PARAM);
        }

        if (_headers != null) {
            newUri._headers = (ParameterByteMap) _headers.clone();
        }

        return newUri;
    }

    /*
     * RFC3261 19.1.4
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    public boolean equals(Object o) {
        if (!(o instanceof SipURIImpl)) {
            return false;
        }

        SipURIImpl uri = (SipURIImpl) o;

        if (_port != uri._port) {
            return false;
        }

        if (_secure != uri._secure) {
            return false;
        }

        if (((_user == null) && (uri._user != null)) ||
                ((_user != null) && (uri._user == null))) {
            return false;
        }

        if ((_user != null) && !getUser().equals(uri.getUser())) {
            return false;
        }

        if (((_password == null) && (uri._password != null)) ||
                ((_password != null) && (uri._password == null))) {
            return false;
        }

        if ((_password != null) &&
                !getUserPassword().equals(uri.getUserPassword())) {
            return false;
        }

        // TODO why not _host?
        // if( (_host == null && uri._host != null) || (_host != null && uri._host
        // == null)) return false;
        if (!_host.equalsIgnoreCase(uri._host)) {
            return false;
        }

        if (((_headers == null) && (uri._headers != null)) ||
                ((_headers != null) && (uri._headers == null))) {
            return false;
        }

        if ((_headers != null) && (uri._headers != null)) {
            if (!_headers.equals(uri._headers)) {
                return false;
            }
        }

        if ((_parameters == null) && (uri._parameters != null)) {
            if (uri._parameters.isOnlySpecialParameters()) {
                return false;
            }
        }

        if ((_parameters != null) && (uri._parameters == null)) {
            if (_parameters.isOnlySpecialParameters()) {
                return false;
            }
        }

        if ((_parameters != null) && (uri._parameters != null)) {
            if (!_parameters.equals(uri._parameters)) {
                return false;
            }
        }

        return true;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#hashCode()
     */
    public int hashCode() {
        // Not all needs to be unique
        // A ^ (((B >> 16) & 0x0000FFFF) | ((B<<16) & 0xFFFF0000))
        // Or try just ^ if this is to slow
        int result = _host.hashCode() ^ _port;

        if (_user != null) {
            result = _user.hashCode() ^ result;
        }

        return result;
    }

    /*
     * Build up the sip uri. If the user or the parameter part contains any
     * unescaped characters then escape them.
     *
     * @see java.lang.Object#toString()
     */
    public String toString() {
        StringBuilder sb = new StringBuilder(getScheme());
        sb.append(':');

        if (_user != null) {
            sb.append(_user.toString());

            if (_password != null) {
                sb.append(':');
                sb.append(_password.toString());
            }

            sb.append('@');
        }

        sb.append(_host.toString());

        if (_port > 0) {
            sb.append(':');
            sb.append(_port);
        }

        // Duplicated in AddressImpl
        if (_parameters != null) {
            sb.append(_parameters.toString());
        }

        if (_headers != null) {
            sb.append('?');
            sb.append(_headers.toString());
        }

        return sb.toString();
    }

    void removeToTag() {
        if (_parameters != null) {
            _parameters.remove(AddressImpl.TAG_PARAM);
        }
    }
}
