/*
* Copyright (C) 2012 Doubango Telecom <http://www.doubango.org>
* License: BSD
* This file is part of Open Source sipML5 solution <http://www.sipml5.org>
*/

/**
@fileoverview This is SIPML5 "library" contains a  lot of classes and functions.

@name sipML5 API
@author      Doubango Telecom <http://www.doubango.org>
@version     1.3.203
*/

/** 
@namespace
@description Root namesapce.
*/
SIPml = {};

/** @private */SIPml.b_initialized = false;
/** @private */SIPml.b_initializing = false;
/** @private */SIPml.s_navigator_friendly_name = 'unknown';
/** @private */SIPml.b_navigator_outdated = false;
/** @private */SIPml.s_navigator_version = 'unknown';
/** @private */SIPml.s_system_friendly_name = 'unknown';
/** @private */SIPml.b_webrtc4all_plugin_outdated = false;
/** @private */SIPml.b_webrtc4all_supported = false;
/** @private */SIPml.s_webrtc4all_version = 'unknown';
/** @private */SIPml.b_have_media_stream = false;
/** @private */SIPml.b_webrtc_supported = false;


/**
Sets the debug level.
@since version 1.3.203
@param {String} level The level. Supported values: <i>info</i>, <i>warn</i>, <i>error</i> and <i>fatal</i>.
*/
SIPml.setDebugLevel = function(level) {
    tsk_utils_log_set_level(level === 'fatal' ? 1 : (level === 'error' ? 2 : (level === 'warn' ? 3 : 4)));
}

/**
Gets the version name of the installed <a href="http://code.google.com/p/webrtc4all/">webrtc4all plugin</a>.
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {String} Version name (e.g. '1.12.756')
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.getWebRtc4AllVersion = function() {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.s_webrtc4all_version;
};

/**
Gets the web browser version (e.g. <i>'1.5.beta'</i>).
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {String} The the web browser version.
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.getNavigatorVersion = function() {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.s_navigator_version; 
};

/**
Gets the web browser friendly name (e.g. <i>'chrome'</i>, <i>'firefox'</i>, <i>'safari'</i>, <i>'opera'</i>, <i>'ie'</i> or <i>'netscape'</i>).
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {String} The web browser friendly name.
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.getNavigatorFriendlyName = function() {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.s_navigator_friendly_name; 
};

/**
Gets the Operating System friendly name (e.g. <i>'windows'</i>, <i>'mac'</i>, <i>'lunix'</i>, <i>'solaris'</i>, <i>'sunos'</i> or <i>'powerpc'</i>).
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {String} The Operating System friendly name.
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.getSystemFriendlyName = function() {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.s_system_friendly_name; 
};

/**
Checks whether the web browser supports WebRTC but is outdated.
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {Boolean} <i>true</i> if outdated; otherwise <i>false</i>
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.isNavigatorOutdated= function () {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.b_navigator_outdated; 
}

/**
Checks whether the <a href="http://code.google.com/p/webrtc4all/">webrtc4all plugin</a> is outdated or not.
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {Boolean} <i>true</i> if outdated; otherwise <i>false</i>
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.isWebRtc4AllPluginOutdated = function(){
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.b_webrtc4all_plugin_outdated; 
}

/**
Checks whether the <a href="http://code.google.com/p/webrtc4all/">webrtc4all plugin</a> is installed or not.
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {Boolean} <i>true</i> if supported; otherwise <i>false</i>
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.isWebRtc4AllSupported = function(){
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.b_webrtc4all_supported; 
}

/**
Checks whether Screen share is supported on this browser.
You must <a href="#.init">initialize</a> the engine before calling this function.
@since version 1.3.203
@static
@returns {Boolean} <i>true</i> if supported; otherwise <i>false</i>
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.isScreenShareSupported = function () {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return (navigator.userAgent.match('Chrome') && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1]) >= 26);
}

/**
Checks whether WebRTC is supported or not.
You must <a href="#.init">initialize</a> the engine before calling this function.
@static
@returns {Boolean} <i>true</i> if supported; otherwise <i>false</i>
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.isWebRtcSupported = function () {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.b_webrtc_supported; 
}

/**
Checks whether WebSocket is supported or not.
@static
@returns {Boolean} <i>true</i> if supported; otherwise <i>false</i>
*/
SIPml.isWebSocketSupported = function () {
    return tsk_utils_have_websocket(); 
}

/**
Checks whether <a href="https://developer.mozilla.org/en-US/docs/WebRTC/navigator.getUserMedia">getUserMedia</a> is supported or not. The engined must be initialized before calling this function.
@static
@returns {Boolean} <i>true</i> if <a href="https://developer.mozilla.org/en-US/docs/WebRTC/navigator.getUserMedia">getUserMedia</a> is supported; otherwise <i>false</i>
*/
SIPml.haveMediaStream = function () {
    if(!SIPml.isInitialized()){
        throw new Error("ERR_NOT_INITIALIZED: Engine not initialized yet. Please call 'SIPml.init()' first");
    }
    return SIPml.b_have_media_stream;
}

/**
Checks whether the engine is ready to make/receive calls or not. <br />
The engine is ready when:
    <ul>
        <li>engine is <a href="#.init">initialized</a></li>
        <li>webrtc is supported</li>
        <li>we got a valid media stream (from <a href="https://developer.mozilla.org/en-US/docs/WebRTC/navigator.getUserMedia">getUserMedia</a>)</li>
    </ul>
@static
@returns {Boolean} <i>true</i> if the engine is ready; otherwise <i>false</i>
@throws {ERR_NOT_INITIALIZED} <font color="red">ERR_NOT_INITIALIZED</font> if the engine is not <a href="#.init">initialized</a>.
*/
SIPml.isReady = function () {
    return (SIPml.isInitialized() && SIPml.isWebRtcSupported() && SIPml.haveMediaStream()); 
}

/**
Checks whether the engine is initialized or not. To initialize the stack you must call <a href="#.init">init()</a> function.
@static
@returns {Boolean} <i>true</i> if the engine is initialized; otherwise <i>false</i>
*/
SIPml.isInitialized = function () { return SIPml.b_initialized; }


/**
Initialize the engine. <b>You must call this function before any other.</b>.
@param {CallbackFunction} [readyCallback] Optional callback function to call when the stack finish initializing and become ready.
@param {CallbackFunction} [errorCallback] Optional callback function to call when initialization fails.

@example
SIPml.init(function(e){ console.info('engine is ready'); }, function(e){ console.info('Error: ' + e.message); });
@static
*/
SIPml.init = function (successCallback, errorCallback) {
    if (!SIPml.b_initialized && !SIPml.b_initializing) {
        SIPml.b_initializing = true;
        tsk_utils_init_webrtc();

        tsk_utils_log_info('User-Agent=' + (navigator.userAgent || "unknown"));

        SIPml.b_have_media_stream = tsk_utils_have_stream();
        SIPml.b_webrtc_supported = tsk_utils_have_webrtc();
        SIPml.b_webrtc4all_supported = tsk_utils_have_webrtc4all();
        SIPml.s_webrtc4all_version = tsk_utils_webrtc4all_get_version();
        SIPml.s_navigator_friendly_name = tsk_utils_get_navigator_friendly_name();
        SIPml.s_system_friendly_name = tsk_utils_get_system_friendly_name();

        // prints whether WebSocket is supported
        tsk_utils_log_info("WebSocket supported = " + (SIPml.isWebSocketSupported() ? "yes" : "no"));

        // check webrtc4all version
        if (tsk_utils_have_webrtc4all()) {
            tsk_utils_log_info("WebRTC type = " + WebRtc4all_GetType() + " version = " + tsk_utils_webrtc4all_get_version());
            if (SIPml.s_webrtc4all_version != '1.35.981') {
                SIPml.b_webrtc4all_plugin_outdated = true;
            }
        }

        // prints navigator friendly name
        tsk_utils_log_info("Navigator friendly name = " + SIPml.s_navigator_friendly_name);

        // gets navigator version
        if(SIPml.s_navigator_friendly_name == 'ie'){
            var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
            if (re.exec(navigator.userAgent) != null) {
                SIPml.s_navigator_version = RegExp.$1;
            }
        }        

        // prints OS friendly name
        tsk_utils_log_info("OS friendly name = " + SIPml.s_system_friendly_name);
        // prints support for WebRTC (native or plugin)
        tsk_utils_log_info("Have WebRTC = " + (tsk_utils_have_webrtc() ? "yes" : "false"));
        // prints support for getUserMedia
        tsk_utils_log_info("Have GUM = " + (tsk_utils_have_stream() ? "yes" : "false"));

        // checks for WebRTC support
        if (!tsk_utils_have_webrtc()) {
            // is it chrome?
            if (SIPml.s_navigator_friendly_name == "chrome") {
                SIPml.b_navigator_outdated = true;
                return;
            }

            // for now the plugins (WebRTC4all only works on Windows)
            if (SIPml.s_system_friendly_name == 'win' || SIPml.s_system_friendly_name == 'windows') {
                // Internet explorer
                if (SIPml.s_navigator_friendly_name == 'ie') {
                    // Check for IE version 
                    var rv = -1;
                    var ua = navigator.userAgent;
                    var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
                    if (re.exec(ua) != null) {
                        rv = parseFloat(RegExp.$1);
                    }
                    if (rv < 9.0) {
                        SIPml.b_navigator_outdated = true;
                        return;
                    }

                    // break page loading ('window.location' won't stop JS execution)
                    if (!tsk_utils_have_webrtc4all()) {
                        return;
                    }
                }
            }
        }

        if(SIPml.b_webrtc_supported && SIPml.b_have_media_stream){
            SIPml.b_initialized = true;
            SIPml.b_initializing = false;
            tsk_utils_log_info("Engine initialized");
            if(successCallback){
                successCallback({});
            }
        }
        else{
            if(errorCallback){
                var s_description = !SIPml.b_webrtc_supported ? "WebRTC not supported" : (!SIPml.b_have_media_stream ? "getUserMedia not supported" : "Internal error");
                errorCallback({description: s_description});
            }
        }
    }
}

