
#include "pch.h"

#ifndef WIN32
#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#endif

#include <boost/bind.hpp>

#include "foreign_menu.h"
#include "foreign_menu_prot.h"

#ifndef INVALID_HANDLE_VALUE
#define INVALID_HANDLE_VALUE (-1)
#endif

ForeignMenu::ForeignMenu(ForeignMenuListener& listener) :
    _listener(listener),
    _ipc_fd(INVALID_HANDLE_VALUE),
    _menu_events_thread(NULL),
    _tail_ptr(_buffer)
{
#ifdef WIN32
    ZeroMemory(&_overlapped, sizeof(_overlapped));
    _overlapped.hEvent = reinterpret_cast<HANDLE>(this);
#endif
}

ForeignMenu::~ForeignMenu()
{
    if (_menu_events_thread != NULL)
    {
        stop();
    }
}

bool ForeignMenu::open_connection(const std::string& pipe_name)
{
    ASSERT(_menu_events_thread == NULL);

    LOG_INFO("Openning a foreign menu connection: " << pipe_name.c_str());

#ifdef WIN32
    _ipc_fd = ::CreateFileA(pipe_name.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED | SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS,
        NULL);
#else
    _ipc_fd = socket(AF_UNIX, SOCK_STREAM, 0);

    if (_ipc_fd != -1)
    {
        if (connect_foreign_menu(pipe_name) == false)
        {
            close(_ipc_fd);
            _ipc_fd = -1;
        }
    }
#endif

    if (_ipc_fd == INVALID_HANDLE_VALUE)
    {
#ifdef WIN32
        LOG_ERROR("Failed to open a foreign menu connection (error = " << ::GetLastError() << ")");
#else
        LOG_ERROR("Failed to open a foreign menu connection (error = " << errno << ")");
#endif
    }

    return (_ipc_fd != INVALID_HANDLE_VALUE);
}

void ForeignMenu::start()
{
    ASSERT(_ipc_fd != INVALID_HANDLE_VALUE);

    _menu_events_thread = new boost::thread(
        boost::bind(&ForeignMenu::wait_for_menu_events, this));

    if (_menu_events_thread == NULL)
    {
        LOG_ERROR("Failed to create a boost:thread");
#ifdef WIN32
        ::CloseHandle(_ipc_fd);
#else
        close(_ipc_fd);
#endif
        _ipc_fd = INVALID_HANDLE_VALUE;
    }
}

void ForeignMenu::stop()
{
    if (_menu_events_thread != NULL)
    {
#ifdef WIN32
        ::CloseHandle(_ipc_fd);
#else
        close(_ipc_fd);
#endif
        _ipc_fd = INVALID_HANDLE_VALUE;

        _menu_events_thread->join();

        delete _menu_events_thread;
        _menu_events_thread = NULL;
    }
}

void ForeignMenu::init(const std::string& title)
{
	LOG_INFO("Initializing foreign menu: " << title.c_str());

	FrgMenuInit msg;

    msg.base.magic = FOREIGN_MENU_MAGIC;
	msg.base.size = sizeof(FrgMenuInit) + title.length() + 1;
	msg.base.version = FOREIGN_MENU_VERSION;

	msg.credentials = 0;

	write_to_client(&msg, sizeof(msg));
	write_to_client(title.c_str(), title.length() + 1);
}

void ForeignMenu::set_title(const std::string& title)
{
    LOG_INFO("Setting foreign menu title: " << title.c_str());

    FrgMenuSetTitle msg;

    msg.base.id = FOREIGN_MENU_SET_TITLE;
    msg.base.size = sizeof(FrgMenuSetTitle) + title.length() + 1;

    write_to_client(&msg, sizeof(msg));
    write_to_client(title.c_str(), title.length() + 1);
}

