/*
 * 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 <string.h>
#include "defs/error.h"
#include "bus/bus.h"
#include "bus/bus_debug.h"
#include "bus/bus_driver.h"
#if MYNEWT_VAL(BUS_STATS)
#include "stats/stats.h"
#endif

static os_time_t g_bus_node_lock_timeout;

#if MYNEWT_VAL(BUS_STATS)
STATS_NAME_START(bus_stats_section)
    STATS_NAME(bus_stats_section, lock_timeouts)
    STATS_NAME(bus_stats_section, read_ops)
    STATS_NAME(bus_stats_section, read_errors)
    STATS_NAME(bus_stats_section, write_ops)
    STATS_NAME(bus_stats_section, write_errors)
STATS_NAME_END(bus_stats_section)

#if MYNEWT_VAL(BUS_STATS_PER_NODE)
#define BUS_STATS_INC(_bdev, _bnode, _var)  \
    do {                                    \
        STATS_INC((_bdev)->stats, _var);    \
        STATS_INC((_bnode)->stats, _var);   \
    } while (0)
#else
#define BUS_STATS_INC(_bdev, _bnode, _var)  \
    do {                                    \
        STATS_INC((_bdev)->stats, _var);    \
    } while (0)
#endif
#else
#define BUS_STATS_INC(_bdev, _bnode, _var)  \
    do {                                    \
    } while (0)
#endif

static inline void
bus_dev_enable(struct bus_dev *bdev)
{
    if (bdev->enabled) {
        return;
    }

    if (bdev->dops->enable) {
        bdev->dops->enable(bdev);
    }

    bdev->enabled = true;
}

static inline void
bus_dev_disable(struct bus_dev *bdev)
{
    if (!bdev->enabled) {
        return;
    }

    if (bdev->dops->disable) {
        bdev->dops->disable(bdev);
    }

    bdev->enabled = false;
}

static int
bus_dev_suspend_func(struct os_dev *odev, os_time_t suspend_at, int force)
{
    struct bus_dev *bdev = (struct bus_dev *)odev;
    int rc;

#if MYNEWT_VAL(BUS_PM)
    if (bdev->pm_mode != BUS_PM_MODE_MANUAL) {
        return OS_EINVAL;
    }
#endif

    /* To make things simple we just allow to suspend "now" */
    if (OS_TIME_TICK_GT(suspend_at, os_time_get())) {
        return OS_EINVAL;
    }

    rc = os_mutex_pend(&bdev->lock, OS_TIMEOUT_NEVER);
    if (rc) {
        return rc;
    }

    bus_dev_disable(bdev);

    os_mutex_release(&bdev->lock);

    return OS_OK;
}

static int
bus_dev_resume_func(struct os_dev *odev)
{
    struct bus_dev *bdev = (struct bus_dev *)odev;
    int rc;

#if MYNEWT_VAL(BUS_PM)
    if (bdev->pm_mode != BUS_PM_MODE_MANUAL) {
        return OS_EINVAL;
    }
#endif

    rc = os_mutex_pend(&bdev->lock, OS_TIMEOUT_NEVER);
    if (rc) {
        return rc;
    }

    bus_dev_enable(bdev);

    os_mutex_release(&bdev->lock);

    return OS_OK;
}

#if MYNEWT_VAL(BUS_PM)
static void
bus_dev_inactivity_tmo_func(struct os_event *ev)
{
    struct bus_dev *bdev = (struct bus_dev *)ev->ev_arg;
    int rc;

    rc = os_mutex_pend(&bdev->lock, OS_TIMEOUT_NEVER);
    if (rc) {
        return;
    }

    /* Just in case PM was changed while timer was running */
    if (bdev->pm_mode == BUS_PM_MODE_AUTO) {
        bus_dev_disable(bdev);
    }

    os_mutex_release(&bdev->lock);
}
#endif