// ================================== SIPml.EventTarget ==========================================

/**
@constructor
Defines an event target. You sould never create an event target object.
*/
SIPml.EventTarget = function () {
    this.ao_listeners = [];
}

/**
Adds an event listener to the target object. <br /><br />
<table border="1">
    <tr>
        <td><b>Target classes<b></td>
        <td><b>Supported event types<b></td>
        <td><b>Raised event object<b></td>
        <td><b>Remarques<b></td>
    </tr>
    <tr>
        <td><a href="SIPml.Stack.html" name="SIPml.EventTarget.Stack">SIPml.Stack</a></td>
        <td>
            <b>*</b><br/> starting<br/> started<br/> stopping<br/> stopped<br/> failed_to_start<br/> failed_to_stop<br/> i_new_call<br /> i_new_message<br />
            m_permission_requested<br/> m_permission_accepted<br/> m_permission_refused
        </td>
        <td><a href="SIPml.Stack.Event.html">SIPml.Stack.Event</a></td>
        <td>'*' is used to listen for all events</td>
    </tr>
    <tr>
        <td>
            <a href="SIPml.Session.html" name="SIPml.EventTarget.Session">SIPml.Session</a>
            <ul>
                <li><a href="SIPml.Session.Call.html">SIPml.Session.Call</a></li>
                <li><a href="SIPml.Session.Message.html">SIPml.Session.Message</a></li>
                <li><a href="SIPml.Session.Message.html">SIPml.Session.Registration</a></li>
                <li><a href="SIPml.Session.Message.html">SIPml.Session.Subscribe</a></li>
                <li><a href="SIPml.Session.Message.html">SIPml.Session.Publish</a></li>
            <ul>
        </td>
        <td><b>*</b><br/> connecting<br/> connected<br/> terminating<br/> terminated<br/>
                i_ao_request<br />
                media_added<br/> media_removed<br/>
                i_request<br/> o_request<br/> cancelled_request<br/> sent_request<br/>
                transport_error<br/> global_error<br/> message_error<br/> webrtc_error
        </td>
        <td><a href="SIPml.Session.Event.html">SIPml.Session.Event</a></td>
        <td>'*' is used to listen for all events<br /></td>
    </tr>
    <tr>
        <td><a href="SIPml.Session.Call.html" name="SIPml.EventTarget.Session.Call">SIPml.Session.Call</a></td>
        <td>
            m_early_media<br/> m_local_hold_ok<br/> m_local_hold_nok<br/> m_local_resume_ok<br/> m_local_resume_nok<br/> m_remote_hold<br/> m_remote_resume<br/>
            m_stream_video_local_added<br /> m_stream_video_local_removed<br/> m_stream_video_remote_added<br/> m_stream_video_remote_removed <br />
            m_stream_audio_local_added<br /> m_stream_audio_local_removed<br/> m_stream_audio_remote_added<br/> m_stream_audio_remote_removed <br />
            i_ect_new_call<br/> o_ect_trying<br/> o_ect_accepted<br/> o_ect_completed<br/> i_ect_completed<br/> o_ect_failed<br/> i_ect_failed<br/> o_ect_notify<br/> i_ect_notify<br/> i_ect_requested
        </td>
        <td><a href="SIPml.Session.Event.html">SIPml.Session.Event</a></td>
        <td>borrows all events supported by <a href="SIPml.Session.html">SIPml.Session</a></td>
    </tr>
    <tr>
        <td><a href="SIPml.Session.Subscribe.html" name="SIPml.EventTarget.Session.Subscribe">SIPml.Session.Subscribe</a></td>
        <td>
            i_notify
        </td>
        <td><a href="SIPml.Session.Event.html">SIPml.Session.Event</a></td>
        <td>borrows all events supported by <a href="SIPml.Session.html">SIPml.Session</a></td>
    </tr>
</table>
@param {String|Array} type The event type/identifier. Must not be null or empty. Use <b>'*'</b> to listen for all events.
@param {function} listener The object that receives a notification when an event of the specified type occurs. This must be an object implementing the <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventListener">EventListener</a> interface, or simply a JavaScript function.
@example
// listen for a single event
this.addEventListener('started', function(e){
    console.info("'started' event fired");
});
// or listen for two or more events
this.addEventListener(['started', 'stopped'], function(e){
    console.info("'"+e.type+"' event fired");
});
// or listen for all events
this.addEventListener('*', function(e){
    console.info("'"+e.type+"' event fired");
});
@see <a href="#removeEventListener">removeEventListener</a>
@throws {ERR_INVALID_PARAMETER_VALUE|ERR_INVALID_PARAMETER_TYPE} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_INVALID_PARAMETER_TYPE</font>
*/
SIPml.EventTarget.prototype.addEventListener = function (o_type, o_listener) {
    if (!o_listener) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: 'listener' must not be null");
    }
    if (!o_type) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: 'type' must not be null");
    }
    if(!(o_type instanceof String || typeof o_type == "string" || o_type instanceof Array)){
        throw new Error("ERR_INVALID_PARAMETER_TYPE: 'type' must be a string or array");
    }
    
    if(o_type instanceof Array){
        var This = this;
        o_type.forEach(function (s_type) {
            if (!tsk_string_is_null_or_empty(s_type) && tsk_string_is_string(s_type)) {
                This.ao_listeners[s_type] = o_listener;
            }
        });
    }
    else{
        this.ao_listeners[o_type] = o_listener;
    }
}

/**
Removes an event listener from the target object.
@param {String} type The event type/identifier to stop listening for.
@see <a href="#addEventListener">addEventListener</a>
@exemple
this.removeEventListener('started');
*/
SIPml.EventTarget.prototype.removeEventListener = function (s_type) {
    if (tsk_string_is_string(s_type) && !tsk_string_is_null_or_empty(s_type)) {
        this.ao_listeners[s_type] = undefined;
    }
}

/**
@ignore
@private
@param {Object} event
*/
SIPml.EventTarget.prototype.dispatchEvent = function (o_event) {
    var o_listener = (this.ao_listeners[o_event.s_type] || this.ao_listeners['*']);
    if (o_listener) {
        o_listener.call(this, o_event.o_value);
    }
}



// ================================== SIPml.Event ==========================================


/** 
SIP  event object. You should never create an instance of this class by yourself.
@constructor
@param {String} type The event type or identifier. Please check <a href="SIPml.EventTarget.html#SIPml.EventTarget.Session">this link</a> for more information about all supported session event types.
@param {tsip_event} [event] Private wrapped session object.
@property {String} type The event <a href="SIPml.EventTarget.html#SIPml.EventTarget.Session">type or identifier</a> (e.g. <i>'connected'</i>).
@property {String} description User-friendly description in english (e.g. <i>'Session is now connected'</i>).
*/
SIPml.Event = function (s_type, o_event) {
    this.type = s_type;
    this.description = o_event ? o_event.s_phrase : s_type;
    this.o_event = o_event;
}

/**
Gets the SIP response code.
@returns {Integer} The SIP response code (e.g. 404).
*/
SIPml.Event.prototype.getSipResponseCode = function () {
    var o_message = this.o_event ? this.o_event.get_message() : null;
    if (o_message && o_message.is_response()) {
        return o_message.get_response_code();
    }
    return -1;
}

/**
Gets the SIP content associated to this event. This function could be called to get the content of the incoming SIP message ('i_new_message' event).
@returns {Object} SIP content.
@see <a href="#getContentType">getContentType</a>, <a href="#getContentString">getContentString</a>
*/
SIPml.Event.prototype.getContent = function () {
    var o_message = this.o_event ? this.o_event.get_message() : null;
    if (o_message) {
        return o_message.get_content();
    }
    return null;
}

/**
Gets the SIP content associated to this event. This function could be called to get the content of the incoming SIP message ('i_new_message' event).
@returns {String} SIP content.
@see <a href="#getContentType">getContentType</a>, <a href="#getContent">getContent</a>
*/
SIPml.Event.prototype.getContentString = function () {
    var o_message = this.o_event ? this.o_event.get_message() : null;
    if (o_message) {
        return o_message.get_content_as_string();
    }
    return null;
}

/**
Gets the SIP content-type associated to this event. This function could be called to get the content-type of the incoming SIP message ('i_new_message' event).
@returns {Object} SIP content-type.
@see <a href="#getContent">getContent</a>
*/
SIPml.Event.prototype.getContentType = function () {
    var o_message = this.o_event ? this.o_event.get_message() : null;
    if (o_message) {
        return o_message.get_content_type();
    }
    return null;
}



// ================================== SIPml.Stack ==========================================