void ForeignMenu::add_item(uint32_t id, const std::string& name, uint32_t type)
{
    LOG_INFO("Adding foreign menu item: " << name.c_str());

    FrgMenuAddItem msg;

    msg.base.id = FOREIGN_MENU_ADD_ITEM;
    msg.base.size = sizeof(FrgMenuAddItem) + name.length() + 1;

    msg.id = id;
    msg.type = type;
    msg.position = 0;

    write_to_client(&msg, sizeof(msg));
    write_to_client(name.c_str(), name.length() + 1);
}

void ForeignMenu::modify_item()
{
    LOG_INFO("Modifing existing foreign menu item: (not implemented!)");
    return;

    FrgMenuModItem msg;

    msg.base.id = FOREIGN_MENU_MODIFY_ITEM;
    msg.base.size = sizeof(FrgMenuModItem);

    write_to_client(&msg, sizeof(msg));
}

void ForeignMenu::remove_item()
{
    LOG_INFO("Removing existing foreign menu item: (not implemented!)");
    return;

    FrgMenuRmItem msg;

    msg.base.id = FOREIGN_MENU_REMOVE_ITEM;
    msg.base.size = sizeof(FrgMenuRmItem);

    write_to_client(&msg, sizeof(msg));
}

void ForeignMenu::remove_items()
{
    LOG_INFO("Removing all existing foreign menu items.");

    FrgMenuRmItems msg;

    msg.id = FOREIGN_MENU_CLEAR;
    msg.size = sizeof(FrgMenuRmItems);

    write_to_client(&msg, sizeof(msg));
}

void ForeignMenu::wait_for_menu_events()
{
    int ret = 1;

    while (ret > 0)
    {
#ifdef WIN32
        ret = ::ReadFileEx(_ipc_fd,
            _tail_ptr, sizeof(_buffer) - (_tail_ptr - _buffer),
            &_overlapped,
            read_completion_routine);

        if (ret != FALSE)
        {
            ::SleepEx(INFINITE, TRUE);
        }
        else
        {
            LOG_ERROR("Error waiting for client data (error = " << ::GetLastError() << ")");
            ret = -1;
        }
#else
        fd_set rfds;
        int nfds;

        FD_ZERO(&rfds);
        FD_SET(_ipc_fd, &rfds);
        nfds = _ipc_fd;

        ret = select(nfds + 1, &rfds, NULL, NULL, NULL);
        if (ret > 0)
        {
            if (FD_ISSET(_ipc_fd, &rfds))
            {
                int size = read(_ipc_fd, _tail_ptr, sizeof(_buffer) - (_tail_ptr - _buffer));
                if (size > 0)
                {
                    read_menu_events(size);
                }
                else
                {
                    LOG_ERROR("Error reading client data (error = " << errno << ")")
                    ret = -1;
                }
            }
        }
        else
        {
            LOG_ERROR("Error waiting for client data (error = " << errno << ")");
        }
#endif
    }

    _listener.on_connection_close();
}

#ifdef WIN32

void ForeignMenu::read_completion_routine(unsigned long err, unsigned long size, LPOVERLAPPED overlapped)
{
    if (err == ERROR_SUCCESS)
    {
        ForeignMenu *pthis = reinterpret_cast<ForeignMenu*>(overlapped->hEvent);
        pthis->read_menu_events(size);
    }
    else
    {
        LOG_ERROR("Error on read completion (err=" << err << " , GetLastError=" << ::GetLastError() << ")");
    }
}

#else

bool ForeignMenu::connect_foreign_menu(const std::string& pipe_name)
{
    struct sockaddr_un remote;
    int len, flags;

    remote.sun_family = AF_UNIX;
    strcpy(remote.sun_path, pipe_name.c_str());
    len = sizeof(remote.sun_family) + pipe_name.length();

    if (connect(_ipc_fd, (struct sockaddr *)&remote, len) == -1)
    {
        LOG_ERROR("Error connecting foreign menu (error = " << errno << ")");
        return false;
    }

    if ((flags = fcntl(_ipc_fd, F_GETFL)) == -1)
    {
        LOG_ERROR("Error getting fd flags (error = " << errno << ")");
        return false;
    }

    if (fcntl(_ipc_fd, F_SETFL, flags | O_NONBLOCK) == -1)
    {
        LOG_ERROR("Error setting fd flags to non-blocking (error = " << errno << ")");
        return false;
    }

    return true;
}