static int
bus_node_open_func(struct os_dev *odev, uint32_t wait, void *arg)
{
    struct bus_node *bnode = (struct bus_node *)odev;

    BUS_DEBUG_VERIFY_NODE(bnode);

    if (!bnode->callbacks.open) {
        return 0;
    }

    /*
     * XXX current os_dev implementation is prone to races since reference
     * counting is done without any locking, we'll need to fix it there
     */

    /* Call open callback if opening first ref */
    if (odev->od_open_ref == 0) {
        bnode->callbacks.open(bnode);
    }

    return 0;
}

static int
bus_node_close_func(struct os_dev *odev)
{
    struct bus_node *bnode = (struct bus_node *)odev;

    BUS_DEBUG_VERIFY_NODE(bnode);

    if (!bnode->callbacks.close) {
        return 0;
    }

    /*
     * XXX current os_dev implementation is prone to races since reference
     * counting is done without any locking, we'll need to fix it there
     */

    /* Call close callback if closing last ref */
    if (odev->od_open_ref == 1) {
        bnode->callbacks.close(bnode);
    }

    return 0;
}

void
bus_node_set_callbacks(struct os_dev *node, struct bus_node_callbacks *cbs)
{
    struct bus_node *bnode = (struct bus_node *)node;

    /* This should be done only once so all callbacks should be NULL here */
    assert(bnode->callbacks.init == NULL);
    assert(bnode->callbacks.open == NULL);
    assert(bnode->callbacks.close == NULL);

    bnode->callbacks = *cbs;
}

int
bus_dev_init_func(struct os_dev *odev, void *arg)
{
    struct bus_dev *bdev = (struct bus_dev *)odev;
    struct bus_dev_ops *ops = arg;
#if MYNEWT_VAL(BUS_STATS)
    char *stats_name;
    int rc;
#endif

    BUS_DEBUG_POISON_DEV(bdev);

    bdev->dops = ops;
    bdev->configured_for = NULL;

    os_mutex_init(&bdev->lock);
#if MYNEWT_VAL(BUS_PM)
    /* XXX allow custom eventq */
    os_callout_init(&bdev->inactivity_tmo, os_eventq_dflt_get(),
                    bus_dev_inactivity_tmo_func, odev);
    bdev->pm_mode = MYNEWT_VAL_CHOICE(BUS_PM_MODE, AUTO) ? BUS_PM_MODE_AUTO : BUS_PM_MODE_MANUAL;
    if (MYNEWT_VAL_CHOICE(BUS_PM_MODE, AUTO)) {
        bdev->pm_opts.pm_mode_auto.disable_tmo = MYNEWT_VAL(BUS_PM_INACTIVITY_TMO);
    }
#endif

#if MYNEWT_VAL(BUS_STATS)
    asprintf(&stats_name, "bd_%s", odev->od_name);
    rc = stats_init_and_reg(STATS_HDR(bdev->stats),
                            STATS_SIZE_INIT_PARMS(bdev->stats, STATS_SIZE_32),
                            STATS_NAME_INIT_PARMS(bus_stats_section),
                            stats_name);
    assert(rc == 0);
#endif

    odev->od_handlers.od_suspend = bus_dev_suspend_func;
    odev->od_handlers.od_resume = bus_dev_resume_func;

    if (!MYNEWT_VAL(BUS_PM) || MYNEWT_VAL_CHOICE(BUS_PM_MODE, MANUAL)) {
        bus_dev_enable(bdev);
    }

    return 0;
}

