//===-- Communication.cpp ---------------------------------------*- C++ -*-===//
//
//                     The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//

// C Includes
// C++ Includes
// Other libraries and framework includes
// Project includes
#include "lldb/lldb-private-log.h"
#include "lldb/Core/Communication.h"
#include "lldb/Core/Connection.h"
#include "lldb/Core/Log.h"
#include "lldb/Core/Timer.h"
#include "lldb/Core/Event.h"
#include "lldb/Host/Host.h"
#include <string.h>

using namespace lldb;
using namespace lldb_private;

ConstString &
Communication::GetStaticBroadcasterClass ()
{
    static ConstString class_name ("lldb.communication");
    return class_name;
}

//----------------------------------------------------------------------
// Constructor
//----------------------------------------------------------------------
Communication::Communication(const char *name) :
    Broadcaster (NULL, name),
    m_connection_sp (),
    m_read_thread (LLDB_INVALID_HOST_THREAD),
    m_read_thread_enabled (false),
    m_bytes(),
    m_bytes_mutex (Mutex::eMutexTypeRecursive),
    m_write_mutex (Mutex::eMutexTypeNormal),
    m_callback (NULL),
    m_callback_baton (NULL),
    m_close_on_eof (true)

{
    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_OBJECT | LIBLLDB_LOG_COMMUNICATION,
                                 "%p Communication::Communication (name = %s)",
                                 this, name);

    SetEventName (eBroadcastBitDisconnected, "disconnected");
    SetEventName (eBroadcastBitReadThreadGotBytes, "got bytes");
    SetEventName (eBroadcastBitReadThreadDidExit, "read thread did exit");
    SetEventName (eBroadcastBitReadThreadShouldExit, "read thread should exit");
    SetEventName (eBroadcastBitPacketAvailable, "packet available");
    
    CheckInWithManager();
}

//----------------------------------------------------------------------
// Destructor
//----------------------------------------------------------------------
Communication::~Communication()
{
    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_OBJECT | LIBLLDB_LOG_COMMUNICATION,
                                 "%p Communication::~Communication (name = %s)",
                                 this, m_broadcaster_name.AsCString(""));
    Clear();
}

void
Communication::Clear()
{
    SetReadThreadBytesReceivedCallback (NULL, NULL);
    Disconnect (NULL);
    StopReadThread (NULL);
}

ConnectionStatus
Communication::Connect (const char *url, Error *error_ptr)
{
    Clear();

    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_COMMUNICATION, "%p Communication::Connect (url = %s)", this, url);

    lldb::ConnectionSP connection_sp (m_connection_sp);
    if (connection_sp.get())
        return connection_sp->Connect (url, error_ptr);
    if (error_ptr)
        error_ptr->SetErrorString("Invalid connection.");
    return eConnectionStatusNoConnection;
}

ConnectionStatus
Communication::Disconnect (Error *error_ptr)
{
    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_COMMUNICATION, "%p Communication::Disconnect ()", this);

    lldb::ConnectionSP connection_sp (m_connection_sp);
    if (connection_sp.get())
    {
        ConnectionStatus status = connection_sp->Disconnect (error_ptr);
        // We currently don't protect connection_sp with any mutex for 
        // multi-threaded environments. So lets not nuke our connection class 
        // without putting some multi-threaded protections in. We also probably
        // don't want to pay for the overhead it might cause if every time we
        // access the connection we have to take a lock.
        //
        // This unique pointer will cleanup after itself when this object goes away,
        // so there is no need to currently have it destroy itself immediately
        // upon disconnnect.
        //connection_sp.reset();
        return status;
    }
    return eConnectionStatusNoConnection;
}

bool
Communication::IsConnected () const
{
    lldb::ConnectionSP connection_sp (m_connection_sp);
    if (connection_sp.get())
        return connection_sp->IsConnected ();
    return false;
}

bool
Communication::HasConnection () const
{
    return m_connection_sp.get() != NULL;
}

