/****************************************************************************
 * apps/testing/sched/schedtest/rwsem.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you 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.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>
#include <nuttx/rwsem.h>
#include <pthread.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <cmocka.h>

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

typedef struct
{
  rw_semaphore_t rwsem;
  int shared_resource;
}shared_data_t;

typedef struct
{
    shared_data_t *data;
    int id;
}reader_arg_t;

static void *reader_thread(void *arg);
static void *writer_thread(void *arg);
static void test_rwsem(void **state);

/****************************************************************************
 * Private Functions
 ****************************************************************************/

static void *writer_thread(void *arg)
{
    shared_data_t *data = (shared_data_t *)arg;
    int id = 1;

    down_write(&data->rwsem);
    printf("writer %d acquired write lock.\n", id);
    data->shared_resource += 10;
    printf("writer %d updated shared_resource "
           "to %d.\n", id, data->shared_resource);
    sleep(2);
    up_write(&data->rwsem);
    printf("writer %d released write lock.\n", id);

  return NULL;
}

static void *reader_thread(void *arg)
{
  reader_arg_t *args = (reader_arg_t *)arg;
  shared_data_t *data = args->data;
  int id = args->id;

  switch (id)
    {
      case 1:
        printf("Reader %d attempt to acquire read lock (blocking).\n", id);
        down_read(&data->rwsem);
        printf("Reader %d acquired read lock.\n", id);
        assert_int_equal(data->shared_resource, 10);
        up_read(&data->rwsem);
        printf("Reader %d released read lock.\n", id);
        break;

      case 2:
        printf("Reader %d attempt to acquire read lock (blocking).\n", id);
        int acquired = down_read_trylock(&data->rwsem);
        assert_int_equal(acquired, 0);
        printf("Reader %d failed to acquire read lock as expected.\n", id);
        break;

      case 3:
        printf("Reader %d attempt to acquire first read lock.\n", id);
        down_read(&data->rwsem);
        printf("Reader %d acquired first read lock.\n", id);

        printf("Reader %d attempt to acquire second read lock.\n", id);
        down_read(&data->rwsem);
        assert_int_equal(data->shared_resource, 10);

        up_read(&data->rwsem);
        printf("Reader %d released read lock.\n", id);

        up_read(&data->rwsem);
        printf("Reader %d released read lock.\n", id);
        break;

      default:
        printf("Unknwown reader id: %d\n", id);
        break;
    }

  free(args);
  return NULL;
}

static void test_rwsem(void **state)
{
  shared_data_t *data = (shared_data_t *)*state;
  pthread_t readers[3], writer;
  int rd_id[] =
    {
      1, 2, 3
    };

  int i;
  init_rwsem(&data->rwsem);
  data->shared_resource = 0;

  pthread_create(&writer,  NULL, writer_thread, data);

  for (i = 0; i < 3; i++)
    {
        reader_arg_t *args = malloc(sizeof(reader_arg_t));
        args->data = data;
        args->id = rd_id[i];
        pthread_create(&readers[i], NULL, reader_thread, args);
    }

  for (i = 0; i < 3; i++)
    {
      pthread_join(readers[i], NULL);
    }

  pthread_join(writer, NULL);

  destroy_rwsem(&data->rwsem);
  printf("rwsem tests completed.\n");
}

static void *downgrade_thread(void *arg)
{
  reader_arg_t *args = (reader_arg_t *)arg;
  shared_data_t *data = args->data;
  int id = args->id;

  printf("Downgrade thread [%d] attempt to acquire write lock.\n", id);
  down_write(&data->rwsem);
  printf("Downgrade thread [%d] acquired write lock.\n", id);
  data->shared_resource = id;
  sleep(1);

  printf("Downgrade thread [%d] attempt to downgrade to read lock.\n", id);
  downgrade_write(&data->rwsem);
  printf("Downgrade thread [%d] acquired read lock.\n", id);
  assert_true(data->shared_resource == id);
  sleep(1);

  up_read(&data->rwsem);
  printf("Downgrade thread [%d] released read lock.\n", id);

  return NULL;
}

static void *reader_thread2(void *arg)
{
  reader_arg_t *args = (reader_arg_t *)arg;
  shared_data_t *data = args->data;
  int id = args->id;

  printf("Reader [%d] attempt to acquire read lock.\n", id);
  down_read(&data->rwsem);
  printf("Reader [%d] acquired read lock.\n", id);

  sleep(2);

  up_read(&data->rwsem);
  printf("Reader [%d] released read lock.\n", id);

  return NULL;
}

static void test_downgrade_write(void **state)
{
  shared_data_t *data = (shared_data_t *)*state;
  reader_arg_t args[3];
  pthread_t grader[3];
  pthread_t reader;
  int i;

  init_rwsem(&data->rwsem);
  data->shared_resource = 0;

  for (i = 0; i < 3; i++)
    {
      args[i].data = data;
      args[i].id = i + 1;
      pthread_create(&grader[i],  NULL, downgrade_thread, &args[i]);
    }

  pthread_create(&reader, NULL, reader_thread2, &args[0]);

  for (i = 0; i < 3; i++)
    {
      pthread_join(grader[i], NULL);
    }

  pthread_join(reader, NULL);
  assert_true(data->rwsem.writer == 0);
  assert_true(data->rwsem.reader == 0);
  assert_true(data->rwsem.holder == RWSEM_NO_HOLDER);

  printf("Test downgrade_write completed successfully !\n");
}

int main(int argc, FAR char *argv[])
{
  shared_data_t data;

  cmocka_set_message_output(CM_OUTPUT_STDOUT);

  const struct CMUnitTest tests[] =
  {
    cmocka_unit_test_prestate(test_rwsem, &data),
    cmocka_unit_test_prestate(test_downgrade_write, &data),
  };

  return cmocka_run_group_tests(tests, NULL, NULL);
}