/**
 * xrdp: A Remote Desktop Protocol server.
 *
 * Copyright (C) Laxmikant Rashinkar 2012 LK.Rashinkar@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "drdynvc.h"

int g_drdynvc_chan_id;
int g_drdynvc_inited = 0;

static int APP_CC drdynvc_send_capability_request(uint16_t version);
static int APP_CC drdynvc_process_capability_response(struct stream* s, unsigned char cmd);
static int APP_CC drdynvc_process_open_channel_response(struct stream *s, unsigned char cmd);
static int APP_CC drdynvc_process_close_channel_response(struct stream *s, unsigned char cmd);
static int APP_CC drdynvc_process_data_first(struct stream* s, unsigned char cmd);
static int APP_CC drdynvc_process_data(struct stream* s, unsigned char cmd);
static int APP_CC drdynvc_insert_uint_124(struct stream *s, uint32_t val);
static int APP_CC drdynvc_get_chan_id(struct stream *s, char cmd, uint32_t *chan_id_p);

/**
 * bring up dynamic virtual channel
 *
 * @return 0 on success, -1 on response
 ******************************************************************************/
int APP_CC
drdynvc_init(void)
{
    /* bring up X11 */
    xcommon_init();

    /* let client know what version of DVC we support */
    drdynvc_send_capability_request(2);

    return 0;
}

/**
 * let DVC Manager on client end know what version of protocol we support
 * client will respond with version that it supports
 *
 * @return 0 on success, -1 on response
 ******************************************************************************/
static int APP_CC
drdynvc_send_capability_request(uint16_t version)
{
    struct stream *s;
    int bytes_in_stream;

    /* setup stream */
    make_stream(s);
    init_stream(s, MAX_PDU_SIZE);

    out_uint8(s, 0x50);         /* insert cmd       */
    out_uint8(s, 0x00);         /* insert padding   */
    out_uint16_le(s, version);  /* insert version   */

    /* channel priority unused for now              */
    out_uint16_le(s, 0x0000);  /* priority charge 0 */
    out_uint16_le(s, 0x0000);  /* priority charge 1 */
    out_uint16_le(s, 0x0000);  /* priority charge 2 */
    out_uint16_le(s, 0x0000);  /* priority charge 3 */

    /* send command to client */
    bytes_in_stream = stream_length_before_p(s);
    send_channel_data(g_drdynvc_chan_id, s->data, bytes_in_stream);
    free_stream(s);

    return 0;
}

/**
 * process capability response received from DVC manager at client end
 *
 * @param s stream containing client response
 *
 * @return 0 on success, -1 on failure
 ******************************************************************************/
static int APP_CC
drdynvc_process_capability_response(struct stream *s, unsigned char cmd)
{
    int cap_version;

    /* skip padding */
    in_uint8s(s, 1);

    /* read client's version */
    in_uint16_le(s, cap_version);

    if (cap_version != 2)
    {
        LOG(0, ("drdynvc_process_capability_response: incompatible DVC version %d detected", cap_version));
        return -1;
    }

    LOG(0, ("drdynvc_process_capability_response: DVC version 2 selected"));
    g_drdynvc_inited = 1;

    return 0;
}

/**
 * create a new dynamic virtual channel
 *
 * @pram  channel_id    channel id number
 * @pram  channel_name  name of channel
 *
 * @return 0 on success, -1 on failure
 ******************************************************************************/
int APP_CC
drdynvc_send_open_channel_request(int chan_pri, unsigned int chan_id,
                                  char *chan_name)
{
    struct stream *s;
    int            bytes_in_stream;
    int            cbChId;
    int            name_length;

    if ((chan_name == NULL) || (strlen(chan_name) == 0))
    {
        LOG(0, ("drdynvc_send_open_channel_request: bad channel name specified"));
        return -1;
    }

    make_stream(s);
    init_stream(s, MAX_PDU_SIZE);

    name_length = strlen(chan_name);

    /* dummy command for now */
    out_uint8(s, 0);

    /* insert channel id */
    cbChId = drdynvc_insert_uint_124(s, chan_id);

    /* insert channel name */
    out_uint8a(s, chan_name, name_length + 1);

    /* insert command */
    s->data[0] = CMD_DVC_OPEN_CHANNEL | ((chan_pri << 2) & 0x0c) | cbChId;

    /* send command */
    bytes_in_stream = stream_length_before_p(s);
    send_channel_data(g_drdynvc_chan_id, s->data, bytes_in_stream);
    free_stream(s);

    return 0;
}

