/*
 * camsession.c - GStreamer CAM (EN50221) Session Layer
 * Copyright (C) 2007 Alessandro Decina
 * 
 * Authors:
 *   Alessandro Decina <alessandro@nnva.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "camsession.h"

#define GST_CAT_DEFAULT cam_debug_cat
#define I_TAG 0
#define I_LENGTH_FB 1

#define TAG_SESSION_NUMBER 0x90
#define TAG_OPEN_SESSION_REQUEST 0x91
#define TAG_OPEN_SESSION_RESPONSE 0x92
#define TAG_CREATE_SESSION 0x93
#define TAG_CREATE_SESSION_RESPONSE 0x94
#define TAG_CLOSE_SESSION_REQUEST 0x95
#define TAG_CLOSE_SESSION_RESPONSE 0x96

static CamReturn connection_data_cb (CamTL * tl, CamTLConnection * connection,
    guint8 * spdu, guint spdu_length);

static CamSLSession *
cam_sl_session_new (CamSL * sl, CamTLConnection * connection,
    guint16 session_nb, guint resource_id)
{
  CamSLSession *session = g_new0 (CamSLSession, 1);

  session->state = CAM_SL_SESSION_STATE_IDLE;
  session->sl = sl;
  session->connection = connection;
  session->session_nb = session_nb;
  session->resource_id = resource_id;

  return session;
}

static void
cam_sl_session_destroy (CamSLSession * session)
{
  g_free (session);
}

CamSL *
cam_sl_new (CamTL * tl)
{
  CamSL *sl = g_new0 (CamSL, 1);

  sl->sessions = g_hash_table_new_full (g_direct_hash, g_direct_equal,
      NULL, (GDestroyNotify) cam_sl_session_destroy);

  tl->user_data = sl;
  tl->connection_data = connection_data_cb;

  return sl;
}

void
cam_sl_destroy (CamSL * sl)
{
  g_hash_table_destroy (sl->sessions);

  g_free (sl);
}

CamReturn
cam_sl_create_session (CamSL * sl,
    CamTLConnection * connection, guint resource_id,
    CamSLSession ** out_session)
{
  CamReturn ret;
  CamSLSession *session = NULL;
  guint size;
  guint offset;
  guint8 *tpdu = NULL;
  guint8 *spdu;
  guint16 session_nb;

  /* FIXME: implement session number allocations properly */
  if (sl->session_ids == G_MAXUINT16)
    return CAM_RETURN_SESSION_TOO_MANY_SESSIONS;

  session_nb = ++sl->session_ids;
  session = cam_sl_session_new (sl, connection, session_nb, resource_id);

  /* SPDU layout (8 bytes):
   * TAG_CREATE_SESSION 1 byte
   * length_field () 1 byte
   * resource_id 4 bytes
   * session_nb 2 bytes
   */

  /* get TPDU size */
  cam_tl_calc_buffer_size (sl->tl, 8, &size, &offset);

  tpdu = (guint8 *) g_malloc (size);
  spdu = tpdu + offset;

  /* SPDU header */
  /* tag */
  spdu[0] = TAG_CREATE_SESSION;
  /* fixed length_field */
  spdu[1] = 6;

  /* SPDU body */
  /* resource id */
  GST_WRITE_UINT32_BE (&spdu[2], resource_id);
  /* session_nb */
  GST_WRITE_UINT16_BE (&spdu[6], session_nb);

  /* write the TPDU */
  ret = cam_tl_connection_write (session->connection, tpdu, size, 8);
  if (CAM_FAILED (ret))
    goto error;

  *out_session = session;

  g_free (tpdu);
  return CAM_RETURN_OK;

error:
  if (session)
    cam_sl_session_destroy (session);

  g_free (tpdu);

  return ret;
}

/* send a TAG_CLOSE_SESSION SPDU */
CamReturn
cam_sl_session_close (CamSLSession * session)
{
  CamReturn ret;
  guint size;
  guint offset;
  guint8 *tpdu = NULL;
  guint8 *spdu;
  CamSL *sl = session->sl;

  /* SPDU layout (4 bytes):
   * TAG_CLOSE_SESSION 1 byte
   * length_field () 1 byte
   * session_nb 2 bytes
   */

  /* get the size of the TPDU */
  cam_tl_calc_buffer_size (sl->tl, 4, &size, &offset);

  tpdu = (guint8 *) g_malloc (size);
  /* the spdu header starts after the TPDU headers */
  spdu = tpdu + offset;

  /* SPDU header */
  /* tag */
  spdu[0] = TAG_CLOSE_SESSION_REQUEST;
  /* fixed length_field */
  spdu[1] = 2;
  /* SPDU body */
  /* session_nb */
  GST_WRITE_UINT16_BE (&spdu[2], session->session_nb);

  /* write the TPDU */
  ret = cam_tl_connection_write (session->connection, tpdu, size, 4);
  if (CAM_FAILED (ret))
    goto error;

  session->state = CAM_SL_SESSION_STATE_CLOSING;

  g_free (tpdu);

  return CAM_RETURN_OK;

error:
  g_free (tpdu);

  return ret;
}

