/*
 * 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.
 */

#include <assert.h>
#include "syscfg/syscfg.h"
#if !MYNEWT_VAL(OS_SYSVIEW_TRACE_SEM)
#define OS_TRACE_DISABLE_FILE_API
#endif
#include "os/mynewt.h"

/* XXX:
 * 1) Should I check to see if we are within an ISR for some of these?
 * 2) Would I do anything different for os_sem_release() if we were in an
 *    ISR when this was called?
 */

os_error_t
os_sem_init(struct os_sem *sem, uint16_t tokens)
{
    os_error_t ret;

    if (!MYNEWT_VAL(OS_SCHEDULING)) {
        return OS_OK;
    }

    os_trace_api_u32x2(OS_TRACE_ID_SEM_INIT, (uint32_t)(uintptr_t)sem, (uint32_t)tokens);

    if (!sem) {
        ret = OS_INVALID_PARM;
        goto done;
    }

    sem->sem_tokens = tokens;
    SLIST_FIRST(&sem->sem_head) = NULL;

    ret = OS_OK;

done:
    os_trace_api_ret_u32(OS_TRACE_ID_SEM_INIT, (uint32_t)ret);
    return ret;
}

os_error_t
os_sem_release(struct os_sem *sem)
{
    int resched;
    os_sr_t sr;
    struct os_task *current;
    struct os_task *rdy;
    os_error_t ret;

    if (!MYNEWT_VAL(OS_SCHEDULING)) {
        return OS_NOT_STARTED;
    }

    os_trace_api_u32(OS_TRACE_ID_SEM_RELEASE, (uint32_t)(uintptr_t)sem);

    /* OS must be started to release semaphores */
    if (!g_os_started) {
        ret = OS_NOT_STARTED;
        goto done;
    }

    /* Check for valid semaphore */
    if (!sem) {
        ret = OS_INVALID_PARM;
        goto done;
    }

    /* Get current task */
    resched = 0;
    current = os_sched_get_current_task();

    OS_ENTER_CRITICAL(sr);

    /* Check if tasks are waiting for the semaphore */
    rdy = SLIST_FIRST(&sem->sem_head);
    if (rdy) {
        /* Clear flag that we are waiting on the semaphore; wake up task */
        rdy->t_flags &= ~OS_TASK_FLAG_SEM_WAIT;
        os_sched_wakeup(rdy);

        /*
         * Schedule if waiting task higher priority.
         * current->t_prio == rdy->t_prio means that current task
         * was already put in semaphore waiting list and context
         * switch may already started, in that case start next
         * context switch. In worst case scenario second task
         * switch interrupt will check that there is no need
         * for switching.
         */
        if (current->t_prio >= rdy->t_prio) {
            resched = 1;
        }
    } else {
        /* Add to the number of tokens */
        sem->sem_tokens++;
    }

    OS_EXIT_CRITICAL(sr);

    /* Re-schedule if needed */
    if (resched) {
        os_sched(rdy);
    }

    ret = OS_OK;

done:
    os_trace_api_ret_u32(OS_TRACE_ID_SEM_RELEASE, (uint32_t)ret);
    return ret;
}

os_error_t
os_sem_pend(struct os_sem *sem, os_time_t timeout)
{
    os_sr_t sr;
    int sched;
    struct os_task *current;
    struct os_task *entry;
    struct os_task *last;
    os_error_t ret;

    if (!MYNEWT_VAL(OS_SCHEDULING)) {
        return OS_NOT_STARTED;
    }

    os_trace_api_u32x2(OS_TRACE_ID_SEM_PEND, (uint32_t)(uintptr_t)sem, (uint32_t)timeout);

    /* Check if OS is started */
    if (!g_os_started) {
        ret = OS_NOT_STARTED;
        goto done;
    }

    /* Check for valid semaphore */
    if (!sem) {
        ret = OS_INVALID_PARM;
        goto done;
    }

    /* Assume we dont have to put task to sleep; get current task */
    sched = 0;
    current = os_sched_get_current_task();

    OS_ENTER_CRITICAL(sr);

    /*
     * If there is a token available, take it. If no token, either return
     * with error if timeout was 0 or put this task to sleep.
     */
    if (sem->sem_tokens != 0) {
        sem->sem_tokens--;
        ret = OS_OK;
    } else if (timeout == 0) {
        ret = OS_TIMEOUT;
    } else {
        /* Silence gcc maybe-uninitialized warning. */
        ret = OS_OK;

        /* Link current task to tasks waiting for semaphore */
        current->t_obj = sem;
        current->t_flags |= OS_TASK_FLAG_SEM_WAIT;
        last = NULL;
        if (!SLIST_EMPTY(&sem->sem_head)) {
            /* Insert in priority order */
            SLIST_FOREACH(entry, &sem->sem_head, t_obj_list) {
                if (current->t_prio < entry->t_prio) {
                    break;
                }
                last = entry;
            }
        }

        if (last) {
            SLIST_INSERT_AFTER(last, current, t_obj_list);
        } else {
            SLIST_INSERT_HEAD(&sem->sem_head, current, t_obj_list);
        }

        /* We will put this task to sleep */
        sched = 1;
        os_sched_sleep(current, timeout);
    }

    OS_EXIT_CRITICAL(sr);

    if (sched) {
        os_sched(NULL);
        /* Check if we timed out or got the semaphore */
        if (current->t_flags & OS_TASK_FLAG_SEM_WAIT) {
            OS_ENTER_CRITICAL(sr);
            current->t_flags &= ~OS_TASK_FLAG_SEM_WAIT;
            OS_EXIT_CRITICAL(sr);
            ret = OS_TIMEOUT;
        } else {
            ret = OS_OK;
        }
    }

done:
    os_trace_api_ret_u32(OS_TRACE_ID_SEM_PEND, (uint32_t)ret);
    return ret;
}