static int APP_CC
drdynvc_process_open_channel_response(struct stream *s, unsigned char cmd)
{
    struct xrdp_api_data *adp;

    uint32_t chan_id;
    int      creation_status;

    drdynvc_get_chan_id(s, cmd, &chan_id);
    in_uint32_le(s, creation_status);

    /* LK_TODO now do something using useful! */

    if (creation_status < 0)
    {
        // TODO
    }
    else
    {
        /* get struct xrdp_api_data containing this channel id */
        if ((adp = struct_from_dvc_chan_id(chan_id)) == NULL)
        {
            LOG(0, ("drdynvc_process_open_channel_response: error : "
                    "could not find xrdp_api_data containing chan_id %d", chan_id));

            return -1;
        }

        adp->is_connected = 1;
    }

    return 0;
}

int APP_CC
drdynvc_send_close_channel_request(unsigned int chan_id)
{
    struct stream *s;
    int            bytes_in_stream;
    int            cbChId;

    make_stream(s);
    init_stream(s, MAX_PDU_SIZE);

    /* insert dummy cmd for now */
    out_uint8(s, 0);

    /* insert channel id */
    cbChId = drdynvc_insert_uint_124(s, chan_id);

    /* insert command */
    s->data[0] = CMD_DVC_CLOSE_CHANNEL | cbChId;

    /* send command */
    bytes_in_stream = stream_length_before_p(s);
    send_channel_data(g_drdynvc_chan_id, s->data, bytes_in_stream);

    free_stream(s);
    return 0;
}

static int APP_CC
drdynvc_process_close_channel_response(struct stream *s, unsigned char cmd)
{
    uint32_t chan_id;

    drdynvc_get_chan_id(s, cmd, &chan_id);

    /* LK_TODO now do something using useful! */

    return 0;
}

/*
 * send data to client
 *
 * @param  chan_id    the virtual channel to write to
 * @param  data       data to write
 * @param  data_size  number of bytes to write
 *
 * @return 0 on success, -1 on failure
 ******************************************************************************/
int APP_CC drdynvc_write_data(uint32_t chan_id, char *data, int data_size)
{
    struct stream *s;
    char          *saved_ptr;
    int            cbChId;
    int            Len;
    int            bytes_in_stream;
    int            frag_size;

    if (data == NULL)
    {
        LOG(0, ("drdynvc_write_data: data is NULL\n"));
        return -1;
    }

    if (data_size <= 0)
    {
        return 0;
    }

    make_stream(s);
    init_stream(s, MAX_PDU_SIZE);

    /* this is a dummy write */
    out_uint8(s, 0);

    /* insert channel id */
    cbChId = drdynvc_insert_uint_124(s, chan_id);

    /* will data fit into one pkt? */
    bytes_in_stream = stream_length_before_p(s);

    if ((bytes_in_stream + data_size) <= MAX_PDU_SIZE)
    {
        /* yes it will - insert data */
        out_uint8p(s, data, data_size);

        /* insert command */
        s->data[0] = CMD_DVC_DATA | cbChId;

        /* write data to client */
        bytes_in_stream = stream_length_before_p(s);
        send_channel_data(g_drdynvc_chan_id, s->data, bytes_in_stream);
        free_stream(s);
        return 0;
    }

    /* no it won't - fragment it */

    saved_ptr = s->p;

    /* let client know how much data to expect */
    Len = drdynvc_insert_uint_124(s, data_size);

    /* insert data into first fragment */
    frag_size = MAX_PDU_SIZE - stream_length_before_p(s);
    out_uint8p(s, data, frag_size);

    /* insert command */
    s->data[0] = CMD_DVC_DATA_FIRST | Len << 2 | cbChId;

    /* write first fragment to client */
    bytes_in_stream = stream_length_before_p(s);
    send_channel_data(g_drdynvc_chan_id, s->data, bytes_in_stream);
    data_size -= frag_size;
    data += frag_size;
    s->data[0] = CMD_DVC_DATA | cbChId;
    s->p = saved_ptr;

    /* now send rest of the data using CMD_DVC_DATA */
    while (data_size > 0)
    {
        frag_size = MAX_PDU_SIZE - stream_length_before_p(s);

        if (frag_size > data_size)
        {
            frag_size = data_size;
        }

        out_uint8p(s, data, frag_size);
        bytes_in_stream = stream_length_before_p(s);
        send_channel_data(g_drdynvc_chan_id, s->data, bytes_in_stream);
        data_size -= frag_size;
        data += frag_size;
        s->p = saved_ptr;
    }

    free_stream(s);
    return 0;
}