/**
Anonymous SIP Stack configuration object.
@namespace SIPml.Stack.Configuration
@name SIPml.Stack.Configuration
@property {String} realm The domain name. Required for stack <a href="SIPml.Stack.html#constructor">constructor</a> but optional when used with <a href="SIPml.Stack.html#setConfiguration">setConfiguration</a>. <br />
Example: <i>example.org</i>
@property {String} impi The authentication name. Required for stack <a href="SIPml.Stack.html#constructor">constructor</a> but optional when used with <a href="SIPml.Stack.html#setConfiguration">setConfiguration</a>.<br />
Example: <i>+33600000000</i> or <i>bob</i>.
@property {string} impu The full SIP uri address. Required for stack <a href="SIPml.Stack.html#constructor">constructor</a> but optional when used with <a href="SIPml.Stack.html#setConfiguration">setConfiguration</a>.<br />
Example: <i>sip:+33600000000@example.com</i> or <i>tel:+33600000000</i> or <i>sip:bob@example.com</i>
@property {String} [password] The password to use for SIP authentication.<br />
Example: <i>mysecret</i>
@property {String} [display_name] The display name to use in SIP requests. This is the String displayed by the called party for incoming calls. <br />
Example: <i>I Am Legend</i>
@property {String} [websocket_proxy_url] The websocket proxy url to connect to (SIP server or gateway address). If unset the stack will use sipml5.org as host and a random port. You should not set this value unless you know what you're doing.<br />
Example: <i>ws://sipml5.org:5060</i>
@property {String} [outbound_proxy_url] The outbound Proxy URL is used to set the destination IP address and Port to use for all outgoing requests regardless the <i>domain name</i> (a.k.a <i>realm</i>). <br />
This is a good option for developers using a SIP domain name without valid DNS A/NAPTR/SRV records. You should not set this value unless you know what you're doing. <br />
Example: <i>udp://192.168.0.12:5060</i>
@property {Array} [ice_servers] The list of the STUN/TURN servers to use. The format must be as explained at <a target=_blank href="http://www.w3.org/TR/webrtc/#rtciceserver-type">http://www.w3.org/TR/webrtc/#rtciceserver-type</a>. <br />
To disable TURN/STUN to speedup ICE candidates gathering you can use an empty array. e.g. <i>[]</i>. <br />
Example: <i>[{ url: 'stun:stun.l.google.com:19302'}, { url:'turn:user@numb.viagenie.ca', credential:'myPassword'}]</i>
@property {Object} [bandwidth] Defines the maximum audio and video bandwidth to use. This will change the outhoing SDP to include a "b:AS=" attribute. Use <i>0</i> to let the browser negotiates the right value using RTCP-REMB and congestion control. Same property could be used at session level to override this value.<br />
<i>Available since version 1.3.203</i>. <br />
Example: <i>{ audio:64, video:512 }</i>
@property {Object} [video_size] Defines the maximum and minimum video size to be used. All values are optional. The browser will try to find the best video size between <i>max</i> and <i>min</i> based on the camera capabilities. Same property could be used at session level to override this value.<br />
<i>Available since version 1.3.203</i>. <br />
Example: <i>{ minWidth:640, minHeight:480, maxWidth:1920, maxHeight:1080 }</i>
@property {Boolean} [enable_rtcweb_breaker] Whether to enable the <a href="http://webrtc2sip.org/#aRTCWebBreaker" target=_blank>RTCWeb Breaker</a> module to allow calling SIP-legacy networks. <br />
Example: <i>true</i>
@property {Boolean} [enable_click2call] Whether to enable the <a href="http://click2dial.org" target=_blank>Click2Call / Click2Dial</a> service.
<i>Available since version 1.2.181</i>. <br />
Example: <i>true</i>
@property {Boolean} [enable_early_ims] Whether to enable 3GGP Early IMS as per <a href="http://www.arib.or.jp/english/html/overview/doc/STD-T63v9_60/5_Appendix/Rel6/33/33978-660.pdf" target=_blank>TR 33.978</a>. Should be 'true' unless you're using a real IMS network. <br />
<i>Available since version 1.3.203</i>. <br />
Example: <i>true</i>
@property {Boolean} [enable_media_stream_cache] Whether to reuse the same media stream for all calls. If your website is <b>not using https</b> then, the browser will request access to the camera (or microphone) every time you try to make a call. Caching the media stream will avoid getting these notifications for each call. <br />
<i>Available since version 1.3.203</i>. <br />
Example: <i>true</i>

@property {Object} [events_listener] Object to subscribe to some events.
Example:
<ul>
    <li><i>{ events: '*', listener: function(e){} }</i> </li>
    <li><i>{ events: 'started', listener: function(e){} }</i></li>
    <li><i>{ events: ['started', 'stopped'], listener: function(e){} }</i></li>
</ul>
You can also use <a href="#addEventListener">addEventListener</a> to add listeners to the stack.
@property {Array} [sip_headers] Stack-level SIP headers to add to all outgoing requests. Each header is an object with a <i>name</i> and <i>value</i> fields. <br />
Example: <i>sip_headers: [{name: 'User-Agent', value: 'IM-client/OMA1.0 sipML5-v1.0.89.0'}, {name: 'Organization', value: 'Doubango Telecom'}]</i>

@example
var configuration = {
        realm: 'example.org',
        impi: 'bob',
        impu: 'sip:bob@example.org',
        password: 'mysecret', // optional
        display_name: 'I Am Legend', // optional
        websocket_proxy_url: 'ws://192.168.0.10:5060', // optional
        outbound_proxy_url: 'udp://192.168.0.12:5060', // optional
        ice_servers: [{ url: 'stun:stun.l.google.com:19302'}, { url:'turn:user@numb.viagenie.ca', credential:'myPassword'}], // optional
        enable_rtcweb_breaker: true, // optional
        enable_click2call: false, // optional
        enable_early_ims: true, // optional
        events_listener: { events: '*', listener: listenerFunc }, // optional
        sip_headers: [ //optional
            {name: 'User-Agent', value: 'IM-client/OMA1.0 sipML5-v1.0.89.0'}, 
            {name: 'Organization', value: 'Doubango Telecom'}
        ]
    };
*/