void
cam_sl_calc_buffer_size (CamSL * sl, guint body_length,
    guint * buffer_size, guint * offset)
{
  /* an APDU is sent in a SESSION_NUMBER SPDU, which has a fixed header size (4
   * bytes) */
  cam_tl_calc_buffer_size (sl->tl, 4 + body_length, buffer_size, offset);
  *offset += 4;
}

CamReturn
cam_sl_session_write (CamSLSession * session,
    guint8 * buffer, guint buffer_size, guint body_length)
{
  guint8 *spdu;

  /* SPDU layout (4 + body_length bytes):
   * TAG_SESSION_NUMBER (1 byte)
   * length_field (1 byte)
   * session number (2 bytes)
   * one or more APDUs (body_length bytes)
   */

  spdu = (buffer + buffer_size) - body_length - 4;
  spdu[0] = TAG_SESSION_NUMBER;
  spdu[1] = 2;
  GST_WRITE_UINT16_BE (&spdu[2], session->session_nb);

  /* add our header to the body length */
  return cam_tl_connection_write (session->connection,
      buffer, buffer_size, 4 + body_length);
}

static CamReturn
send_open_session_response (CamSL * sl, CamSLSession * session, guint8 status)
{
  CamReturn ret;
  guint8 *tpdu;
  guint size;
  guint offset;
  guint8 *spdu;

  /* SPDU layout (9 bytes):
   * TAG_OPEN_SESSION_RESPONSE 1 byte
   * length_field () 1 byte
   * session_status 1 byte
   * resource_id 4 bytes
   * session_nb 2 bytes
   */

  cam_tl_calc_buffer_size (session->sl->tl, 9, &size, &offset);

  tpdu = g_malloc0 (size);
  spdu = tpdu + offset;

  spdu[0] = TAG_OPEN_SESSION_RESPONSE;
  /* fixed length_field () */
  spdu[1] = 7;
  spdu[2] = status;
  GST_WRITE_UINT32_BE (&spdu[3], session->resource_id);
  GST_WRITE_UINT16_BE (&spdu[7], session->session_nb);

  ret = cam_tl_connection_write (session->connection, tpdu, size, 9);
  g_free (tpdu);
  if (CAM_FAILED (ret))
    return ret;

  return CAM_RETURN_OK;
}

static CamReturn
send_close_session_response (CamSL * sl, CamSLSession * session, guint8 status)
{
  CamReturn ret;
  guint8 *tpdu;
  guint size;
  guint offset;
  guint8 *spdu;

  /* SPDU layout (5 bytes):
   * TAG_CLOSE_SESSION_RESPONSE 1 byte
   * length_field () 1 byte
   * session_status 1 byte
   * session_nb 2 bytes
   */

  cam_tl_calc_buffer_size (session->sl->tl, 5, &size, &offset);

  tpdu = g_malloc0 (size);
  spdu = tpdu + offset;

  spdu[0] = TAG_OPEN_SESSION_RESPONSE;
  /* fixed length_field() */
  spdu[1] = 3;
  spdu[2] = status;
  GST_WRITE_UINT16_BE (&spdu[3], session->session_nb);

  ret = cam_tl_connection_write (session->connection, tpdu, size, 5);
  g_free (tpdu);
  if (CAM_FAILED (ret))
    return ret;

  return CAM_RETURN_OK;
}