size_t
Communication::Read (void *dst, size_t dst_len, uint32_t timeout_usec, ConnectionStatus &status, Error *error_ptr)
{
    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_COMMUNICATION,
                                         "%p Communication::Read (dst = %p, dst_len = %" PRIu64 ", timeout = %u usec) connection = %p",
                                         this, 
                                         dst, 
                                         (uint64_t)dst_len,
                                         timeout_usec, 
                                         m_connection_sp.get());

    if (m_read_thread_enabled)
    {
        // We have a dedicated read thread that is getting data for us
        size_t cached_bytes = GetCachedBytes (dst, dst_len);
        if (cached_bytes > 0 || timeout_usec == 0)
        {
            status = eConnectionStatusSuccess;
            return cached_bytes;
        }

        if (m_connection_sp.get() == NULL)
        {
            if (error_ptr)
                error_ptr->SetErrorString("Invalid connection.");
            status = eConnectionStatusNoConnection;
            return 0;
        }
        // Set the timeout appropriately
        TimeValue timeout_time;
        if (timeout_usec != UINT32_MAX)
        {
            timeout_time = TimeValue::Now();
            timeout_time.OffsetWithMicroSeconds (timeout_usec);
        }

        Listener listener ("Communication::Read");
        listener.StartListeningForEvents (this, eBroadcastBitReadThreadGotBytes | eBroadcastBitReadThreadDidExit);
        EventSP event_sp;
        while (listener.WaitForEvent (timeout_time.IsValid() ? &timeout_time : NULL, event_sp))
        {
            const uint32_t event_type = event_sp->GetType();
            if (event_type & eBroadcastBitReadThreadGotBytes)
            {
                return GetCachedBytes (dst, dst_len);
            }

            if (event_type & eBroadcastBitReadThreadDidExit)
            {
                Disconnect (NULL);
                break;
            }
        }
        return 0;
    }

    // We aren't using a read thread, just read the data synchronously in this
    // thread.
    lldb::ConnectionSP connection_sp (m_connection_sp);
    if (connection_sp.get())
    {
        return connection_sp->Read (dst, dst_len, timeout_usec, status, error_ptr);
    }

    if (error_ptr)
        error_ptr->SetErrorString("Invalid connection.");
    status = eConnectionStatusNoConnection;
    return 0;
}


size_t
Communication::Write (const void *src, size_t src_len, ConnectionStatus &status, Error *error_ptr)
{
    lldb::ConnectionSP connection_sp (m_connection_sp);

    Mutex::Locker locker(m_write_mutex);
    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_COMMUNICATION,
                                         "%p Communication::Write (src = %p, src_len = %" PRIu64 ") connection = %p",
                                         this, 
                                         src, 
                                         (uint64_t)src_len,
                                         connection_sp.get());

    if (connection_sp.get())
        return connection_sp->Write (src, src_len, status, error_ptr);

    if (error_ptr)
        error_ptr->SetErrorString("Invalid connection.");
    status = eConnectionStatusNoConnection;
    return 0;
}


bool
Communication::StartReadThread (Error *error_ptr)
{
    if (error_ptr)
        error_ptr->Clear();

    if (IS_VALID_LLDB_HOST_THREAD(m_read_thread))
        return true;

    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_COMMUNICATION,
                                 "%p Communication::StartReadThread ()", this);


    char thread_name[1024];
    snprintf(thread_name, sizeof(thread_name), "<lldb.comm.%s>", m_broadcaster_name.AsCString());

    m_read_thread_enabled = true;
    m_read_thread = Host::ThreadCreate (thread_name, Communication::ReadThread, this, error_ptr);
    if (!IS_VALID_LLDB_HOST_THREAD(m_read_thread))
        m_read_thread_enabled = false;
    return m_read_thread_enabled;
}

bool
Communication::StopReadThread (Error *error_ptr)
{
    if (!IS_VALID_LLDB_HOST_THREAD(m_read_thread))
        return true;

    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_COMMUNICATION,
                                 "%p Communication::StopReadThread ()", this);

    m_read_thread_enabled = false;

    BroadcastEvent (eBroadcastBitReadThreadShouldExit, NULL);

    //Host::ThreadCancel (m_read_thread, error_ptr);

    bool status = Host::ThreadJoin (m_read_thread, NULL, error_ptr);
    m_read_thread = LLDB_INVALID_HOST_THREAD;
    return status;
}


size_t
Communication::GetCachedBytes (void *dst, size_t dst_len)
{
    Mutex::Locker locker(m_bytes_mutex);
    if (m_bytes.size() > 0)
    {
        // If DST is NULL and we have a thread, then return the number
        // of bytes that are available so the caller can call again
        if (dst == NULL)
            return m_bytes.size();

        const size_t len = std::min<size_t>(dst_len, m_bytes.size());

        ::memcpy (dst, m_bytes.c_str(), len);
        m_bytes.erase(m_bytes.begin(), m_bytes.begin() + len);

        return len;
    }
    return 0;
}