/**
This is the root object used by any other object to make/receive calls, messages or manage presence.
You have to create an instance of this class before anything else.
@extends SIPml.EventTarget
@constructor
@class
@param {SIPml.Stack.Configuration} configuration Configuration object. Could be updated later using <a href="#setConfiguration">setConfiguration</a>.
@throws {ERR_INVALID_PARAMETER_VALUE|ERR_INVALID_PARAMETER_TYPE} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_INVALID_PARAMETER_TYPE</font>
@example
var listenerFunc = function(e){
    console.info('stack event = ' + e.type);
    // Please check <a href="SIPml.EventTarget.html#SIPml.EventTarget.Stack">this link</a> for more information on all supported events.
}

var o_stack = new SIPml.Stack({
        realm: 'example.org',
        impi: 'bob',
        impu: 'sip:bob@example.org',
        password: 'mysecret', // optional
        display_name: 'I Am Legend', // optional
        websocket_proxy_url: 'ws://192.168.0.10:5060', // optional
        outbound_proxy_url: 'udp://192.168.0.12:5060', // optional
        ice_servers: [{ url: 'stun:stun.l.google.com:19302'}, { url:'turn:user@numb.viagenie.ca', credential:'myPassword'}], // optional
        bandwidth: { audio:64, video:512 }, // optional
        video_size: { minWidth:640, minHeight:480, maxWidth:1920, maxHeight:1080 }, // optional
        enable_rtcweb_breaker: true, // optional
        enable_click2call: false, // optional
        events_listener: { events: '*', listener: listenerFunc }, //optional
        sip_headers: [ //optional
            {name: 'User-Agent', value: 'IM-client/OMA1.0 sipML5-v1.0.89.0'}, 
            {name: 'Organization', value: 'Doubango Telecom'}
        ]
    }
);

@see <a href="#setConfiguration">setConfiguration</a>
*/
SIPml.Stack = function (o_conf) {
    SIPml.init();
    SIPml.EventTarget.call(this);
    /*
    members:
    - o_stack {tsip_stack}
    */

    if (!o_conf) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: null configuration value");
    }
    if (tsk_string_is_null_or_empty(o_conf.realm)) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: '" + o_conf.realm + "' is not valid as realm value");
    }
    if (tsk_string_is_null_or_empty(o_conf.impi)) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: '" + o_conf.impi + "' is not valid as impi value");
    }
    if (tsk_string_is_null_or_empty(o_conf.impu)) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: '" + o_conf.impu + "' is not valid as impu value");
    }
    // check IMPU validity
    var o_impu = tsip_uri.prototype.Parse(o_conf.impu);
    if (!o_impu || !o_impu.s_user_name || !o_impu.s_host) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: '" + o_conf.impu + "' is not valid as SIP Uri");
    }

    var i_port;
    var s_proxy;    

    if (!SIPml.isWebSocketSupported()) {
        // port and host will be updated using the result from DNS SRV(NAPTR(realm))
        i_port = 5060;
        s_proxy = o_conf.realm;
    }
    else {
        // there are at least 5 servers running on the cloud.
        // we will connect to one of them and let the balancer to choose the right one (less connected sockets)
        // each port can accept up to 65K connections which means that the cloud can manage 325K active connections
        // the number of port will be increased or decreased based on the current trafic

        // webrtc2sip 2.2+ (Doubango): 
        //      WS: 10060, 11060, 12060, 13060, 14060
        //      WSS: 10062, 11062, 12062, 13062, 14062
        //

        i_port = (o_conf.enable_rtcweb_breaker ? 10062 : 10060) + (((new Date().getTime()) % 5) * 1000);
        s_proxy = "ns313841.ovh.net";
    }

    // create the stack
    this.o_stack = new tsip_stack(o_conf.realm, o_conf.impi, o_conf.impu, s_proxy, i_port);
    this.o_stack.oStack = this;
    // set configurations
    this.setConfiguration(o_conf);

    // listen for stack events
    this.o_stack.on_event_stack = function(e) {
        var s_type;
        switch (e.i_code) {
            case tsip_event_code_e.STACK_STARTING: s_type = 'starting'; break;
            case tsip_event_code_e.STACK_STARTED: s_type = 'started'; break;
            case tsip_event_code_e.STACK_STOPPING: s_type = 'stopping'; break;
            case tsip_event_code_e.STACK_STOPPED: s_type = 'stopped'; break;
            case tsip_event_code_e.STACK_FAILED_TO_START: s_type = 'failed_to_start'; break;
            case tsip_event_code_e.STACK_FAILED_TO_STOP: s_type = 'failed_to_stop'; break;
        }
        if(s_type){
             e.o_stack.oStack.dispatchEvent({ s_type: s_type, o_value: new SIPml.Stack.Event(s_type, e) });
        }
    }


     // listen for dialog events
     this.o_stack.on_event_dialog = function (e) {
         var s_type = null;
         var i_session_id = e.o_session.i_id;
         var oSession = e.o_session.o_stack.oStack.ao_sessions[i_session_id];
         if (!oSession) {
             tsk_utils_log_warn('Cannot find session with id = ' + i_session_id);
             return;
         }

         switch (e.i_code) {
             case tsip_event_code_e.DIALOG_TRANSPORT_ERROR: s_type = 'transport_error'; break;
             case tsip_event_code_e.DIALOG_GLOBAL_ERROR: s_type = 'global_error'; break;
             case tsip_event_code_e.DIALOG_MESSAGE_ERROR: s_type = 'message_error'; break;
             case tsip_event_code_e.DIALOG_WEBRTC_ERROR: s_type = 'webrtc_error'; break;
             case tsip_event_code_e.DIALOG_REQUEST_INCOMING: s_type = 'i_request'; break;
             case tsip_event_code_e.DIALOG_REQUEST_OUTGOING: s_type = 'o_request'; break;
             case tsip_event_code_e.DIALOG_REQUEST_CANCELLED: s_type = 'cancelled_request'; break;
             case tsip_event_code_e.DIALOG_REQUEST_SENT: s_type = 'sent_request'; break;
             case tsip_event_code_e.DIALOG_MEDIA_ADDED: s_type = 'media_added'; break;
             case tsip_event_code_e.DIALOG_MEDIA_REMOVED: s_type = 'media_removed'; break;
             case tsip_event_code_e.DIALOG_CONNECTING: s_type = 'connecting'; break;
             case tsip_event_code_e.DIALOG_CONNECTED: s_type = 'connected'; break;
             case tsip_event_code_e.DIALOG_TERMINATING: s_type = 'terminating'; break;
             case tsip_event_code_e.DIALOG_TERMINATED: 
                {
                    s_type = 'terminated'; 
                    e.o_session.o_stack.oStack.ao_sessions[i_session_id] = undefined; 
                    break;
                }
             default: break;
         }

         if (s_type) {
             oSession.dispatchEvent({ s_type: s_type, o_value: new SIPml.Session.Event(oSession, s_type, e) });
         }
     }

     // listen for MESSAGE events
     this.o_stack.on_event_message = function (e) {
         var s_type = null;
         var i_session_id = e.o_session.i_id;
         var oSession = e.o_session.o_stack.oStack.ao_sessions[i_session_id];

         switch (e.e_message_type) {
             case tsip_event_message_type_e.I_MESSAGE: s_type = 'i_new_message'; break;
             case tsip_event_message_type_e.AO_MESSAGE: s_type = 'i_ao_request'; break;
         }

         if (s_type) {
             // 'i_new_call' is stack-level event
             if (s_type == 'i_new_message') {
                var oNewEvent = new SIPml.Stack.Event(s_type, e);
                oNewEvent.newSession = new SIPml.Session.Message(e.o_session);
                e.o_session.o_stack.oStack.ao_sessions[i_session_id] = oNewEvent.newSession; // save session
                e.o_session.o_stack.oStack.dispatchEvent({ s_type: s_type, o_value:  oNewEvent});
             }
             else {
                 if(oSession){
                    oSession.dispatchEvent({ s_type: s_type, o_value: new SIPml.Session.Event(oSession, s_type, e) });
                 }
                 else{
                    tsk_utils_log_warn('Cannot find session with id = ' + i_session_id + ' and event = ' + e.e_invite_type);
                 }
             }
         }
     };

      // listen for PUBLISH events
     this.o_stack.on_event_publish = function (e) {
         var s_type = null;
         var i_session_id = e.o_session.i_id;
         var oSession = e.o_session.o_stack.oStack.ao_sessions[i_session_id];
         if(!oSession){
            tsk_utils_log_warn('Cannot find session with id = ' + i_session_id + ' and event = ' + e.e_invite_type);
            return;
         }

         switch(e.e_publish_type){
            case tsip_event_publish_type_e.I_PUBLISH: break;
            case tsip_event_publish_type_e.I_UNPUBLISH: break;
            case tsip_event_publish_type_e.AO_PUBLISH: 
            case tsip_event_publish_type_e.AO_UNPUBLISH:
                {
                    s_type = 'i_ao_request'; 
                    break;
                }
         }
         if(s_type){
            oSession.dispatchEvent({ s_type: s_type, o_value: new SIPml.Session.Event(oSession, s_type, e) });
         }
     }

     // listen for SUBSCRIBE events
     this.o_stack.on_event_subscribe = function (e) {
         var s_type = null;
         var i_session_id = e.o_session.i_id;
         var oSession = e.o_session.o_stack.oStack.ao_sessions[i_session_id];
         if(!oSession){
            tsk_utils_log_warn('Cannot find session with id = ' + i_session_id + ' and event = ' + e.e_invite_type);
            return;
         }

         switch(e.e_subscribe_type){
            case tsip_event_subscribe_type_e.I_SUBSCRIBE: break;
            case tsip_event_subscribe_type_e.I_UNSUBSRIBE: break;
            case tsip_event_subscribe_type_e.AO_SUBSCRIBE: 
            case tsip_event_subscribe_type_e.AO_UNSUBSCRIBE:
            case tsip_event_subscribe_type_e.AO_NOTIFY:
                {
                    s_type = 'i_ao_request';
                    break;
                }
            case tsip_event_subscribe_type_e.I_NOTIFY:
                {
                    s_type = 'i_notify';
                    break;
                }
         }
         if(s_type){
            oSession.dispatchEvent({ s_type: s_type, o_value: new SIPml.Session.Event(oSession, s_type, e) });
         }
     }


     // listen for INVITE events
     this.o_stack.on_event_invite = function (e) {
         var s_type = null;
         var i_session_id = e.o_session.i_id;
         var oSession = e.o_session.o_stack.oStack.ao_sessions[i_session_id];
         if (!oSession) {
             switch (e.e_invite_type) {
                case tsip_event_invite_type_e.I_NEW_CALL:
                case tsip_event_invite_type_e.M_STREAM_LOCAL_REQUESTED:
                case tsip_event_invite_type_e.M_STREAM_LOCAL_ACCEPTED:
                case tsip_event_invite_type_e.M_STREAM_LOCAL_REFUSED:
                    break;

                case tsip_event_invite_type_e.M_STREAM_LOCAL_ADDED:
                case tsip_event_invite_type_e.M_STREAM_REMOTE_ADDED:
                case tsip_event_invite_type_e.M_STREAM_LOCAL_REMOVED:
                case tsip_event_invite_type_e.M_STREAM_REMOTE_REMOVED:
                case tsip_event_invite_type_e.I_AO_REQUEST:
                    tsk_utils_log_info('Not notifying to session with id = ' + i_session_id + ' for event = ' + e.e_invite_type);
                    return;

                 default:
                    tsk_utils_log_warn('Cannot find session with id = ' + i_session_id + ' and event = ' + e.e_invite_type);
                    return;
             }
         }

         

         var _setStream = function(o_view, o_stream, o_url, b_audio){
            if(o_stream){
                if(!b_audio && o_stream.videoTracks.length > 0){
                    if (window.HTMLVideoElement && o_view instanceof window.HTMLVideoElement){
                        if((o_view.src = o_url)){
                            o_view.play();
                        }
                    }
                    return true;
                }
                if(b_audio && o_stream.audioTracks.length > 0){
                    if (window.HTMLAudioElement && o_view instanceof window.HTMLAudioElement){
                        if((o_view.src = o_url)){
                            o_view.play();
                        }
                    }
                    return true;
                }
            }
         }

         var attachStream = function(bLocal){
            var o_stream = bLocal ? e.o_session.get_stream_local() : e.o_session.get_stream_remote();
            var o_url = bLocal ? e.o_session.get_url_local() : e.o_session.get_url_remote();
            if(_setStream((bLocal ? oSession.videoLocal : oSession.videoRemote), o_stream, o_url, false)){
                dispatchEvent(bLocal ? 'm_stream_video_local_added' : 'm_stream_video_remote_added');
            }
            if(_setStream((bLocal ? oSession.audioLocal : oSession.audioRemote), o_stream, o_url, true)){
                dispatchEvent(bLocal ? 'm_stream_audio_local_added' : 'm_stream_audio_remote_added');
            }
         }
         var deattachStream = function(bLocal){
            var o_stream = bLocal ? e.o_session.get_stream_local() : e.o_session.get_stream_remote();
            if(_setStream((bLocal ? oSession.videoLocal : oSession.videoRemote), o_stream, null, false)){
                dispatchEvent(bLocal ? 'm_stream_video_local_removed' : 'm_stream_video_remote_removed');
            }
            if(_setStream((bLocal ? oSession.audioLocal : oSession.audioRemote), o_stream, null, true)){
                dispatchEvent(bLocal ? 'm_stream_audio_local_removed' : 'm_stream_audio_remote_removed');
            }
         }

        var dispatchEvent = function (s_event_type) {
            if (s_event_type) {
                // 'i_new_call', 'm_permission_requested', 'm_permission_accepted' and 'm_permission_refused' are stack-level event
                switch (s_event_type) {
                    case 'i_new_call':
                    case 'm_permission_requested':
                    case 'm_permission_accepted':
                    case 'm_permission_refused':
                        {
                            var oNewEvent = new SIPml.Stack.Event(s_event_type, e);
                            if(s_event_type == 'i_new_call'){
                                oNewEvent.newSession = new SIPml.Session.Call(e.o_session);
                                e.o_session.o_stack.oStack.ao_sessions[i_session_id] = oNewEvent.newSession; // save session
                            }
                            e.o_session.o_stack.oStack.dispatchEvent({ s_type: s_event_type, o_value: oNewEvent });
                            break;
                        }
                    default:
                        {
                            oSession.dispatchEvent({ s_type: s_event_type, o_value: new SIPml.Session.Event(oSession, s_event_type, e) });
                            break;
                        }
                }
            }
        }

         switch (e.e_invite_type) {
             case tsip_event_invite_type_e.I_NEW_CALL: s_type = 'i_new_call'; break;
             case tsip_event_invite_type_e.I_ECT_NEW_CALL: s_type = 'i_ect_new_call'; break;
             case tsip_event_invite_type_e.I_AO_REQUEST: s_type = 'i_ao_request'; break;
             case tsip_event_invite_type_e.M_EARLY_MEDIA: s_type = 'm_early_media'; break;
             case tsip_event_invite_type_e.M_STREAM_LOCAL_REQUESTED: s_type = 'm_permission_requested'; break;
             case tsip_event_invite_type_e.M_STREAM_LOCAL_ACCEPTED: s_type = 'm_permission_accepted'; break;
             case tsip_event_invite_type_e.M_STREAM_LOCAL_REFUSED: s_type = 'm_permission_refused'; break;
             case tsip_event_invite_type_e.M_STREAM_LOCAL_ADDED:
                 {
                   return attachStream(true);
                 }
             case tsip_event_invite_type_e.M_STREAM_LOCAL_REMOVED:
                 { 
                    return deattachStream(true);
                 }
             case tsip_event_invite_type_e.M_STREAM_REMOTE_ADDED:
                 { 
                    return attachStream(false);
                 }
             case tsip_event_invite_type_e.M_STREAM_REMOTE_REMOVED:
                {
                    return deattachStream(false);
                }
             case tsip_event_invite_type_e.M_LOCAL_HOLD_OK: s_type = 'm_local_hold_ok'; break;
             case tsip_event_invite_type_e.M_LOCAL_HOLD_NOK: s_type = 'm_local_hold_nok'; break;
             case tsip_event_invite_type_e.M_LOCAL_RESUME_OK: s_type = 'm_local_resume_ok'; break;
             case tsip_event_invite_type_e.M_LOCAL_RESUME_NOK: s_type = 'm_local_resume_nok'; break;
             case tsip_event_invite_type_e.M_REMOTE_HOLD: s_type = 'm_remote_hold'; break;
             case tsip_event_invite_type_e.M_REMOTE_RESUME: s_type = 'm_remote_resume'; break;
             case tsip_event_invite_type_e.O_ECT_TRYING: s_type = 'o_ect_trying'; break;
             case tsip_event_invite_type_e.O_ECT_ACCEPTED: s_type = 'o_ect_accepted'; break;
             case tsip_event_invite_type_e.O_ECT_COMPLETED: s_type = 'o_ect_completed'; break;
             case tsip_event_invite_type_e.I_ECT_COMPLETED: s_type = 'i_ect_completed'; break;
             case tsip_event_invite_type_e.O_ECT_FAILED: s_type = 'o_ect_failed'; break;
             case tsip_event_invite_type_e.I_ECT_FAILED: s_type = 'i_ect_failed'; break;
             case tsip_event_invite_type_e.O_ECT_NOTIFY: s_type = 'o_ect_notify'; break;
             case tsip_event_invite_type_e.I_ECT_NOTIFY: s_type = 'i_ect_notify'; break;
             case tsip_event_invite_type_e.I_ECT_REQUESTED: s_type = 'i_ect_requested'; break;
             default: break;
         }

         // dispatch event
         dispatchEvent(s_type);
     }
}