static CamReturn
handle_open_session_request (CamSL * sl, CamTLConnection * connection,
    guint8 * spdu, guint spdu_length)
{
  CamReturn ret;
  guint resource_id;
  guint status;
  guint16 session_nb;
  CamSLSession *session;

  /* SPDU layout (6 bytes):
   * TAG_OPEN_SESSION_REQUEST (1 byte)
   * length_field() (1 byte)
   * resource id (4 bytes)
   */
  if (spdu_length != 6) {
    GST_ERROR ("expected OPEN_SESSION_REQUEST to be 6 bytes, got %d",
        spdu_length);
    return CAM_RETURN_SESSION_ERROR;
  }

  /* skip tag and length_field () */
  resource_id = GST_READ_UINT32_BE (&spdu[2]);

  /* create a new session */
  if (sl->session_ids == G_MAXUINT16) {
    GST_ERROR ("too many sessions opened");
    return CAM_RETURN_SESSION_TOO_MANY_SESSIONS;
  }

  session_nb = ++sl->session_ids;
  session = cam_sl_session_new (sl, connection, session_nb, resource_id);

  GST_INFO ("session request: %d %x", session_nb, session->resource_id);

  if (sl->open_session_request) {
    /* forward the request to the upper layer */
    ret = sl->open_session_request (sl, session, &status);
    if (CAM_FAILED (ret))
      goto error;
  } else {
    status = 0xF0;
  }

  ret = send_open_session_response (sl, session, (guint8) status);
  if (CAM_FAILED (ret))
    goto error;

  GST_INFO ("session request response: %d %x", session_nb, status);

  if (status == CAM_SL_RESOURCE_STATUS_OPEN) {
    /* if the session has been accepted add it and signal */
    session->state = CAM_SL_SESSION_STATE_ACTIVE;
    g_hash_table_insert (sl->sessions,
        GINT_TO_POINTER ((guint) session_nb), session);

    if (sl->session_opened) {
      /* notify the upper layer */
      ret = sl->session_opened (sl, session);
      if (CAM_FAILED (ret))
        return ret;
    }
  } else {
    /* session request wasn't accepted */
    cam_sl_session_destroy (session);
  }

  return CAM_RETURN_OK;

error:
  cam_sl_session_destroy (session);

  return ret;
}

static CamReturn
handle_create_session_response (CamSL * sl, CamTLConnection * connection,
    guint8 * spdu, guint spdu_length)
{
  guint16 session_nb;
  CamSLSession *session;

  /* SPDU layout (9 bytes):
   * TAG_CREATE_SESSION_RESPONSE (1 byte)
   * length_field() (1 byte)
   * status (1 byte)
   * resource id (4 bytes)
   * session number (2 bytes)
   */
  if (spdu_length != 9) {
    GST_ERROR ("expected CREATE_SESSION_RESPONSE to be 9 bytes, got %d",
        spdu_length);
    return CAM_RETURN_SESSION_ERROR;
  }

  /* skip tag and length */
  /* status = spdu[2]; */
  /* resource_id = GST_READ_UINT32_BE (&spdu[3]); */
  session_nb = GST_READ_UINT16_BE (&spdu[7]);

  session = g_hash_table_lookup (sl->sessions,
      GINT_TO_POINTER ((guint) session_nb));
  if (session == NULL) {
    GST_DEBUG ("got CREATE_SESSION_RESPONSE for unknown session: %d",
        session_nb);
    return CAM_RETURN_SESSION_ERROR;
  }

  if (session->state == CAM_SL_SESSION_STATE_CLOSING) {
    GST_DEBUG ("ignoring CREATE_SESSION_RESPONSE for closing session: %d",
        session_nb);
    return CAM_RETURN_OK;
  }

  session->state = CAM_SL_SESSION_STATE_ACTIVE;

  GST_DEBUG ("session opened %d", session->session_nb);

  if (sl->session_opened)
    /* notify the upper layer */
    return sl->session_opened (sl, session);
  return CAM_RETURN_OK;
}

static CamReturn
handle_close_session_request (CamSL * sl, CamTLConnection * connection,
    guint8 * spdu, guint spdu_length)
{
  CamReturn ret;
  guint16 session_nb;
  CamSLSession *session;
  guint8 status = 0;

  /* SPDU layout (4 bytes):
   * TAG_CLOSE_SESSION_REQUEST (1 byte)
   * length_field () (1 byte)
   * session number (2 bytes)
   */
  if (spdu_length != 4) {
    GST_ERROR ("expected CLOSE_SESSION_REQUEST to be 4 bytes, got %d",
        spdu_length);
    return CAM_RETURN_SESSION_ERROR;
  }

  /* skip tag and length_field() */
  session_nb = GST_READ_UINT16_BE (&spdu[2]);

  GST_DEBUG ("close session request %d", session_nb);

  session = g_hash_table_lookup (sl->sessions,
      GINT_TO_POINTER ((guint) session_nb));

  if (session == NULL) {
    GST_WARNING ("got CLOSE_SESSION_REQUEST for unknown session: %d",
        session_nb);
    return CAM_RETURN_OK;
  }

  if (session->state == CAM_SL_SESSION_STATE_CLOSING) {
    GST_WARNING ("got CLOSE_SESSION_REQUEST for closing session: %d",
        session_nb);
    status = 0xF0;
  }

  GST_DEBUG ("close session response: %d %d", session->session_nb, status);

  ret = send_close_session_response (sl, session, status);
  if (CAM_FAILED (ret))
    return ret;

  if (session->state != CAM_SL_SESSION_STATE_CLOSING) {
    GST_DEBUG ("session closed %d", session->session_nb);

    if (sl->session_closed)
      ret = sl->session_closed (sl, session);

    g_hash_table_remove (sl->sessions,
        GINT_TO_POINTER ((guint) session->session_nb));

    if (CAM_FAILED (ret))
      return ret;
  }

  return CAM_RETURN_OK;
}

