/*
 * 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_MUTEX)
#define OS_TRACE_DISABLE_FILE_API
#endif
#include "os/mynewt.h"

os_error_t
os_mutex_init(struct os_mutex *mu)
{
    os_error_t ret;

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

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

    os_trace_api_u32(OS_TRACE_ID_MUTEX_INIT, (uintptr_t)mu);

    /* Initialize to 0 */
    mu->mu_prio = 0;
    mu->mu_level = 0;
    mu->mu_owner = NULL;
    SLIST_FIRST(&mu->mu_head) = NULL;

    ret = OS_OK;

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

os_error_t
os_mutex_release(struct os_mutex *mu)
{
    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_MUTEX_RELEASE, (uintptr_t)mu);

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

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

    /* We better own this mutex! */
    current = os_sched_get_current_task();
    if ((mu->mu_level == 0) || (mu->mu_owner != current)) {
        ret = OS_BAD_MUTEX;
        goto done;
    }

    /* Don't release if nested, just decrement nesting level */
    if (mu->mu_level != 1) {
        --mu->mu_level;
        ret = OS_OK;
        goto done;
    }

    OS_ENTER_CRITICAL(sr);

    /* Decrement nesting level (this effectively sets nesting level to 0) */
    --mu->mu_level;

    /* Restore owner task's priority; resort list if different  */
    if (current->t_prio != mu->mu_prio) {
        current->t_prio = mu->mu_prio;
        os_sched_resort(current);
    }

    /* Check if tasks are waiting for the mutex */
    rdy = SLIST_FIRST(&mu->mu_head);
    if (rdy) {
        /* There is one waiting. Wake it up */
        assert(rdy->t_obj);
        os_sched_wakeup(rdy);

        /* Set mutex internals */
        mu->mu_level = 1;
        mu->mu_prio = rdy->t_prio;
    }

    /* Set new owner of mutex (or NULL if not owned) */
    mu->mu_owner = rdy;
    if (rdy) {
        rdy->t_lockcnt++;
    }
    --current->t_lockcnt;

    /* Do we need to re-schedule? */
    resched = 0;
    rdy = os_sched_next_task();
    if (rdy != current) {
        resched = 1;
    }
    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_MUTEX_RELEASE, (uint32_t)ret);
    return ret;
}

os_error_t
os_mutex_pend(struct os_mutex *mu, os_time_t timeout)
{
    os_sr_t sr;
    os_error_t ret;
    struct os_task *current;
    struct os_task *entry;
    struct os_task *last;

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

    os_trace_api_u32x2(OS_TRACE_ID_MUTEX_PEND, (uintptr_t)mu, (uint32_t)timeout);

    /* OS must be started when calling this function */
    if (!g_os_started) {
        ret = OS_NOT_STARTED;
        goto done;
    }

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

    OS_ENTER_CRITICAL(sr);

    /* Is this owned? */
    current = os_sched_get_current_task();
    if (mu->mu_level == 0) {
        mu->mu_owner = current;
        mu->mu_prio  = current->t_prio;
        current->t_lockcnt++;
        mu->mu_level = 1;
        OS_EXIT_CRITICAL(sr);
        ret = OS_OK;
        goto done;
    }

    /* Are we owner? */
    if (mu->mu_owner == current) {
        ++mu->mu_level;
        OS_EXIT_CRITICAL(sr);
        ret = OS_OK;
        goto done;
    }

    /* Mutex is not owned by us. If timeout is 0, return immediately */
    if (timeout == 0) {
        OS_EXIT_CRITICAL(sr);
        ret = OS_TIMEOUT;
        goto done;
    }

    /* Change priority of owner if needed */
    if (mu->mu_owner->t_prio > current->t_prio) {
        mu->mu_owner->t_prio = current->t_prio;
        os_sched_resort(mu->mu_owner);
    }

    /* Link current task to tasks waiting for mutex */
    last = NULL;
    if (!SLIST_EMPTY(&mu->mu_head)) {
        /* Insert in priority order */
        SLIST_FOREACH(entry, &mu->mu_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(&mu->mu_head, current, t_obj_list);
    }

    /* Set mutex pointer in task */
    current->t_obj = mu;
    current->t_flags |= OS_TASK_FLAG_MUTEX_WAIT;
    os_sched_sleep(current, timeout);
    OS_EXIT_CRITICAL(sr);

    os_sched(NULL);

    OS_ENTER_CRITICAL(sr);
    current->t_flags &= ~OS_TASK_FLAG_MUTEX_WAIT;
    OS_EXIT_CRITICAL(sr);

    /* If we are owner we did not time out. */
    if (mu->mu_owner == current) {
        ret = OS_OK;
    } else {
        ret = OS_TIMEOUT;
    }

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