SIPml.Stack.prototype = Object.create(SIPml.EventTarget.prototype);
SIPml.Stack.prototype.ao_sessions = [];

/**
Updates configuration values.
@param {SIPml.Stack.Configuration} configuration Configuration object value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
// add two new headers and change the <i>proxy_url</i>
o_stack.setConfiguration({
    proxy_url: 'ws://192.168.0.10:5060',
    sip_headers: [
            {name: 'User-Agent', value: 'IM-client/OMA1.0 sipML5-v1.0.89.0'}, 
            {name: 'Organization', value: 'Doubango Telecom'}
        ]
    }
);
*/
SIPml.Stack.prototype.setConfiguration = function (o_conf) {
    if (o_conf.realm && !tsk_string_is_string(o_conf.realm)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.realm + "' not a valid type for realm. String is expected");
    }
    if (o_conf.impi && !tsk_string_is_string(o_conf.impi)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.impi + "' not a valid type for impi. String is expected");
    }
    if (o_conf.impu && !tsk_string_is_string(o_conf.impu)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.impu + "' not a valid type for impu. String is expected");
    }
    if (o_conf.password && !tsk_string_is_string(o_conf.password)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.password + "' not a valid type for password. String is expected");
    }
    if (o_conf.display_name && !tsk_string_is_string(o_conf.display_name)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.display_name + "' not a valid type for display_name. String is expected");
    }
    if (o_conf.websocket_proxy_url && !tsk_string_is_string(o_conf.websocket_proxy_url)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.websocket_proxy_url + "' not a valid type for websocket_proxy_url. String is expected");
    }
    if (o_conf.outbound_proxy_url && !tsk_string_is_string(o_conf.outbound_proxy_url)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.outbound_proxy_url + "' not a valid type for outbound_proxy_url. String is expected");
    }
    if (o_conf.sip_headers && typeof o_conf.sip_headers != "Array" && !(o_conf.sip_headers instanceof Array)) {
        throw new Error("ERR_INVALID_PARAMETER_TYPE: '" + typeof o_conf.sip_headers + "' not a valid type for sip_headers. Array is expected");
    }

    // event-listener: must be first to be defined as other configs could raise events
    if(o_conf.events_listener){
        this.addEventListener(o_conf.events_listener.events, o_conf.events_listener.listener);
    }

    var b_rtcweb_breaker_enabled = !!o_conf.enable_rtcweb_breaker;
    var b_click2call_enabled = !!o_conf.enable_click2call;
    var b_early_ims = (o_conf.enable_early_ims == undefined) ? true : !!o_conf.enable_early_ims; // default value is true
    var b_enable_media_stream_cache = !!o_conf.enable_media_stream_cache;
    var o_bandwidth = o_conf.bandwidth ? o_conf.bandwidth : { audio:undefined, video:undefined };
    var o_video_size = o_conf.video_size ? o_conf.video_size : { minWidth:undefined, minHeight:undefined, maxWidth:undefined, maxHeight:undefined };
    var o_stack = this.o_stack;
    tsk_utils_log_info("s_websocket_server_url=" + (o_conf.websocket_proxy_url || "(null)"));
    tsk_utils_log_info("s_sip_outboundproxy_url=" + (o_conf.outbound_proxy_url || "(null)"));
    tsk_utils_log_info("b_rtcweb_breaker_enabled=" + (b_rtcweb_breaker_enabled ? "yes" : "no"));
    tsk_utils_log_info("b_click2call_enabled=" + (b_click2call_enabled ? "yes" : "no"));
    tsk_utils_log_info("b_early_ims=" + (b_early_ims ? "yes" : "no"));
    tsk_utils_log_info("b_enable_media_stream_cache=" + (b_enable_media_stream_cache ? "yes" : "no"));
    tsk_utils_log_info("o_bandwidth=" + JSON.stringify(o_bandwidth));
    tsk_utils_log_info("o_video_size=" + JSON.stringify(o_video_size));

    o_stack.set(tsip_stack.prototype.SetPassword(o_conf.password),
                     tsip_stack.prototype.SetDisplayName(o_conf.display_name),
                     tsip_stack.prototype.SetProxyOutBoundUrl(o_conf.outbound_proxy_url),
                     tsip_stack.prototype.SetRTCWebBreakerEnabled(b_rtcweb_breaker_enabled),
                     tsip_stack.prototype.SetClick2CallEnabled(b_click2call_enabled),
                     tsip_stack.prototype.SetSecureTransportEnabled(b_rtcweb_breaker_enabled), // always use secure transport when RTCWebBreaker
                     tsip_stack.prototype.SetEarlyIMSEnabled(b_early_ims), // should be 'true' unless you're using a real IMS network
                     tsip_stack.prototype.SetWebsocketServerUrl(o_conf.websocket_proxy_url),
                     tsip_stack.prototype.SetIceServers(o_conf.ice_servers),
                     tsip_stack.prototype.SetMediaStreamCacheEnabled(b_enable_media_stream_cache),
                     tsip_stack.prototype.SetBandwidth(o_bandwidth),
                     tsip_stack.prototype.SetVideoSize(o_video_size));

    // add sip headers
    if (o_conf.sip_headers) {
        o_conf.sip_headers.forEach(function (o_header) {
            if (o_header && !tsk_string_is_null_or_empty(o_header.name) && (!o_header.value || tsk_string_is_string(o_header.value))) {
                o_stack.set(
                    tsip_stack.prototype.SetHeader(o_header.name, o_header.value)
                );
            }
        });
    }

    return 0;
}


/**
Starts the SIP stack and connect the network transport to the WebSocket server without sending any SIP request. 
This function must be be called before any attempt to make or receive calls/messages. This function is asynchronous which means that the stack will not be immediately started after the call.
Please check <a href="SIPml.EventTarget.html#SIPml.EventTarget.Stack">this link</a> for more information on all supported events.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_INVALID_STATE} <font color="red">ERR_INVALID_STATE</font>
*/
SIPml.Stack.prototype.start = function () {
    return this.o_stack.start();
}

/**
Stops the SIP stack and disconnect the network transport from the WebSocket server. This function will also hangup all calls and unregister the user from the SIP server.
Please check <a href="SIPml.EventTarget.html#SIPml.EventTarget.Stack">this link</a> for more information on all supported events.
@param {Integer} [timeout] Optional parameter used to defined maximum time in milliseconds to take to stop the stack. 
Default value: 2000 millis
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_INVALID_STATE} <font color="red">ERR_INVALID_STATE</font>
*/
SIPml.Stack.prototype.stop = function (i_timeout) {
    return this.o_stack.stop(i_timeout);
}