int
bus_node_init_func(struct os_dev *odev, void *arg)
{
    struct bus_node *bnode = (struct bus_node *)odev;
    struct bus_node_cfg *node_cfg = arg;
    struct os_dev *parent_odev;
    struct bus_dev *bdev;
    void *init_arg;
#if MYNEWT_VAL(BUS_STATS_PER_NODE)
    char *stats_name;
#endif
    int rc;

    parent_odev = os_dev_lookup(node_cfg->bus_name);
    if (!parent_odev) {
        return OS_EINVAL;
    }

    BUS_DEBUG_POISON_NODE(bnode);

    /* We need to save init_arg here since it will be overwritten by parent_bus */
    init_arg = bnode->init_arg;
    bnode->parent_bus = (struct bus_dev *)parent_odev;

    bdev = (struct bus_dev *)parent_odev;
    rc = bdev->dops->init_node(bdev, bnode, arg);
    if (rc) {
        return rc;
    }

    if (node_cfg->lock_timeout_ms) {
        bnode->lock_timeout = os_time_ms_to_ticks32(node_cfg->lock_timeout_ms);
    } else {
        /* Use default */
        bnode->lock_timeout = 0;
    }

    odev->od_handlers.od_open = bus_node_open_func;
    odev->od_handlers.od_close = bus_node_close_func;

#if MYNEWT_VAL(BUS_STATS_PER_NODE)
    asprintf(&stats_name, "bn_%s", odev->od_name);
    rc = stats_init_and_reg(STATS_HDR(bnode->stats),
                            STATS_SIZE_INIT_PARMS(bnode->stats, STATS_SIZE_32),
                            STATS_NAME_INIT_PARMS(bus_stats_section),
                            stats_name);
    assert(rc == 0);
#endif

    if (bnode->callbacks.init) {
        bnode->callbacks.init(bnode, init_arg);
    }

    return 0;
}

int
bus_node_read(struct os_dev *node, void *buf, uint16_t length,
              os_time_t timeout, uint16_t flags)
{
    struct bus_node *bnode = (struct bus_node *)node;
    struct bus_dev *bdev = bnode->parent_bus;
    int rc;

    BUS_DEBUG_VERIFY_DEV(bdev);
    BUS_DEBUG_VERIFY_NODE(bnode);

    if (!bdev->dops->read) {
        return SYS_ENOTSUP;
    }

    rc = bus_node_lock(node, bus_node_get_lock_timeout(node));
    if (rc) {
        return rc;
    }

    if (!bdev->enabled) {
        rc = SYS_EIO;
        goto done;
    }

    BUS_STATS_INC(bdev, bnode, read_ops);
    rc = bdev->dops->read(bdev, bnode, buf, length, timeout, flags);
    if (rc) {
        BUS_STATS_INC(bdev, bnode, read_errors);
    }

done:
    (void)bus_node_unlock(node);

    return rc;
}

int
bus_node_write(struct os_dev *node, const void *buf, uint16_t length,
               os_time_t timeout, uint16_t flags)
{
    struct bus_node *bnode = (struct bus_node *)node;
    struct bus_dev *bdev = bnode->parent_bus;
    int rc;

    BUS_DEBUG_VERIFY_DEV(bdev);
    BUS_DEBUG_VERIFY_NODE(bnode);

    if (!bdev->dops->write) {
        return SYS_ENOTSUP;
    }

    rc = bus_node_lock(node, bus_node_get_lock_timeout(node));
    if (rc) {
        return rc;
    }

    if (!bdev->enabled) {
        rc = SYS_EIO;
        goto done;
    }

    BUS_STATS_INC(bdev, bnode, write_ops);
    rc = bdev->dops->write(bdev, bnode, buf, length, timeout, flags);
    if (rc) {
        BUS_STATS_INC(bdev, bnode, write_errors);
    }

done:
    (void)bus_node_unlock(node);

    return rc;
}