static CamReturn
handle_close_session_response (CamSL * sl, CamTLConnection * connection,
    guint8 * spdu, guint spdu_length)
{
  guint16 session_nb;
  CamSLSession *session;
  CamReturn ret = CAM_RETURN_OK;

  /* SPDU layout (5 bytes):
   * TAG_CLOSE_SESSION_RESPONSE (1 byte)
   * length_field () (1 byte)
   * status (1 byte)
   * session number (2 bytes)
   */

  if (spdu_length != 5) {
    GST_ERROR ("expected CLOSE_SESSION_RESPONSE to be 5 bytes, got %d",
        spdu_length);
    return CAM_RETURN_SESSION_ERROR;
  }

  /* skip tag, length_field() and session_status */
  session_nb = GST_READ_UINT16_BE (&spdu[3]);

  session = g_hash_table_lookup (sl->sessions,
      GINT_TO_POINTER ((guint) session_nb));
  if (session == NULL || session->state != CAM_SL_SESSION_STATE_ACTIVE) {
    GST_ERROR ("unexpected CLOSED_SESSION_RESPONSE");
    return CAM_RETURN_SESSION_ERROR;
  }

  GST_DEBUG ("session closed %d", session->session_nb);

  if (sl->session_closed)
    ret = sl->session_closed (sl, session);

  g_hash_table_remove (sl->sessions,
      GINT_TO_POINTER ((guint) session->session_nb));

  return ret;
}

static CamReturn
handle_session_data (CamSL * sl, CamTLConnection * connection,
    guint8 * spdu, guint length)
{
  guint16 session_nb;
  CamSLSession *session;

  /* SPDU layout (>= 4 bytes):
   * TAG_SESSION_NUMBER (1 byte)
   * length_field() (1 byte)
   * session number (2 bytes)
   * one or more APDUs 
   */

  if (length < 4) {
    GST_ERROR ("invalid SESSION_NUMBER SPDU length %d", length);
    return CAM_RETURN_SESSION_ERROR;
  }

  session_nb = GST_READ_UINT16_BE (&spdu[2]);

  session = g_hash_table_lookup (sl->sessions,
      GINT_TO_POINTER ((guint) session_nb));
  if (session == NULL) {
    GST_ERROR ("got SESSION_NUMBER on an unknown connection: %d", session_nb);
    return CAM_RETURN_SESSION_ERROR;
  }

  if (sl->session_data)
    /* pass the APDUs to the upper layer, removing our 4-bytes header */
    return sl->session_data (sl, session, spdu + 4, length - 4);

  return CAM_RETURN_OK;
}

static CamReturn
connection_data_cb (CamTL * tl, CamTLConnection * connection,
    guint8 * spdu, guint spdu_length)
{
  CamReturn ret;
  CamSL *sl = CAM_SL (tl->user_data);

  switch (spdu[I_TAG]) {
    case TAG_CREATE_SESSION_RESPONSE:
      ret = handle_create_session_response (sl, connection, spdu, spdu_length);
      break;
    case TAG_OPEN_SESSION_REQUEST:
      ret = handle_open_session_request (sl, connection, spdu, spdu_length);
      break;
    case TAG_CLOSE_SESSION_REQUEST:
      ret = handle_close_session_request (sl, connection, spdu, spdu_length);
      break;
    case TAG_CLOSE_SESSION_RESPONSE:
      ret = handle_close_session_response (sl, connection, spdu, spdu_length);
      break;
    case TAG_SESSION_NUMBER:
      ret = handle_session_data (sl, connection, spdu, spdu_length);
      break;
    default:
      g_return_val_if_reached (CAM_RETURN_SESSION_ERROR);
  }

  return ret;
}