/**
Create new SIP session.
@param {String} type Session type. Supported values: <b>'register'</b>, <b>'call-audio'</b>, <b>'call-audiovideo'</b>, <b>'call-video'</b>, <b>'call-screenshare'</b>, <b>'message'</b>, <b>'subscribe'</b> or <b>'publish'</b>.
@param {SIPml.Session.Configuration} [configuration] Anonymous object used to configure the newly created session.
@throws {ERR_INVALID_PARAMETER_VALUE} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> <br>
@returns {SIPml.Session} New session if successful; otherwise null.<br> The session type would be <a href="SIPml.Session.Registration.html">SIPml.Session.Registration</a>, <a href="SIPml.Session.Call.html">SIPml.Session.Call</a> or <a href="SIPml.Session.Message.html">SIPml.Session.Message</a> <br>

@example
var <a href="SIPml.Session.Registration.html">o_registration</a> = this.<a href="#newSession">newSession</a>('register', {
            expires: 200,
            sip_caps: [
                    {name: '+g.oma.sip-im'},
                    {name: '+audio'},
                    {name: 'language', value: '\"en,fr\"'}
            ],
            sip_headers: [
                    {name: 'What', value: 'Registration', session: false}, 
                    {name: 'Organization', value: 'Doubango Telecom', session: true}
            ]
        });
o_registration.<a href="SIPml.Session.Registration.html#register">register</a>();

// or
var <a href="SIPml.Session.Call.html">o_audiovideo</a> = this.<a href="#newSession">newSession</a>('call-audiovideo', {
            video_local: document.getElementById('video_local'), // &lt;video id="video_local" .../&gt;
            video_remote: document.getElementById('video_remote'), // &lt;video id="video_remote" .../&gt;
            audio_remote: document.getElementById('audio_remote'), // &lt;audio id="audio_remote" .../&gt;
            sip_caps: [
                    {name: '+g.oma.sip-im'},
                    {name: '+sip.ice'},
                    {name: 'language', value: '\"en,fr\"'}
            ],
            sip_headers: [
                    {name: 'What', value: 'Audio/Video call', session: false}, 
                    {name: 'Organization', value: 'Doubango Telecom', session: false}
            ]
        });
o_audiovideo.<a href="SIPml.Session.Call.html#call">call</a>('alice'); // call alice
*/
SIPml.Stack.prototype.newSession = function (s_type, o_conf) {
    var o_session;
    var cls;
    if (s_type == 'register') {
        o_session = new tsip_session_register(this.o_stack);
        cls = SIPml.Session.Registration;
    }
    else if (s_type == 'message') {
        o_session = new tsip_session_message(this.o_stack);
        cls = SIPml.Session.Message;
    }
    else if (s_type == 'publish') {
        o_session = new tsip_session_publish(this.o_stack);
        cls = SIPml.Session.Publish;
    }
    else if (s_type == 'subscribe') {
        o_session = new tsip_session_subscribe(this.o_stack);
        cls = SIPml.Session.Subscribe;
    }
    else if (s_type == 'call-audio' || s_type == 'call-audiovideo' || s_type == 'call-video' || s_type == 'call-screenshare') {
        o_session = new tsip_session_invite(this.o_stack);
        o_session.s_type = s_type;
        cls = SIPml.Session.Call;
    }
    else {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: '" + s_type + "' not valid as session type");
    }

    o_session.b_local = true; // locally created session
    var oSession = new cls(o_session, o_conf);
    this.ao_sessions[oSession.getId()] = oSession;
    return oSession;
}

// ================================== SIPml.Stack.Event ==========================================

/** 
SIP Stack event object. You should never create an instance of this object.
@constructor
@extends SIPml.Event
@param {String} type The event type/identifier.
@param {tsip_event} [event] The wrapped event object.
@property {String} type The event type or identifier (e.g. <i>'started'</i> or <i>'i_new_call'</i>). Please check <a href="SIPml.EventTarget.html#SIPml.EventTarget.Stack">this link</a> for more information on all supported events.
@property {String} description User-friendly description in english (e.g. <i>'Stack started'</i> or <i>'<b>I</b>ncoming <b>new</b> <b>call</b>'</i>).
@property {SIPml.Session} [newSession] Optional session object only defined when the event is about a new session creation (e.g. <i>'i_new_call'</i>).
The session type would be <a href="SIPml.Session.Call.html">SIPml.Session.Call</a> or <a href="SIPml.Session.Message.html">SIPml.Session.Message</a>
*/
SIPml.Stack.Event = function (s_type, o_event) {
    SIPml.Event.call(this, s_type, o_event);
}

SIPml.Stack.Event.prototype = Object.create(SIPml.Event.prototype);

// ================================== SIPml.Session ==========================================

/**
Anonymous SIP Session configuration object.
@namespace SIPml.Session.Configuration
@name SIPml.Session.Configuration
@property {Integer} [expires] Session timeout in seconds. 
@property {HTMLVideoElement} [video_local] <a href="https://developer.mozilla.org/en-US/docs/DOM/HTMLVideoElement">HTMLVideoElement<a> where to display the local video preview. This propety should be only used for <a href="SIPml.Session.Call.html">video sessions</a>.
@property {HTMLVideoElement} [video_remote] <a href="https://developer.mozilla.org/en-US/docs/DOM/HTMLVideoElement">HTMLVideoElement<a> where to display the remote video stream. This propety should be only used for <a href="SIPml.Session.Call.html">video sessions</a>.
@property {HTMLAudioElement} [audio_remote] <a href="https://developer.mozilla.org/en-US/docs/DOM/HTMLAudioElement">HTMLAudioElement<a> used to playback the remote audio stream. This propety should be only used for <a href="SIPml.Session.Call.html">audio sessions</a>.
@property {Array} [sip_caps] <i>{name,value}</i> pairs defining the SIP capabilities associated to this session. The capabilities are added to the Contact header. Please refer to <a href="http://tools.ietf.org/html/rfc3840">rfc3840</a> and <a href="http://tools.ietf.org/html/rfc3841">rfc3841</a> for more information.
@property {String} [from] Set the source uri string to be used in the <i>From</i> header (available since API version 1.2.170).
@property {Object} [bandwidth] Defines the maximum audio and video bandwidth to use. This will change the outhoing SDP to include a "b:AS=" attribute. Use <i>0</i> to let the browser negotiates the right value using RTCP-REMB and congestion control. A default value for all sessions could be defined at stack level.<br />
<i>Available since version 1.3.203</i>. <br />
Example: <i>{ audio:64, video:512 }</i>
@property {Object} [video_size] Defines the maximum and minimum video size to be used. All values are optional. The browser will try to find the best video size between <i>max</i> and <i>min</i> based on the camera capabilities. A default value for all sessions could be defined at stack level.<br />
<i>Available since version 1.3.203</i>. <br />
Example: <i>{ minWidth:640, minHeight:480, maxWidth:1920, maxHeight:1080 }</i>
@property {Array} [sip_headers] <i>{name,value,session}</i> trios defining the SIP headers associated to this session. <i>session</i> is a boolean defining whether the header have to be added to all outgoing request or not (initial only).
@example
var configuration = 
{
    expires: 200,
    audio_remote: document.getElementById('audio_remote'), // &lt;audio id="audio_remote" .../&gt;
    video_local: document.getElementById('video_local'),  // &lt;video id="video_local" .../&gt;
    video_remote: document.getElementById('video_remote'),  // &lt;video id="video_remote" .../&gt;
    sip_caps: [
                    {name: '+g.oma.sip-im'},
                    {name: '+sip.ice'},
                    {name: 'language', value: '\"en,fr\"'}
            ],
    sip_headers: [
                    {name: 'What', value: 'Audio/Video call', session: false}, 
                    {name: 'Organization', value: 'Doubango Telecom', session: false}
            ]
}
*/

/** 
Base (abstract) SIP session. You should never create an instance of this class by yourself. You have to use <a href="SIPml.Stack.html#newSession"> newSession()</a> to create a new instance.
This is a base class for <a href="SIPml.Session.Registration.html">SIPml.Session.Registration</a>, <a href="SIPml.Session.Call.html">SIPml.Session.Call</a> and <a href="SIPml.Session.Message.html">SIPml.Session.Message</a>.
@constructor
@extends SIPml.EventTarget
@param {tsip_session_t} session Private wrapped session object.
@param {SIPml.Session.Configuration} [configuration] Optional configuration object.
@throws {ERR_INVALID_PARAMETER_VALUE|ERR_INVALID_PARAMETER_TYPE} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_INVALID_PARAMETER_TYPE</font>
*/
SIPml.Session = function (o_session, o_conf) {
     SIPml.EventTarget.call(this);
    /*
        - o_configuration: []
        - o_session: tsip_session_xxx
    */
    if (!o_session) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: Invalid session value");
    }

    this.o_session = o_session;
    this.setConfiguration(o_conf);
}

SIPml.Session.prototype = Object.create(SIPml.EventTarget.prototype);
SIPml.Session.prototype.o_session = null;
SIPml.Session.prototype.o_session = null;
SIPml.Session.prototype.o_configuration = null;

/**
Gets the session unique identifier.
@returns {Integer} Read-only session unique identifier.
*/
SIPml.Session.prototype.getId = function(){
    return this.o_session.get_id();
}

