/*
 * Copyright (c) Contributors, http://opensimulator.org/
 * See CONTRIBUTORS.TXT for a full list of copyright holders.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSimulator Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Reflection;

using OpenMetaverse.StructuredData;
using OpenMetaverse;

using log4net;

namespace osWebRtcVoice
{

    /// <summary>
    /// Wrappers around the Janus requests and responses.
    /// Since the messages are JSON and, because of the libraries we are using,
    /// the internal structure is an OSDMap, these routines hold all the logic
    /// to getting and setting the values in the JSON.
    /// </summary>
    public class JanusMessage
    {
        protected static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        protected static readonly string LogHeader = "[JANUS MESSAGE]";

        protected OSDMap m_message = new OSDMap();

        public JanusMessage()
        {
        }
        // A basic Janus message is:
        // {
        //    "janus": "operation",
        //    "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
        //    "session_id": 5645225333294848,   // optional, gives the session ID
        //    "handle_id": 6969906757968657    // optional, gives the plugin handle ID
        //    "sender": 6969906757968657        // optional, gives the ID of the sending subsystem
        //    "jsep": { "type": "offer", "sdp": "..." }  // optional, gives the SDP
        //  }
        public JanusMessage(string pType) : this()
        {
            m_message["janus"] = pType;
            m_message["transaction"] = UUID.Random().ToString();
        }
        public OSDMap RawBody => m_message;

        public string TransactionId { 
            get { return m_message.TryGetString("transaction", out string tid) ? tid : null;  }
            set { m_message["transaction"] = value; }
        }
        public string Sender { 
            get { return m_message.TryGetString("sender", out string tid) ? tid : null;  }
            set { m_message["sender"] = value; }
        }
        public OSDMap Jsep { 
            get { return m_message.TryGetOSDMap("jsep", out OSDMap jsep) ? jsep : null; }
            set { m_message["jsep"] = value; }
        }
        public void SetJsep(string pOffer, string pSdp)
        {
            m_message["jsep"] = new OSDMap()
            {
                { "type", pOffer },
                { "sdp", pSdp }
            };
        }

        public void AddAPIToken(string pToken)
        {
            m_message["apisecret"] = pToken;
        }
        // Note that the session_id is a long number in the JSON so we convert the string.
        public string sessionId { 
            get { return m_message.TryGetValue("session_id", out OSD tmposd) ? OSDToLong(tmposd).ToString() : string.Empty; }
            set { m_message["session_id"] = long.Parse(value); }
        }
        public bool hasSessionId { get { return m_message.ContainsKey("session_id"); } }
        public void AddSessionId(string pToken)
        {
            AddSessionId(long.Parse(pToken));
        }
        public void AddSessionId(long pToken)
        {
            m_message["session_id"] = pToken;
        }
        public bool hasHandleId { get { return m_message.ContainsKey("handle_id"); } }
        public void AddHandleId(string pToken)
        {
            m_message["handle_id"] = long.Parse(pToken);
        }
        public string sender
        {
            get { return m_message.TryGetString("sender", out string str) ? str : string.Empty; }
        }

        public virtual string ToJson()
        {
            return m_message.ToString();
        }
        public override string ToString()
        {
            return m_message.ToString();
        }
        // Utility function to convert an OSD object to an long. The OSD object can be an OSDInteger
        //    or an OSDArray of 4 or 8 integers. 
        // This exists because the JSON to OSD parser can return an OSDArray for a long number
        //    since there is not an OSDLong type.
        // The design of the OSD conversion functions kinda needs one to know how the number
        //    is stored in order to extract it. Like, if it's stored as a long value (8 bytes)
        //    and one fetches it with .AsInteger(), it will return the first 4 bytes as an integer
        //    and not the long value. So this function looks at the type of the OSD object and
        //    extracts the number appropriately.
        public static long OSDToLong(OSD pIn)
        {
            long ret = 0;
            switch (pIn.Type)
            {
                case OSDType.Integer:
                    ret = (long)(pIn as OSDInteger).AsInteger();
                    break;
                case OSDType.Binary:
                    byte[] value = (pIn as OSDBinary).value;
                    if (value.Length == 4)
                    {
                        ret = (long)(pIn as OSDBinary).AsInteger();
                    }
                    if (value.Length == 8)
                    {
                        ret = (pIn as OSDBinary).AsLong();
                    }
                    break;
                case OSDType.Array:
                    if ((pIn as OSDArray).Count == 4)
                    {
                        ret = (long)pIn.AsInteger();
                    }
                    if ((pIn as OSDArray).Count == 8)
                    {
                        ret = pIn.AsLong();
                    }
                    break;
            }
            return ret;
        }
    }
    // ==============================================================
    // A Janus request message is a basic Janus message with an API token
    public class JanusMessageReq : JanusMessage
    {
        public JanusMessageReq(string pType) : base(pType)
        {
        }
    }

    // ==============================================================
    // Janus message response is a basic Janus message with the response data
    // {
    //    "janus": "success",
    //    "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea", // ID of the requesting message
    //    "data": { ... }  // the response data
    //    "error": { "code": 123, "reason": "..." }  // if there was an error
    // }
    // The "janus" return code changes depending on the request. The above is for
    //    a successful response. Could be
    //      "event": for an event message (See JanusEventResp)
    //      "keepalive": for a keepalive event
    public class JanusMessageResp : JanusMessage
    {
        public JanusMessageResp() : base()
        {
        }

        public JanusMessageResp(string pType) : base(pType)
        {
        }

        public JanusMessageResp(OSDMap pMap) : base()
        {
            m_message = pMap;
        }

        public static JanusMessageResp FromJson(string pJson)
        {
            var newBody = OSDParser.DeserializeJson(pJson) as OSDMap;
            return new JanusMessageResp(newBody);
        }

        // Return the "data" portion of the response as an OSDMap or null if there is none
        public OSDMap dataSection { get { return m_message.TryGetOSDMap("data", out OSDMap osdm) ? osdm : null; } }

        // Check if a successful response code is in the response
        public virtual bool isSuccess { get { return CheckReturnCode("success"); } }
        public virtual bool isEvent { get { return CheckReturnCode("event"); } }
        public virtual bool isError { get { return CheckReturnCode("error"); } }
        public virtual bool CheckReturnCode(string pCode)
        {
            return ReturnCode == pCode;
        }
        public virtual string ReturnCode { get { 
            string ret = String.Empty;
            if (m_message is not null && m_message.ContainsKey("janus"))
            {
                ret = m_message["janus"].AsString();
            }
            return ret;
        } }
    }

    // ==============================================================
    // An error response is a Janus response with an error code and reason.
    // {
    //    "janus": "error",
    //    "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea", // ID of the requesting message
    //    "error": { "code": 123, "reason": "..." }  // if there was an error
    // }
    public class ErrorResp : JanusMessageResp
    {
        public ErrorResp() : base("error")
        {
        }

        public ErrorResp(string pType) : base(pType)
        {
        }

        public ErrorResp(JanusMessageResp pResp) : base(pResp.RawBody)
        {
        }

        public void SetError(int pCode, string pReason)
        {
            m_message["error"] = new OSDMap()
            {
                { "code", pCode },
                { "reason", pReason }
            };
        }

        // Dig through the response to get the error code or 0 if there is none
        public int errorCode { get {
            int ret = 0;
            if (m_message.ContainsKey("error"))
            {
                var err = m_message["error"];
                if (err is OSDMap)
                    ret = (int)OSDToLong((err as OSDMap)["code"]);
            }
            return ret;
        }}

        // Dig through the response to get the error reason or empty string if there is none
        public string errorReason { get {
            string ret = String.Empty;
            if (m_message.ContainsKey("error"))
            {
                var err = m_message["error"];
                if (err is OSDMap)
                    ret = (err as OSDMap)["reason"];
            }
            // return ((m_message["error"] as OSDMap)?["reason"]) ?? String.Empty;
            return ret;
        }}
    }
    // ==============================================================
    // Create session request and response
    public class CreateSessionReq : JanusMessageReq
    {
        public CreateSessionReq() : base("create")
        {
        }
    }
    public class CreateSessionResp : JanusMessageResp
    {
        public CreateSessionResp(JanusMessageResp pResp) : base(pResp.RawBody)
        { }
        public string returnedId { get {
            // The JSON response gives a long number (not a string)
            //    and the ODMap conversion interprets it as a long (OSDLong).
            // If one just does a "ToString()" on the OSD object, you
            //    get an interpretation of the binary value.
            return dataSection.ContainsKey("id") ? OSDToLong(dataSection["id"]).ToString() : String.Empty;
        }}  
    }
    // ==============================================================
    public class DestroySessionReq : JanusMessageReq
    {
        public DestroySessionReq() : base("destroy")
        {
            // Doesn't include the session ID because it is the URI
        }
    }   
    // ==============================================================
    public class TrickleReq : JanusMessageReq
    {
        // An empty trickle request is used to signal the end of the trickle
        public TrickleReq(JanusViewerSession pVSession) : base("trickle")
        {
            m_message["candidate"] = new OSDMap()
            {
                { "completed", true },
            };

        }
        public TrickleReq(JanusViewerSession pVSession, OSD pCandidates) : base("trickle")
        {
            m_message["viewer_session"] = pVSession.ViewerSessionID;
            if (pCandidates is OSDArray)
                m_message["candidates"] = pCandidates;
            else
                m_message["candidate"] = pCandidates;
        }
    }   
    // ==============================================================
    public class AttachPluginReq : JanusMessageReq
    {
        public AttachPluginReq(string pPlugin) : base("attach")
        {
            m_message["plugin"] = pPlugin;
        }
    }
    public class AttachPluginResp : JanusMessageResp
    {
        public AttachPluginResp(JanusMessageResp pResp) : base(pResp.RawBody)
        { }
        public string pluginId { get {
            return dataSection.ContainsKey("id") ? OSDToLong(dataSection["id"]).ToString() : String.Empty;
        }}
    }
    // ==============================================================
    public class DetachPluginReq : JanusMessageReq
    {
        public DetachPluginReq() : base("detach")
        {
            // Doesn't include the session ID or plugin ID because it is the URI
        }
    }
    // ==============================================================
    public class HangupReq : JanusMessageReq
    {
        public HangupReq() : base("hangup")
        {
            // Doesn't include the session ID or plugin ID because it is the URI
        }
    }   
    // ==============================================================
    // Plugin messages are defined here as wrappers around OSDMap.
    // The ToJson() method is overridden to put the OSDMap into the
    //    message body.
    // A plugin request is formatted like:
    //  {
    //    "janus": "message",
    //    "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
    //    "body": {
    //        "request": "create",
    //        "room": 10,
    //        "is_private": false,
    // }
    public class PluginMsgReq : JanusMessageReq
    {
        private OSDMap m_body = new OSDMap();
        
        // Note that the passed OSDMap is placed in the "body" section of the message
        public PluginMsgReq(OSDMap pBody) : base("message")
        {
            m_body = pBody;
        }
        public void AddStringToBody(string pKey, string pValue)
        {
            m_body[pKey] = pValue;
        }
        public void AddIntToBody(string pKey, int pValue)
        {
            m_body[pKey] = pValue;
        }
        public void AddBoolToBody(string pKey, bool pValue)
        {
            m_body[pKey] = pValue;
        }
        public void AddOSDToBody(string pKey, OSD pValue)
        {
            m_body[pKey] = pValue;
        }

        public override string ToJson()
        {
            m_message["body"] = m_body;
            return base.ToJson();
        }
    }
    // A plugin response is formatted like:
    //    {
    //    "janus": "success",
    //    "session_id": 5645225333294848,
    //    "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
    //    "sender": 6969906757968657,
    //    "plugindata": {
    //        "plugin": "janus.plugin.audiobridge",
    //        "data": {
    //            "audiobridge": "created",
    //            "room": 10,
    //            "permanent": false
    //        }
    //    }
    public class PluginMsgResp : JanusMessageResp
    {
        public OSDMap m_pluginData;
        public OSDMap m_data;
        public PluginMsgResp(JanusMessageResp pResp) : base(pResp.RawBody)
        {
            if (m_message is not null && m_message.ContainsKey("plugindata"))
            {
                // Move the plugin data up into the m_data var so it is easier to get to
                m_pluginData = m_message["plugindata"] as OSDMap;
                if (m_pluginData is not null && m_pluginData.ContainsKey("data"))
                {
                    m_data = m_pluginData["data"] as OSDMap;
                    // m_log.DebugFormat("{0} AudioBridgeResp. Found both plugindata and data: data={1}", LogHeader, m_data.ToString());
                }
            }
        }

        public OSDMap PluginRespData { get { return m_data; } }

        // Get an integer value for a key in the response data or zero if not there
        public int PluginRespDataInt(string pKey)
        {
            if (m_data is null)
                return 0;
            return m_data.ContainsKey(pKey) ? (int)OSDToLong(m_data[pKey]) : 0;
        }
        // Get a string value for a key in the response data or empty string if not there
        public string PluginRespDataString(string pKey)
        {
            if (m_data is null)
                return String.Empty;
            return m_data.ContainsKey(pKey) ? m_data[pKey].AsString() : String.Empty;
        }
    }
    // ==============================================================
    // Plugin messages for the audio bridge.
    // Audiobridge responses are formatted like:
    //    {
    //    "janus": "success",
    //    "session_id": 5645225333294848,
    //    "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
    //    "sender": 6969906757968657,
    //    "plugindata": {
    //        "plugin": "janus.plugin.audiobridge",
    //        "data": {
    //            "audiobridge": "created",
    //            "room": 10,
    //            "permanent": false
    //        }
    //    }
    public class AudioBridgeResp: PluginMsgResp
    {
        public AudioBridgeResp(JanusMessageResp pResp) : base(pResp)
        {
        }
        public override bool isSuccess { get { return PluginRespDataString("audiobridge") == "success"; } }
        // Return the return code if it is in the response or empty string if not
        public string AudioBridgeReturnCode { get { return PluginRespDataString("audiobridge"); } }
        // Return the error code if it is in the response or zero if not
        public int AudioBridgeErrorCode { get { return PluginRespDataInt("error_code"); } }
        // Return the room ID if it is in the response or zero if not
        public int RoomId { get { return PluginRespDataInt("room"); } }
    }
    // ==============================================================
    public class AudioBridgeCreateRoomReq : PluginMsgReq
    {
        public AudioBridgeCreateRoomReq(int pRoomId) : this(pRoomId, false, null)
        {
        }
        public AudioBridgeCreateRoomReq(int pRoomId, bool pSpatial, string pDesc) : base(new OSDMap() {
                                                { "room", pRoomId },
                                                { "request", "create" },
                                                { "is_private", false },
                                                { "permanent", false },
                                                { "sampling_rate", 48000 },
                                                { "spatial_audio", pSpatial },
                                                { "denoise", false },
                                                { "record", false }
                                            })  
        {
            if (!String.IsNullOrEmpty(pDesc))
                AddStringToBody("description", pDesc);
        }
    }
    // ==============================================================
    public class AudioBridgeDestroyRoomReq : PluginMsgReq
    {
        public AudioBridgeDestroyRoomReq(int pRoomId) : base(new OSDMap() {
                                                { "request", "destroy" },
                                                { "room", pRoomId },
                                                { "permanent", true }
                                            })  
        {
        }
    }
    // ==============================================================
    public class AudioBridgeJoinRoomReq : PluginMsgReq
    {
        public AudioBridgeJoinRoomReq(int pRoomId, string pAgentName) : base(new OSDMap() {
                                                { "request", "join" },
                                                { "room", pRoomId },
                                                { "display", pAgentName }
                                            })
        {
        }
    }
    // A successful response contains the participant ID and the SDP
    public class AudioBridgeJoinRoomResp : AudioBridgeResp
    {
        public AudioBridgeJoinRoomResp(JanusMessageResp pResp) : base(pResp)
        {
        }
        public int ParticipantId { get { return PluginRespDataInt("id"); } }
    }
    // ==============================================================
    public class AudioBridgeConfigRoomReq : PluginMsgReq
    {
        // TODO:
        public AudioBridgeConfigRoomReq(int pRoomId, string pSdp) : base(new OSDMap() {
                                                { "request", "configure" },
                                            })
        {
        }
    }
    public class AudioBridgeConfigRoomResp : AudioBridgeResp
    {
        // TODO:
        public AudioBridgeConfigRoomResp(JanusMessageResp pResp) : base(pResp)
        {
        }
    }
    // ==============================================================
    public class AudioBridgeLeaveRoomReq : PluginMsgReq
    {
        public AudioBridgeLeaveRoomReq(int pRoomId, int pAttendeeId) : base(new OSDMap() {
                                                { "request", "leave" },
                                                { "room", pRoomId },
                                                { "id", pAttendeeId }
                                            })  
        {
        }
    }
    // ==============================================================
    public class AudioBridgeListRoomsReq : PluginMsgReq
    {
        public AudioBridgeListRoomsReq() : base(new OSDMap() {
                                                { "request", "list" }
                                            })  
        {
        }
    }
    // ==============================================================
    public class AudioBridgeListParticipantsReq : PluginMsgReq
    {
        public AudioBridgeListParticipantsReq(int pRoom) : base(new OSDMap() {
                                                { "request", "listparticipants" },
                                                { "room", pRoom }
                                            })  
        {
        }
    }
    // ==============================================================
    public class AudioBridgeEvent : AudioBridgeResp
    {
        public AudioBridgeEvent(JanusMessageResp pResp) : base(pResp)
        {
        }
    }
    // ==============================================================
    // The LongPoll request returns events from  the plugins. These are formatted
    //    like the other responses but are not responses to requests.
    // They are formatted like:
    //    {
    //    "janus": "event",
    //    "sender": 6969906757968657,
    //    "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
    //    "plugindata": {
    //        "plugin": "janus.plugin.audiobridge",
    //        "data": {
    //            "audiobridge": "event",
    //            "room": 10,
    //            "participants": 1,
    //            "participants": [
    //                {
    //                    "id": 1234,
    //                    "display": "John Doe",
    //                    "audio_level": 0.0,
    //                    "video_room": false,
    //                    "video_muted": false,
    //                    "audio_muted": false,
    //                    "feed": 1234
    //                }
    //            ]
    //        }
    //    }
    public class EventResp : JanusMessageResp
    {
        public EventResp() : base()
        {
        }

        public EventResp(string pType) : base(pType)
        {
        }

        public EventResp(JanusMessageResp pResp) : base(pResp.RawBody)
        {
        }
    }
    // ==============================================================
}
