/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt VS Tools.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace QtVsTools.Qml.Debug.V4
{
    interface IMessageEventSink
    {
        bool QueryRuntimeFrozen();

        void NotifyStateTransition(
            DebugClient client,
            DebugClientState oldState,
            DebugClientState newState);

        void NotifyRequestResponded(Request req);
        void NotifyEvent(Event evt);
        void NotifyMessage(Message msg);
    }

    class ProtocolDriver : Finalizable, IConnectionEventSink
    {
        IMessageEventSink sink;
        DebugClient client;
        int nextRequestSeq = 0;
        Dictionary<int, PendingRequest> pendingRequests = new Dictionary<int, PendingRequest>();
        Task eventHandlingThread;
        EventWaitHandle eventReceived = new EventWaitHandle(false, EventResetMode.AutoReset);
        ConcurrentQueue<Event> eventQueue = new ConcurrentQueue<Event>();

        public uint? ThreadId { get { return client.ThreadId; } }

        public static ProtocolDriver Create(IMessageEventSink sink)
        {
            var _this = new ProtocolDriver();
            return _this.Initialize(sink) ? _this : null;
        }

        private ProtocolDriver()
        { }

        private bool Initialize(IMessageEventSink sink)
        {
            this.sink = sink;
            eventHandlingThread = Task.Run(() => EventHandlingThread());
            client = DebugClient.Create(this);
            if (client == null)
                return false;

            return true;
        }

        protected override void DisposeManaged()
        {
            foreach (var req in ThreadSafe(() => pendingRequests.Values.ToList()))
                req.Dispose();

            ThreadSafe(() => pendingRequests.Clear());
            client.Dispose();
        }

        protected override void DisposeFinally()
        {
            eventReceived.Set();
            eventHandlingThread.Wait();
            eventReceived.Dispose();
        }

        private void EventHandlingThread()
        {
            while (!Disposing) {
                eventReceived.WaitOne();
                Event evt;
                while (!Disposing && eventQueue.TryDequeue(out evt))
                    sink.NotifyEvent(evt);
            }
        }

        public DebugClientState ConnectionState
        {
            get { return client.State; }
        }

        bool IConnectionEventSink.QueryRuntimeFrozen()
        {
            return sink.QueryRuntimeFrozen();
        }

        public EventWaitHandle Connect(string hostName, ushort hostPort)
        {
            return client.Connect(hostName, hostPort);
        }

        public EventWaitHandle StartLocalServer(string fileName)
        {
            return client.StartLocalServer(fileName);
        }

        public bool Disconnect()
        {
            return client.Disconnect();
        }

        public bool SendMessage(Message message)
        {
            if (client.State != DebugClientState.Connected)
                return false;

            var messageParams = message.Serialize();

            System.Diagnostics.Debug
                .Assert(messageParams != null, "Empty message data");

            return client.SendMessage(message.Type, messageParams);
        }

        public PendingRequest SendRequest(Request request)
        {
            ThreadSafe(() => request.SequenceNum = nextRequestSeq++);

            var pendingRequest = new PendingRequest(request);
            ThreadSafe(() => pendingRequests[request.SequenceNum] = pendingRequest);

            if (!SendMessage(request as Message)) {
                ThreadSafe(() => pendingRequests.Remove(request.SequenceNum));
                pendingRequest.Dispose();
                return new PendingRequest();
            }

            return pendingRequest;
        }

        void IConnectionEventSink.NotifyStateTransition(
            DebugClient client,
            DebugClientState oldState,
            DebugClientState newState)
        {
            sink.NotifyStateTransition(client, oldState, newState);
        }

        void IConnectionEventSink.NotifyMessageReceived(
            DebugClient client,
            string messageType,
            byte[] messageParams)
        {
            if (client != this.client || Disposing)
                return;

            var msg = Message.Deserialize(messageType, messageParams);
            if (msg == null)
                return;

            if (msg is Response) {
                var msgResponse = msg as Response;
                EnterCriticalSection();
                PendingRequest pendingRequest = null;
                if (pendingRequests.TryGetValue(msgResponse.RequestSeq, out pendingRequest)) {
                    pendingRequests.Remove(msgResponse.RequestSeq);
                    LeaveCriticalSection();
                    pendingRequest.SetResponse(msgResponse);
                    sink.NotifyRequestResponded(pendingRequest.Request);
                    pendingRequest.Dispose();
                } else {
                    LeaveCriticalSection();
                    sink.NotifyMessage(msg);
                }

            } else if (msg is Event) {
                var msgEvent = msg as Event;
                eventQueue.Enqueue(msgEvent);
                eventReceived.Set();

            } else {
                sink.NotifyMessage(msg);
            }
        }

        #region //////////////////// PendingRequest ///////////////////////////////////////////////

        public class PendingRequest : Finalizable
        {
            public Request Request { get; private set; }
            EventWaitHandle responded;

            public PendingRequest()
            {
                Request = null;
                responded = null;
            }

            public PendingRequest(Request req)
            {
                responded = new EventWaitHandle(false, EventResetMode.ManualReset);
                Request = req;
            }

            public bool RequestSent
            {
                get { return Request != null; }
            }

            public bool ResponseReceived
            {
                get { return Request != null && Request.Response != null; }
            }

            public Response WaitForResponse()
            {
                if (Request == null)
                    return null;
                if (responded != null)
                    responded.WaitOne();
                return Request.Response;
            }

            protected override void DisposeManaged()
            {
                if (Request == null)
                    return;

                if (responded != null)
                    responded.Dispose();
            }

            public void SetResponse(Response res)
            {
                if (Request == null || Disposing)
                    return;

                Request.Response = res;
                if (responded != null)
                    responded.Set();
            }
        }

        #endregion //////////////////// PendingRequest ////////////////////////////////////////////

    }
}