static int APP_CC
drdynvc_process_data_first(struct stream *s, unsigned char cmd)
{
    struct xrdp_api_data *adp;
    struct stream        *ls;

    uint32_t chan_id;
    int      bytes_in_stream;
    int      data_len;
    int      Len;

    drdynvc_get_chan_id(s, cmd, &chan_id);

    Len = (cmd >> 2) & 0x03;

    if (Len == 0)
    {
        in_uint8(s, data_len);
    }
    else if (Len == 1)
    {
        in_uint16_le(s, data_len);
    }
    else
    {
        in_uint32_le(s, data_len);
    }

    bytes_in_stream = stream_length_after_p(s);

    /* get struct xrdp_api_data containing this channel id */
    if ((adp = struct_from_dvc_chan_id(chan_id)) == NULL)
    {
        LOG(0, ("drdynvc_process_data_first: error : "
                "could not find xrdp_api_data containing chan_id %d", chan_id));

        return -1;
    }

    ls = trans_get_out_s(adp->transp, MAX_PDU_SIZE);
    out_uint8p(ls, s->p, bytes_in_stream);
    s_mark_end(ls);
    trans_force_write(adp->transp);

    return 0;
}

static int APP_CC
drdynvc_process_data(struct stream *s, unsigned char cmd)
{
    struct xrdp_api_data *adp;
    struct stream        *ls;

    uint32_t chan_id;
    int      bytes_in_stream;

    drdynvc_get_chan_id(s, cmd, &chan_id);
    bytes_in_stream = stream_length_after_p(s);

    /* get struct xrdp_api_data containing this channel id */
    if ((adp = struct_from_dvc_chan_id(chan_id)) == NULL)
    {
        LOG(0, ("drdynvc_process_data: error : "
                "could not find xrdp_api_data containing chan_id %d", chan_id));

        return -1;
    }

    ls = trans_get_out_s(adp->transp, MAX_PDU_SIZE);
    out_uint8p(ls, s->p, bytes_in_stream);
    s_mark_end(ls);
    trans_force_write(adp->transp);

    return 0;
}

/**
 * process incoming data on a dynamic virtual channel
 *
 * @pram  s             stream containing the incoming data
 * @pram  chand_id      LK_TODO
 * @pram  chan_flags    LK_TODO
 * @pram  length        LK_TODO
 * @pram  total_length  LK_TODO
 *
 * @return 0 on success, -1 on failure
 ******************************************************************************/
int APP_CC
drdynvc_data_in(struct stream *s, int chan_id, int chan_flags, int length,
                int total_length)
{
    unsigned char cmd;

    in_uint8(s, cmd); /* read command */

    switch (cmd & 0xf0)
    {
        case CMD_DVC_CAPABILITY:
            drdynvc_process_capability_response(s, cmd);
            break;

        case CMD_DVC_OPEN_CHANNEL:
            drdynvc_process_open_channel_response(s, cmd);
            break;

        case CMD_DVC_CLOSE_CHANNEL:
            drdynvc_process_close_channel_response(s, cmd);
            break;

        case CMD_DVC_DATA_FIRST:
            drdynvc_process_data_first(s, cmd);
            break;

        case CMD_DVC_DATA:
            drdynvc_process_data(s, cmd);
            break;

        default:
            LOG(0, ("drdynvc_data_in: got unknown command 0x%x", cmd));
            break;
    }

    return 0;
}

/*
 * insert a byte, short or 32bit value into specified stream
 *
 * @param  s    stream used for insertion
 * @param  val  value to insert
 *
 * @return 0 for byte insertions
 * @return 1 for short insertion
 * @return 2 for uint32_t insertions
 ******************************************************************************/
static int APP_CC
drdynvc_insert_uint_124(struct stream *s, uint32_t val)
{
    int ret_val;

    if (val <= 0xff)
    {
        out_uint8(s, val);
        ret_val = 0;
    }
    else if (val <= 0xffff)
    {
        out_uint16_le(s, val);
        ret_val = 1;
    }
    else
    {
        out_uint32_le(s, val);
        ret_val = 2;
    }

    return ret_val;
}

/*
 * extract channel id from stream
 *
 * @param   s        stream containing channel id
 * @param   cmd      first byte in stream
 * @param   chan_id  return channel id  here
 ******************************************************************************/
static int APP_CC
drdynvc_get_chan_id(struct stream *s, char cmd, uint32_t *chan_id_p)
{
    int cbChId;
    int chan_id;

    cbChId = cmd & 0x03;

    if (cbChId == 0)
    {
        in_uint8(s, chan_id);
    }
    else if (cbChId == 1)
    {
        in_uint16_le(s, chan_id);
    }
    else
    {
        in_uint32_le(s, chan_id);
    }

    *chan_id_p = chan_id;

    return 0;
}