int
bus_node_write_read_transact(struct os_dev *node, const void *wbuf,
                             uint16_t wlength, void *rbuf, uint16_t rlength,
                             os_time_t timeout, uint16_t flags)
{
    struct bus_node *bnode = (struct bus_node *)node;
    struct bus_dev *bdev = bnode->parent_bus;
    int rc;

    BUS_DEBUG_VERIFY_DEV(bdev);
    BUS_DEBUG_VERIFY_NODE(bnode);

    if (!bdev->dops->write || !bdev->dops->read) {
        return SYS_ENOTSUP;
    }

    rc = bus_node_lock(node, bus_node_get_lock_timeout(node));
    if (rc) {
        return rc;
    }

    if (!bdev->enabled) {
        rc = SYS_EIO;
        goto done;
    }

    if (bdev->dops->write_read) {
        BUS_STATS_INC(bdev, bnode, write_ops);
        BUS_STATS_INC(bdev, bnode, read_ops);
        rc = bdev->dops->write_read(bdev, bnode, wbuf, wlength, rbuf, rlength, timeout, flags);
        if (rc) {
            BUS_STATS_INC(bdev, bnode, write_errors);
            BUS_STATS_INC(bdev, bnode, read_errors);
        }
    } else {
        /*
         * XXX we probably should pass flags here but with some of them stripped,
         * e.g. BUS_F_NOSTOP should not be present here, but since we do not have
         * too many flags now (like we literally have only one flag) let's just pass
         * no flags for now
         */
        BUS_STATS_INC(bdev, bnode, write_ops);
        rc = bdev->dops->write(bdev, bnode, wbuf, wlength, timeout, BUS_F_NOSTOP);
        if (rc) {
            BUS_STATS_INC(bdev, bnode, write_errors);
            goto done;
        }

        BUS_STATS_INC(bdev, bnode, read_ops);
        rc = bdev->dops->read(bdev, bnode, rbuf, rlength, timeout, flags);
        if (rc) {
            BUS_STATS_INC(bdev, bnode, read_errors);
            goto done;
        }
    }

done:
    (void)bus_node_unlock(node);

    return rc;
}

int
bus_node_duplex_write_read(struct os_dev *node, const void *wbuf,
                           void *rbuf, uint16_t length,
                           os_time_t timeout, uint16_t flags)
{
    struct bus_node *bnode = (struct bus_node *)node;
    struct bus_dev *bdev = bnode->parent_bus;
    int rc;

    BUS_DEBUG_VERIFY_DEV(bdev);
    BUS_DEBUG_VERIFY_NODE(bnode);

    if (!bdev->dops->duplex_write_read) {
        return SYS_ENOTSUP;
    }

    rc = bus_node_lock(node, bus_node_get_lock_timeout(node));
    if (rc) {
        return rc;
    }

    if (!bdev->enabled) {
        rc = SYS_EIO;
        goto done;
    }

    BUS_STATS_INC(bdev, bnode, write_ops);
    BUS_STATS_INC(bdev, bnode, read_ops);
    rc = bdev->dops->duplex_write_read(bdev, bnode, wbuf, rbuf, length, timeout, flags);
    if (rc) {
        BUS_STATS_INC(bdev, bnode, write_errors);
        BUS_STATS_INC(bdev, bnode, read_errors);
    }

done:
    (void)bus_node_unlock(node);

    return rc;
}

int
bus_node_lock(struct os_dev *node, os_time_t timeout)
{
    struct bus_node *bnode = (struct bus_node *)node;
    struct bus_dev *bdev = bnode->parent_bus;
    os_error_t err;
    int rc;

    BUS_DEBUG_VERIFY_DEV(bdev);
    BUS_DEBUG_VERIFY_NODE(bnode);

    if (timeout == BUS_NODE_LOCK_DEFAULT_TIMEOUT) {
        timeout = g_bus_node_lock_timeout;
    }

    if (MYNEWT_VAL(OS_SCHEDULING)) {
        err = os_mutex_pend(&bdev->lock, timeout);
        if (err == OS_TIMEOUT) {
            BUS_STATS_INC(bdev, bnode, lock_timeouts);
            return SYS_ETIMEOUT;
        }

        assert(err == OS_OK || err == OS_NOT_STARTED);
    }

#if MYNEWT_VAL(BUS_PM)
    /* In auto PM we need to enable bus device on first lock */
    if ((bdev->pm_mode == BUS_PM_MODE_AUTO) &&
        (os_mutex_get_level(&bdev->lock) == 1)) {
        os_callout_stop(&bdev->inactivity_tmo);
        bus_dev_enable(bdev);
    }
#endif

    /*
     * Configuration is done on 1st lock. For nested lock make sure that
     * bnode did not change.
     */
    if (MYNEWT_VAL(OS_SCHEDULING) && os_mutex_get_level(&bdev->lock) != 1) {
        if (bdev->configured_for == bnode) {
            return 0;
        }
        (void)bus_node_unlock(node);
        return SYS_EACCES;
    }

    rc = bdev->dops->configure(bdev, bnode);
    if (rc) {
        bdev->configured_for = NULL;
        (void)bus_node_unlock(node);
    } else {
        bdev->configured_for = bnode;
    }

    return rc;
}