/**
Updates or sets the session configuration.
@param {SIPml.Session.Configuration} configuration
*/
SIPml.Session.prototype.setConfiguration = function(o_conf){
    if(!o_conf){
        return;
    }

    var o_session = this.o_session;

    // event-listener: must be first to be defined as other configs could raise events
    if(o_conf.events_listener){
        this.addEventListener(o_conf.events_listener.events, o_conf.events_listener.listener);
    }

    if(this instanceof SIPml.Session.Call){
        // Bandwidth and video size
        o_session.set(
                    tsip_session.prototype.SetBandwidth(o_conf.bandwidth ? o_conf.bandwidth : { audio:undefined, video:undefined }),
                    tsip_session.prototype.SetVideoSize(o_conf.video_size ? o_conf.video_size : { minWidth:undefined, minHeight:undefined, maxWidth:undefined, maxHeight:undefined })
                );

        this.videoLocal = o_conf.video_local;
        this.videoRemote = o_conf.video_remote;
        this.audioRemote = o_conf.audio_remote;
        this.audioLocal = o_conf.audio_local;
        
         var _addStream = function(o_view, o_stream, o_url, b_audio){
            if(o_stream){
                if(!b_audio && o_stream.videoTracks.length > 0){
                    if (window.HTMLVideoElement && o_view instanceof window.HTMLVideoElement){
                        if(o_view.src == o_url) return false; // unchanged
                        else if(o_view.src = o_url) o_view.play();
                    }
                    return true;
                }
                if(b_audio && o_stream.audioTracks.length > 0){
                    if (window.HTMLAudioElement && o_view instanceof window.HTMLAudioElement){
                        if(o_view.src == o_url) return false; // unchanged
                        else if(o_view.src = o_url) o_view.play();
                    }
                    return true;
                }
            }
         }

        if(_addStream(this.videoLocal, o_session.get_stream_local(), o_session.get_url_local(), false)){
            this.dispatchEvent({ s_type: 'm_stream_video_local_added', o_value: new SIPml.Session.Event(this, 'm_stream_video_local_added') });
        }
        if(_addStream(this.videoRemote, o_session.get_stream_remote(), o_session.get_url_remote(), false)){
            this.dispatchEvent({ s_type: 'm_stream_video_remote_added', o_value: new SIPml.Session.Event(this, 'm_stream_video_remote_added') });
        }
        if(_addStream(this.audioLocal, o_session.get_stream_local(), o_session.get_url_local(), true)){
            this.dispatchEvent({ s_type: 'm_stream_audio_local_added', o_value: new SIPml.Session.Event(this, 'm_stream_audio_local_added') });
        }
        if(_addStream(this.audioRemote, o_session.get_stream_remote(), o_session.get_url_remote(), true)){
            this.dispatchEvent({ s_type: 'm_stream_audio_remote_added', o_value: new SIPml.Session.Event(this, 'm_stream_audio_remote_added') });
        }
    }

    
    // headers
    if (o_conf.sip_headers) {
        o_conf.sip_headers.forEach(function (o_header) {
            if (o_header && !tsk_string_is_null_or_empty(o_header.name) && (!o_header.value || tsk_string_is_string(o_header.value))) {
                o_session.set(
                    tsip_session.prototype.SetHeader(o_header.name, o_header.value)
                );
            }
        });
    }
    // caps
    if (o_conf.sip_caps) {
        o_conf.sip_caps.forEach(function (o_cap) {
            if (o_cap && !tsk_string_is_null_or_empty(o_cap.name) && (!o_cap.value || tsk_string_is_string(o_cap.value))) {
                o_session.set(
                    tsip_session.prototype.SetCaps(o_cap.name, o_cap.value)
                );
            }
        });
    }
    // expires
    if (o_conf.expires) {
        o_session.set(tsip_session.prototype.SetExpires(o_conf.expires));
    }
    // from
    if (o_conf.from) {
        o_session.set(tsip_session.prototype.SetFromStr(o_conf.from));
    }
}

/**
Gets the remote party SIP Uri (e.g. sip:john.doe@example.com). This Uri could be used a match an incoming call to a contact from your address book.
@returns {String} The remote party SIP Uri.
@see <a href="#getRemoteFriendlyName">getRemoteFriendlyName</a>
*/
SIPml.Session.prototype.getRemoteUri = function(){
    return (this.o_session.b_local ? this.o_session.o_uri_to : this.o_session.o_uri_from).toString();
}

/**
Gets the remote party friendly name (e.g. 'John Doe').
@returns {String} The remote party friendly name.
@see <a href="#getRemoteUri">getRemoteUri</a>
*/
SIPml.Session.prototype.getRemoteFriendlyName = function(){
    var o_uri = this.o_session.b_local ? this.o_session.o_uri_to : this.o_session.o_uri_from;
    return o_uri.s_display_name ? o_uri.s_display_name : o_uri.s_user_name;
}

/**
Rejects an incoming SIP MESSAGE (SMS-like) or audio/video call.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.prototype.reject = function (o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.reject();
}

/**
Accepts an incoming SIP MESSAGE (SMS-like) or audio/video call.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.prototype.accept = function (o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.accept();
}



// ================================== SIPml.Session.Event ==================================


/** 
SIP session event object. You should never create an instance of this class by yourself.
@constructor
@extends SIPml.Event
@param {SIPml.Session} [session] The session type would be <a href="SIPml.Session.Registration.html">SIPml.Session.Registration</a>, <a href="SIPml.Session.Call.html">SIPml.Session.Call</a> or <a href="SIPml.Session.Message.html">SIPml.Session.Message</a>.
@param {String} type The event type or identifier. Please check <a href="SIPml.EventTarget.html#SIPml.EventTarget.Session">this link</a> for more information about all supported session event types.
@param {tsip_event} [event] Private wrapped session object.
@property {SIPml.Session} session Session associated to this event. Would be <a href="SIPml.Session.Registration.html">SIPml.Session.Registration</a>, <a href="SIPml.Session.Call.html">SIPml.Session.Call</a> or <a href="SIPml.Session.Message.html">SIPml.Session.Message</a>.
*/
SIPml.Session.Event = function (o_session, s_type, o_event) {
    SIPml.Event.call(this, s_type, o_event);
    this.session = o_session;
}

SIPml.Session.Event.prototype = Object.create(SIPml.Event.prototype);


/**
Gets the name of destination for the current call transfer. 
@returns {String} The name of destination for the current call transfer (e.g. 'John Doe').
*/
SIPml.Session.Event.prototype.getTransferDestinationFriendlyName = function () {
    var o_message = this.o_event ? this.o_event.get_message() : null;
    if (o_message) {
        var o_hdr_Refer_To = o_message.get_header(tsip_header_type_e.Refer_To);
        if(o_hdr_Refer_To && o_hdr_Refer_To.o_uri){
            return (o_hdr_Refer_To.s_display_name ? o_hdr_Refer_To.s_display_name : o_hdr_Refer_To.o_uri.s_user_name);
        }
    }
    return null;
}

// ================================== SIPml.Registration ==================================

/**
SIP session registration class. You should never create an instance of this class by yourself.
Please use <a href="SIPml.Stack.html#newSession">stack.newSession()</a> function to create a new registration session.
@constructor
@extends SIPml.Session
@param {tsip_session} session Private wrapped session object
@param {SIPml.Session.Configuration} [configuration] Configuration value.
*/
SIPml.Session.Registration = function (o_session, o_configuration) {
    SIPml.Session.call(this, o_session, o_configuration);
}

SIPml.Session.Registration.prototype = Object.create(SIPml.Session.prototype);

/**
Sends SIP REGISTER request to login the user. Refreshing requests will be automatically done based on the expiration time.
@param {SIPml.Session.Configuration} [configuration] Optional configuration value.
@example
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('register', {
                            expires: 200,
                            events_listener: { events: '*', listener: onSipEventSession },
                            sip_caps: [
                                        { name: '+g.oma.sip-im', value: null },
                                        { name: '+audio', value: null },
                                        { name: 'language', value: '\"en,fr\"' }
                                ]
                        });
session.register();

@see <a href="#unregister">unregister</a>
@throws {ERR_INVALID_STATE} <font color="red">ERR_INVALID_STATE</font>
*/
SIPml.Session.Registration.prototype.register = function (o_conf) {
    // FIXME: apply o_configuration
    // FIXME: raise error if stack not started
    this.setConfiguration(o_conf);
    return this.o_session.register();
}

/**
Sends SIP REGISTER (expires=0) request to logout the user.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@see <a href="#register">register</a>
@throws {ERR_INVALID_STATE} <font color="red">ERR_INVALID_STATE</font>
*/
SIPml.Session.Registration.prototype.unregister = function (o_conf) {
    // FIXME: raise error if stack not started
    this.setConfiguration(o_conf);
    return this.o_session.unregister();
}

// ================================== SIPml.Call ==========================================

/** 
SIP audio/video/screenshare call session class. You should never create an instance of this class by yourself.
Please use <a href="SIPml.Stack.html#newSession">stack.newSession()</a> function to create a new audio/video/screenshare session.
@constructor
@extends SIPml.Session
@param {tsip_session} session Private wrapped session object
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@example
var listenerFunc = function(e){
    console.info('session event = ' + e.type);
}
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('call-audiovideo', {
            video_local: document.getElementById('video-local'), // &lt;video id="video-local" .../&gt;
            video_remote: document.getElementById('video-remote'), // &lt;video id="video-remote" .../&gt;
            audio_remote: document.getElementById('audio-remote'), // &lt;audio id="audio-remote" .../&gt;
            events_listener: { events: '*', listener: listenerFunc },
            sip_caps: [
                            { name: '+g.oma.sip-im' },
                            { name: '+sip.ice' },
                            { name: 'language', value: '\"en,fr\"' }
                        ]
        });
@throws {ERR_INVALID_PARAMETER_VALUE} <font color="red">ERR_INVALID_PARAMETER_VALUE</font>
*/
SIPml.Session.Call = function (o_session, o_conf) {
    SIPml.Session.call(this, o_session, o_conf);

    switch (o_session.s_type) {
        case 'call-audio': this.mediaType = tmedia_type_e.AUDIO; break;
        case 'call-audiovideo': this.mediaType = tmedia_type_e.AUDIO_VIDEO; break;
        case 'call-video': this.mediaType = tmedia_type_e.VIDEO; break;
        case 'call-screenshare': this.mediaType = tmedia_type_e.SCREEN_SHARE; break;
    }
}

SIPml.Session.Call.prototype = Object.create(SIPml.Session.prototype);
SIPml.Session.Call.prototype.videoLocal = null;
SIPml.Session.Call.prototype.videoRemote = null;