#endif

void ForeignMenu::read_menu_events(unsigned long size)
{
    char *head_ptr = _buffer;
    size_t nread = _tail_ptr - _buffer;

    if (size == 0) {
#ifdef WIN32
        int err = ::GetLastError();
#else
        int err = errno;
#endif
        LOG_ERROR("No data was read from client (error = " <<  err << ")");
        return;
    }

    nread += size;

    while (nread > 0) {

        if (nread < sizeof(FrgMenuMsg))
        {
            // need to wait for more header information.
            break;
        }

        FrgMenuMsg *hdr = reinterpret_cast<FrgMenuMsg *>(head_ptr);

        if (hdr->size == 0) {
            LOG_ERROR("Ignoring bad foreign menu message (size = " << hdr->size << ")");
            nread = 0;
            break;
        }

        if (nread < hdr->size) {
            // need to wait for more data.
            break;
        }

        handle_foreign_menu_message(hdr);

        nread -= hdr->size;
        head_ptr += hdr->size;
    }

    if ((nread > 0) && (head_ptr != _buffer))
    {
        memcpy(_buffer, head_ptr, nread);
    }

    _tail_ptr = _buffer + nread;
}

void ForeignMenu::handle_foreign_menu_message(FrgMenuMsg *hdr)
{
    switch (hdr->id)
    {
    case FOREIGN_MENU_EVENT:
    {
        FrgMenuEvent *evt = reinterpret_cast<FrgMenuEvent*>(hdr);
        LOG_DEBUG("Got a menu event: FOREIGN_MENU_EVENT " << evt->id);
        _listener.on_menu_item_click(evt->id);
        break;
    }
    case FOREIGN_MENU_ACTIVATE:
        LOG_DEBUG("Got a menu event: FOREIGN_MENU_ACTIVATE");
        _listener.on_menu_activate();
        break;
    case FOREIGN_MENU_DEACTIVATE:
        LOG_DEBUG("Got a menu event: FOREIGN_MENU_DEACTIVATE");
        _listener.on_menu_deactivate();
        break;
    default:
        LOG_ERROR("Ignoring an unknown foreign menu identifier " << hdr->id);
        break;
    }
}

void ForeignMenu::write_to_client(const void *buf, uint32_t len)
{
#ifdef WIN32
    unsigned long written = 0;
    OVERLAPPED overlap;

    ZeroMemory(&overlap, sizeof(overlap));
    overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

    if (::WriteFile(_ipc_fd, buf, len, &written, &overlap) != FALSE) {
        LOG_TRACE("Sending " << written << "/" << len << " bytes to client.");
    }
    else {
        DWORD err = ::GetLastError();
        LOG_TRACE("err= " << err << ", wrote " << written << "/" << len << ")");
        if (err == ERROR_IO_PENDING) {
            LOG_TRACE("write is asynchroneous");
            DWORD wait_ret = WaitForSingleObject(overlap.hEvent, 3000);
            if (wait_ret != WAIT_OBJECT_0) {
                LOG_ERROR("WaitForSingleObject() failed: " << wait_ret << "  error:" <<
                          ((wait_ret == WAIT_FAILED) ? GetLastError() : 0));
            } else {
                LOG_TRACE("async write comleted");
            }
        } else {
            LOG_ERROR("Error sending data to client (error = " << ::GetLastError() << ")");
        }
    }
    ::CloseHandle(overlap.hEvent);

#else
    write(_ipc_fd, buf, len);
#endif
}
