/*
   Copyright (C) 2009 Red Hat, Inc.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/

#include "common.h"
#include "foreign_menu.h"
#include <spice/foreign_menu_prot.h>
#include "menu.h"
#include "utils.h"
#include "debug.h"
#include "platform.h"

#define PIPE_NAME_MAX_LEN 50

#ifdef WIN32
#define PIPE_NAME "SpiceForeignMenu-%lu"
#elif defined(__i386__)
#define PIPE_NAME "/tmp/SpiceForeignMenu-%llu.uds"
#else
#define PIPE_NAME "/tmp/SpiceForeignMenu-%lu.uds"
#endif

ForeignMenu::ForeignMenu(ForeignMenuInterface *handler, bool active)
    : _handler (handler)
    , _active (active)
    , _refs (1)
{
    char pipe_name[PIPE_NAME_MAX_LEN];

    ASSERT(_handler != NULL);
    snprintf(pipe_name, PIPE_NAME_MAX_LEN, PIPE_NAME, Platform::get_process_id());
    LOG_INFO("Creating a foreign menu connection %s", pipe_name);
    _foreign_menu = NamedPipe::create(pipe_name, *this);
    if (!_foreign_menu) {
        LOG_ERROR("Failed to create a foreign menu connection");
    }
}

ForeignMenu::~ForeignMenu()
{
    std::map<NamedPipe::ConnectionRef, ForeignMenuConnection*>::const_iterator conn;
    for (conn = _connections.begin(); conn != _connections.end(); ++conn) {
        conn->second->reset_handler();
        delete conn->second;
    }
    if (_foreign_menu) {
        NamedPipe::destroy(_foreign_menu);
    }
}

NamedPipe::ConnectionInterface& ForeignMenu::create()
{
    ForeignMenuConnection *conn = new ForeignMenuConnection(_handler, *this);

    if (conn == NULL) {
        throw Exception("Error allocating a new foreign menu connection");
    }
    return *conn;
}

void ForeignMenu::add_connection(NamedPipe::ConnectionRef conn_ref, ForeignMenuConnection *conn)
{
    _connections[conn_ref] = conn;
    if (_active) {
        send_active_state(conn, FOREIGN_MENU_APP_ACTIVATED);
    }
    conn->on_data();
}

void ForeignMenu::remove_connection(NamedPipe::ConnectionRef conn_ref)
{
    ForeignMenuConnection *conn = _connections[conn_ref];
    _connections.erase(conn_ref);
    delete conn;
}

void ForeignMenu::add_sub_menus()
{
    std::map<NamedPipe::ConnectionRef, ForeignMenuConnection*>::const_iterator conn;
    for (conn = _connections.begin(); conn != _connections.end(); ++conn) {
        conn->second->add_sub_menu();
    }
}

void ForeignMenu::on_command(NamedPipe::ConnectionRef conn_ref, int32_t id)
{
    ForeignMenuConnection *conn = _connections[conn_ref];
    FrgMenuEvent msg;

    ASSERT(conn);
    msg.base.id = FOREIGN_MENU_ITEM_EVENT;
    msg.base.size = sizeof(FrgMenuEvent);
    msg.id = id;
    msg.action = FOREIGN_MENU_EVENT_CLICK;
    conn->write_msg(&msg.base, msg.base.size);
}

void ForeignMenu::on_activate()
{
    std::map<NamedPipe::ConnectionRef, ForeignMenuConnection*>::const_iterator conn;
    _active = true;
    for (conn = _connections.begin(); conn != _connections.end(); ++conn) {
        send_active_state(conn->second, FOREIGN_MENU_APP_ACTIVATED);
    }
}

void ForeignMenu::on_deactivate()
{
    std::map<NamedPipe::ConnectionRef, ForeignMenuConnection*>::const_iterator conn;
    _active = false;
    for (conn = _connections.begin(); conn != _connections.end(); ++conn) {
        send_active_state(conn->second, FOREIGN_MENU_APP_DEACTIVATED);
    }
}

void ForeignMenu::send_active_state(ForeignMenuConnection *conn, int32_t cmd)
{
    FrgMenuMsg msg;

    ASSERT(conn != NULL);
    msg.id = cmd;
    msg.size = sizeof(FrgMenuMsg);
    conn->write_msg(&msg, msg.size);
}

ForeignMenuConnection::ForeignMenuConnection(ForeignMenuInterface *handler, ForeignMenu& parent)
    : _handler (handler)
    , _parent (parent)
    , _sub_menu (NULL)
    , _initialized (false)
    , _write_pending (0)
    , _write_pos (_write_buf)
    , _read_pos (_read_buf)
{
}

ForeignMenuConnection::~ForeignMenuConnection()
{
    if (_opaque != NamedPipe::INVALID_CONNECTION) {
        NamedPipe::destroy_connection(_opaque);
    }
    if (_handler) {
        AutoRef<Menu> app_menu(_handler->get_app_menu());
        (*app_menu)->remove_sub(_sub_menu);
        _handler->update_menu();
        _handler->clear_menu_items(_opaque);
    }
    if (_sub_menu) {
        _sub_menu->unref();
    }
}

void ForeignMenuConnection::bind(NamedPipe::ConnectionRef conn_ref)
{
    _opaque = conn_ref;
    _parent.add_connection(conn_ref, this);
}

void ForeignMenuConnection::on_data()
{
    if (_write_pending) {
        LOG_INFO("Resume pending write %d", _write_pending);
        if (!write_msg(_write_pos, _write_pending)) {
            return;
        }
    }
    while (read_msgs());
}

bool ForeignMenuConnection::read_msgs()
{
    uint8_t *pos = _read_buf;
    size_t nread = _read_pos - _read_buf;
    int32_t size;

    ASSERT(_handler);
    ASSERT(_opaque != NamedPipe::INVALID_CONNECTION);
    size = NamedPipe::read(_opaque, (uint8_t*)_read_pos, sizeof(_read_buf) - nread);
    if (size == 0) {
        return false;
    } else if (size < 0) {
        LOG_ERROR("Error reading from named pipe %d", size);
        _parent.remove_connection(_opaque);
        return false;
    }
    nread += size;
    while (nread > 0) {
        if (!_initialized && nread >= sizeof(FrgMenuInitHeader)) {
            FrgMenuInitHeader *init = (FrgMenuInitHeader *)pos;
            if (init->magic != FOREIGN_MENU_MAGIC || init->version != FOREIGN_MENU_VERSION) {
                LOG_ERROR("Bad foreign menu init, magic=0x%x version=%u", init->magic,
                          init->version);
                _parent.remove_connection(_opaque);
                return false;
            }
            if (nread < init->size) {
                break;
            }
            if (!handle_init((FrgMenuInit*)init)) {
                _parent.remove_connection(_opaque);
                return false;
            }
            nread -= init->size;
            pos += init->size;
            _initialized = true;
        }
        if (!_initialized || nread < sizeof(FrgMenuMsg)) {
            break;
        }
        FrgMenuMsg *hdr = (FrgMenuMsg *)pos;
        if (hdr->size < sizeof(FrgMenuMsg)) {
            LOG_ERROR("Bad foreign menu message, size=%u", hdr->size);
            _parent.remove_connection(_opaque);
            return false;
        }
        if (nread < hdr->size) {
            break;
        }
        handle_message(hdr);
        nread -= hdr->size;
        pos += hdr->size;
    }
    if (nread > 0 && pos != _read_buf) {
        memcpy(_read_buf, pos, nread);
    }
    _read_pos = _read_buf + nread;
    return true;
}

bool ForeignMenuConnection::write_msg(const void *buf, int len)
{
    RecurciveLock lock(_write_lock);
    uint8_t *pos;
    int32_t written = 0;

    ASSERT(_opaque != NamedPipe::INVALID_CONNECTION);
    if (_write_pending && buf != _write_pos) {
        if ((_write_pos + _write_pending + len > _write_buf + sizeof(_write_buf)) &&
                                              !write_msg(_write_pos, _write_pending)) {
            return false;
        }
        if (_write_pending) {
            if (_write_pos + _write_pending + len > _write_buf + sizeof(_write_buf)) {
                DBG(0, "Dropping message, due to insufficient space in write buffer");
                return true;
            }
            memcpy(_write_pos + _write_pending, buf, len);
            _write_pending += len;
        }
    }
    pos = (uint8_t*)buf;
    while (len && (written = NamedPipe::write(_opaque, pos, len)) > 0) {
        pos += written;
        len -= written;
    }
    if (len && written == 0) {
        if (_write_pending) {
            _write_pos = pos;
        } else {
            _write_pos = _write_buf;
            memcpy(_write_buf, pos, len);
        }
        _write_pending = len;
    } else if (written < 0) {
        LOG_ERROR("Error writing to named pipe %d", written);
        _parent.remove_connection(_opaque);
        return false;
    } else {
        _write_pending = 0;
    }
    return true;
}

bool ForeignMenuConnection::handle_init(FrgMenuInit *init)
{
    std::string title = "Untitled";

    ASSERT(_handler);
    if (_sub_menu) {
        LOG_ERROR("Foreign menu already initialized");
        return false;
    }
    if (init->credentials != 0) {
        LOG_ERROR("Foreign menu has wrong credentials 0x%x", init->credentials);
        return false;
    }
    if (init->base.size > offsetof(FrgMenuInit, title)) {
        ((char*)init)[init->base.size - 1] = '\0';
        title = (char*)init->title;
    }
    _sub_menu = new Menu((CommandTarget&)*_handler, title);
    add_sub_menu();
    _handler->update_menu();
    return true;
}

void ForeignMenuConnection::add_sub_menu()
{
    if (_sub_menu) {
        AutoRef<Menu> app_menu(_handler->get_app_menu());
        (*app_menu)->add_sub(_sub_menu);
    }
}

bool ForeignMenuConnection::handle_message(FrgMenuMsg *hdr)
{
    ASSERT(_sub_menu);
    ASSERT(_handler);
    switch (hdr->id) {
    case FOREIGN_MENU_SET_TITLE:
        ((char*)hdr)[hdr->size - 1] = '\0';
        _sub_menu->set_name((char*)((FrgMenuSetTitle*)hdr)->string);
        break;
    case FOREIGN_MENU_ADD_ITEM: {
        FrgMenuAddItem *msg = (FrgMenuAddItem*)hdr;
        ((char*)hdr)[hdr->size - 1] = '\0';
        int id = _handler->get_foreign_menu_item_id(_opaque, msg->id);
        _sub_menu->add_command((char*)msg->string, id, get_item_state(msg->type));
        break;
    }
    case FOREIGN_MENU_REMOVE_ITEM: {
        int id = _handler->get_foreign_menu_item_id(_opaque, ((FrgMenuRmItem*)hdr)->id);
        _sub_menu->remove_command(id);
        _handler->remove_menu_item(id);
        break;
    }
    case FOREIGN_MENU_CLEAR:
        _sub_menu->clear();
        _handler->clear_menu_items(_opaque);
        break;
    case FOREIGN_MENU_MODIFY_ITEM:
    default:
        LOG_ERROR("Ignoring an unknown foreign menu identifier %u", hdr->id);
        return false;
    }
    _handler->update_menu();
    return true;
}

int ForeignMenuConnection::get_item_state(int item_type)
{
    int state = 0;

    if (item_type & FOREIGN_MENU_ITEM_TYPE_CHECKED) {
        state |= Menu::MENU_ITEM_STATE_CHECKED;
    }
    if (item_type & FOREIGN_MENU_ITEM_TYPE_DIM) {
        state |= Menu::MENU_ITEM_STATE_DIM;
    }
    return state;
}