/**
Makes audio/video call.
@param {String} to Destination name, uri, phone number or identifier (e.g. 'sip:johndoe@example.com' or 'johndoe' or '+33600000000').
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
var listenerFunc = function(e){
    console.info('session event = ' + e.type);
}
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('call-audiovideo');
session.call('johndoe', {
            video_local: document.getElementById('video-local'), // &lt;video id="video-local" .../&gt;
            video_remote: document.getElementById('video-remote'), // &lt;video id="video-remote" .../&gt;
            audio_remote: document.getElementById('audio-remote'), // &lt;audio id="audio-remote" .../&gt;
            events_listener: { events: '*', listener: listenerFunc },
            sip_caps: [
                            { name: '+g.oma.sip-im' },
                            { name: '+sip.ice' },
                            { name: 'language', value: '\"en,fr\"' }
                        ]
        });

@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.call = function (s_to, o_conf) {
    if (tsk_string_is_null_or_empty(s_to)) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: 'to' must not be null");
    }
    if (!SIPml.haveMediaStream()) {
        throw new Error("ERR_NOT_READY: Media engine not ready yet");
    }
    // set destination
    this.o_session.set(tsip_session.prototype.SetToStr(s_to));
    // set conf
    this.setConfiguration(o_conf);
    // make call
    return this.o_session.call(this.mediaType);
}

/**
Terminates the audio/video call.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.hangup = function (o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.hangup();
}

/**
Holds the audio/video call.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.hold = function (o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.hold(this.mediaType);
}

/**
Resumes the audio/video call.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.resume = function (o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.resume(this.mediaType);
}

/**
Sends SIP INFO message.
@param {Object|String} [content] SIP INFO request content.
@param {String} [contentType] Content Type.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
session.info('Device orientation: portrait', 'doubango/device-orientation.xml');
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.info = function (o_content, s_content_type, o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.info(o_content, s_content_type);
}

/**
Sends a SIP DTMF digit.
@param {Char} [digit] The digit to send.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
session.dtmf('#');
@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.dtmf = function (c_digit, o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.dtmf(c_digit);
}

/**
Transfers the call to a new destination.
@param {String} to Transfer destination name, uri, phone number or identifier (e.g. 'sip:johndoe@example.com' or 'johndoe' or '+33600000000').
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
session.transfer('johndoe');
@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.transfer = function (s_to, o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.transfer(s_to);
}

/**
Accepts incoming transfer request.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.acceptTransfer = function (o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.transfer_accept();
}

/**
Rejects incoming transfer request.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_NOT_READY} <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Call.prototype.rejectTransfer = function (o_conf) {
    this.setConfiguration(o_conf);
    return this.o_session.transfer_reject();
}

// ================================== SIPml.Session.Message ==========================================

/** 
SIP MESSAGE (SMS) session class. You should never create an instance of this class by yourself.
Please use <a href="SIPml.Stack.html#newSession">stack.newSession()</a> function to create a messaging/IM session.
@constructor
@param {tsip_session} session Private session object.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@example
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('message');
*/
SIPml.Session.Message = function (o_session, o_conf) {
    SIPml.Session.call(this, o_session, o_conf);

}

SIPml.Session.Message.prototype = Object.create(SIPml.Session.prototype);

/**
Sends a SIP MESSAGE (SMS-like) request.
@param {String} to Destination name, uri, phone number or identifier (e.g. 'sip:johndoe@example.com' or 'johndoe' or '+33600000000').
@param {Object|String} [content] The message content.
@param {String} [contentType] The content type.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('message');
session.send('johndoe', 'P&ecirc;che &agrave; la moule', 'text/plain;charset=utf8',{
    sip_caps: [
                                    { name: '+g.oma.sip-im' },
                                    { name: '+sip.ice' },
                                    { name: 'language', value: '\"en,fr\"' }
                            ],
    sip_headers: [
                            { name: 'What', value: 'Sending SMS' },
                            { name: 'My-Organization', value: 'Doubango Telecom' }
                    ]
});
@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
*/
SIPml.Session.Message.prototype.send = function (s_to, o_content, s_content_type, o_conf){
    if (tsk_string_is_null_or_empty(s_to)) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: 'to' must not be null");
    }

    // apply configuration values
    this.setConfiguration(o_conf);
    // set destination
    this.o_session.set(tsip_session.prototype.SetToStr(s_to));
    // sends the message
    return this.o_session.send(o_content, s_content_type);
}



// ================================== SIPml.Session.Publish ==========================================


/** 
SIP PUBLISH (for presence status publication) session class.You should never create an instance of this class by yourself.
Please use <a href="SIPml.Stack.html#newSession">stack.newSession()</a> function to create a new presence publication session.
@constructor
@extends SIPml.Session
@since version 1.1.0
@param {tsip_session} session Private session object.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@example
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('publish', {
                            expires: 200,
                            events_listener: { events: '*', listener: function(e){} },
                            sip_headers: [
                                          { name: 'Event', value: 'presence' } // very important
                                ],
                            sip_caps: [
                                        { name: '+g.oma.sip-im', value: null },
                                        { name: '+audio', value: null },
                                        { name: 'language', value: '\"en,fr\"' }
                                ]
                        });
*/
SIPml.Session.Publish = function (o_session, o_conf) {
    SIPml.Session.call(this, o_session, o_conf);
    // set destination to ourself (https://groups.google.com/forum/#!topic/doubango/XKWTQ9TgjPU)
    o_session.set(tsip_session.prototype.SetToUri(o_session.get_stack().identity.o_uri_impu));
}

SIPml.Session.Publish.prototype = Object.create(SIPml.Session.prototype);

/**
Sends a SIP PUBLISH (for presence status publication) request.
@since version 1.1.0
@param {Object|String} [content] The request content.
@param {String} [contentType] The content type.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('publish');
var contentType = 'application/pidf+xml';
var content = '&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n' +
                '&lt;presence xmlns=\"urn:ietf:params:xml:ns:pidf\"\n' +
                    ' xmlns:im=\"urn:ietf:params:xml:ns:pidf:im\"' +
             	    ' entity=\"sip:bob@example.com\"&gt;\n' +
                    '&lt;tuple id=\"s8794\"&gt;\n' +
                    '&lt;status&gt;\n'+
                    '   &lt;basic>open&lt;/basic&gt;\n' +
                    '   &lt;im:im>away&lt;/im:im&gt;\n' +
                    '&lt;/status&gt;\n' +
                    '&lt;contact priority=\"0.8\"&gt;tel:+33600000000&lt;/contact&gt;\n' +
                    '&lt;note  xml:lang=\"fr\"&gt;Bonjour de Paris :)&lt;/note&gt;\n' +
                    '&lt;/tuple&gt;\n' +
   	            '&lt;/presence&gt;';

session.publish(content, contentType,{
    expires: 200,
    sip_caps: [
                                    { name: '+g.oma.sip-im' },
                                    { name: '+sip.ice' },
                                    { name: 'language', value: '\"en,fr\"' }
                            ],
    sip_headers: [
                            { name: 'Event', value: 'presence' },
                            { name: 'Organization', value: 'Doubango Telecom' }
                    ]
});
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
@see <a href="#unpublish">unpublish</a>
*/
SIPml.Session.Publish.prototype.publish = function (o_content, s_content_type, o_conf){
    // apply configuration values
    this.setConfiguration(o_conf);
    // sends the PUBLISH request
    return this.o_session.publish(o_content, s_content_type);
}

/**
Remove/unpublish presence data from the server.
@since version 1.1.0
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
@see <a href="#publish">publish</a>
*/
SIPml.Session.Publish.prototype.unpublish = function (o_conf){
    // apply configuration values
    this.setConfiguration(o_conf);
    // sends the PUBLISH request (expires = 0)
    return this.o_session.unpublish();
}





// ================================== SIPml.Session.Subscribe ==========================================


/** 
SIP SUBSCRIBE (for presence status subscription) session class.You should never create an instance of this class by yourself.
Please use <a href="SIPml.Stack.html#newSession">stack.newSession()</a> function to create a new presence subscription session.
@constructor
@extends SIPml.Session
@since version 1.1.0
@param {tsip_session} session Private session object.
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@example
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('subscribe', {
                expires: 200,
                events_listener: { events: '*', listener: function(e){} },
                sip_headers: [
                                { name: 'Event', value: 'presence' },
                                { name: 'Accept', value: 'application/pidf+xml' }
                    ],
                sip_caps: [
                            { name: '+g.oma.sip-im', value: null },
                            { name: '+audio', value: null },
                            { name: 'language', value: '\"en,fr\"' }
                    ]
            });
*/
SIPml.Session.Subscribe = function (o_session, o_conf) {
    SIPml.Session.call(this, o_session, o_conf);

}

SIPml.Session.Subscribe.prototype = Object.create(SIPml.Session.prototype);

/**
Sends a SIP SUBSCRIBE (for presence status subscription) request.
@since version 1.1.0
@param {String} to Destination name, uri, phone number or identifier (e.g. 'sip:johndoe@example.com' or 'johndoe' or '+33600000000').
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@example
var onEvent = function(e){
    if(e.type == 'i_notify'){
        // process incoming NOTIFY request
    }
}
var session = <a href="SIPml.Stack.html#newSession">stack.newSession</a>('subscribe', {
                expires: 200,
                events_listener: { events: '*', listener: onEvent },
                sip_headers: [
                                { name: 'Event', value: 'presence' },
                                { name: 'Accept', value: 'application/pidf+xml' }
                    ],
                sip_caps: [
                            { name: '+g.oma.sip-im', value: null },
                            { name: '+audio', value: null },
                            { name: 'language', value: '\"en,fr\"' }
                    ]
            });
session.subscribe('johndoe'); // watch for johndoe's presence status

@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
@see <a href="#unsubscribe">unsubscribe</a>
*/
SIPml.Session.Subscribe.prototype.subscribe = function (s_to, o_conf){
     if (tsk_string_is_null_or_empty(s_to)) {
        throw new Error("ERR_INVALID_PARAMETER_VALUE: 'to' must not be null");
    }
    // set destination
    this.o_session.set(tsip_session.prototype.SetToStr(s_to));
    // apply configuration values
    this.setConfiguration(o_conf);
    // sends the PUBLISH request
    return this.o_session.subscribe();
}

/**
Unsubscribe.
@since version 1.1.0
@param {SIPml.Session.Configuration} [configuration] Configuration value.
@returns {Integer} 0 if successful; otherwise nonzero
@throws {ERR_INVALID_PARAMETER_VALUE | ERR_NOT_READY} <font color="red">ERR_INVALID_PARAMETER_VALUE</font> | <font color="red">ERR_NOT_READY</font>
@see <a href="#subscribe">subscribe</a>
*/
SIPml.Session.Subscribe.prototype.unsubscribe = function (o_conf){
    // apply configuration values
    this.setConfiguration(o_conf);
    // sends the SUBSCRIBE request (expires = 0)
    return this.o_session.unsubscribe();
}