int
bus_node_unlock(struct os_dev *node)
{
    struct bus_node *bnode = (struct bus_node *)node;
    struct bus_dev *bdev = bnode->parent_bus;
    os_error_t err;

    BUS_DEBUG_VERIFY_DEV(bdev);
    BUS_DEBUG_VERIFY_NODE(bnode);

#if MYNEWT_VAL(BUS_PM)
    /* In auto PM we should disable bus device on last unlock */
    if ((bdev->pm_mode == BUS_PM_MODE_AUTO) &&
        (os_mutex_get_level(&bdev->lock) == 1)) {
        if (bdev->pm_opts.pm_mode_auto.disable_tmo == 0) {
            bus_dev_disable(bdev);
        } else {
            os_callout_reset(&bdev->inactivity_tmo,
                             bdev->pm_opts.pm_mode_auto.disable_tmo);
        }
    }
#endif

    if (!MYNEWT_VAL(OS_SCHEDULING)) {
        return 0;
    }
    err = os_mutex_release(&bdev->lock);

    /*
     * Probably no one cares about return value from unlock, so for debugging
     * purposes let's assert on anything that is not a success. This includes
     * OS_INVALID_PARM (we basically can't pass invalid mutex here unless our
     * structs are broken) and OS_BAD_MUTEX (unlock shall be only done by the
     * same task which locked it).
     */
    assert(err == OS_OK || err == OS_NOT_STARTED);

    return 0;
}

os_time_t
bus_node_get_lock_timeout(struct os_dev *node)
{
    struct bus_node *bnode = (struct bus_node *)node;

    BUS_DEBUG_VERIFY_NODE(bnode);

    return bnode->lock_timeout ? bnode->lock_timeout : g_bus_node_lock_timeout;
}

int
bus_dev_set_pm(struct os_dev *bus, bus_pm_mode_t pm_mode,
               union bus_pm_options *pm_opts)
{
#if MYNEWT_VAL(BUS_PM)
    struct bus_dev *bdev = (struct bus_dev *)bus;
    int rc;

    BUS_DEBUG_VERIFY_DEV(bdev);

    rc = os_mutex_pend(&bdev->lock, OS_TIMEOUT_NEVER);
    if (rc) {
        return SYS_EACCES;
    }

    bdev->pm_mode = pm_mode;

    if (pm_opts) {
        memcpy(&bdev->pm_opts, pm_opts, sizeof(*pm_opts));
    } else {
        memset(&bdev->pm_opts, 0, sizeof(*pm_opts));
    }

    os_mutex_release(&bdev->lock);

    return 0;
#else
    return SYS_ENOTSUP;
#endif
}

void
bus_pkg_init(void)
{
    uint32_t lock_timeout_ms;

    lock_timeout_ms = MYNEWT_VAL(BUS_DEFAULT_LOCK_TIMEOUT_MS);

    g_bus_node_lock_timeout = os_time_ms_to_ticks32(lock_timeout_ms);
}