void
Communication::AppendBytesToCache (const uint8_t * bytes, size_t len, bool broadcast, ConnectionStatus status)
{
    lldb_private::LogIfAnyCategoriesSet (LIBLLDB_LOG_COMMUNICATION,
                                 "%p Communication::AppendBytesToCache (src = %p, src_len = %" PRIu64 ", broadcast = %i)",
                                 this, bytes, (uint64_t)len, broadcast);
    if ((bytes == NULL || len == 0)
        && (status != lldb::eConnectionStatusEndOfFile))
        return;
    if (m_callback)
    {
        // If the user registered a callback, then call it and do not broadcast
        m_callback (m_callback_baton, bytes, len);
    }
    else if (bytes != NULL && len > 0)
    {
        Mutex::Locker locker(m_bytes_mutex);
        m_bytes.append ((const char *)bytes, len);
        if (broadcast)
            BroadcastEventIfUnique (eBroadcastBitReadThreadGotBytes);
    }
}

size_t
Communication::ReadFromConnection (void *dst, 
                                   size_t dst_len, 
                                   uint32_t timeout_usec,
                                   ConnectionStatus &status, 
                                   Error *error_ptr)
{
    lldb::ConnectionSP connection_sp (m_connection_sp);
    if (connection_sp.get())
        return connection_sp->Read (dst, dst_len, timeout_usec, status, error_ptr);
    return 0;
}

bool
Communication::ReadThreadIsRunning ()
{
    return m_read_thread_enabled;
}

void *
Communication::ReadThread (void *p)
{
    Communication *comm = (Communication *)p;

    Log *log(lldb_private::GetLogIfAllCategoriesSet (LIBLLDB_LOG_COMMUNICATION));

    if (log)
        log->Printf ("%p Communication::ReadThread () thread starting...", p);

    uint8_t buf[1024];

    Error error;
    ConnectionStatus status = eConnectionStatusSuccess;
    bool done = false;
    while (!done && comm->m_read_thread_enabled)
    {
        size_t bytes_read = comm->ReadFromConnection (buf, sizeof(buf), 5 * TimeValue::MicroSecPerSec, status, &error);
        if (bytes_read > 0)
            comm->AppendBytesToCache (buf, bytes_read, true, status);
        else if ((bytes_read == 0)
                && status == eConnectionStatusEndOfFile)
        {
            if (comm->GetCloseOnEOF ())
                comm->Disconnect ();
            comm->AppendBytesToCache (buf, bytes_read, true, status);
        }

        switch (status)
        {
        case eConnectionStatusSuccess:
            break;

        case eConnectionStatusEndOfFile:
            if (comm->GetCloseOnEOF())
                 done = true;
             break;
        case eConnectionStatusNoConnection:     // No connection
        case eConnectionStatusLostConnection:   // Lost connection while connected to a valid connection
            done = true;
            // Fall through...
        case eConnectionStatusError:            // Check GetError() for details
        case eConnectionStatusTimedOut:         // Request timed out
            if (log)
                error.LogIfError (log,
                                  "%p Communication::ReadFromConnection () => status = %s",
                                  p,
                                  Communication::ConnectionStatusAsCString (status));
            break;
        }
    }
    log = lldb_private::GetLogIfAllCategoriesSet (LIBLLDB_LOG_COMMUNICATION);
    if (log)
        log->Printf ("%p Communication::ReadThread () thread exiting...", p);

    // Let clients know that this thread is exiting
    comm->BroadcastEvent (eBroadcastBitReadThreadDidExit);
    return NULL;
}

void
Communication::SetReadThreadBytesReceivedCallback
(
    ReadThreadBytesReceived callback,
    void *callback_baton
)
{
    m_callback = callback;
    m_callback_baton = callback_baton;
}

void
Communication::SetConnection (Connection *connection)
{
    Disconnect (NULL);
    StopReadThread(NULL);
    m_connection_sp.reset(connection);
}

const char *
Communication::ConnectionStatusAsCString (lldb::ConnectionStatus status)
{
    switch (status)
    {
    case eConnectionStatusSuccess:        return "success";
    case eConnectionStatusError:          return "error";
    case eConnectionStatusTimedOut:       return "timed out";
    case eConnectionStatusNoConnection:   return "no connection";
    case eConnectionStatusLostConnection: return "lost connection";
    case eConnectionStatusEndOfFile:      return "end of file";
    }

    static char unknown_state_string[64];
    snprintf(unknown_state_string, sizeof (unknown_state_string), "ConnectionStatus = %i", status);
    return unknown_state_string;
}