/*
 * 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 <stdarg.h>
#include "os/mynewt.h"
#include "rwlock/rwlock.h"
#include "log/log.h"
#include "modlog/modlog.h"

/* Only enable modlog if logging is also enabled. */
#if MYNEWT_VAL(LOG_FULL)

struct modlog_mapping {
    SLIST_ENTRY(modlog_mapping) next;
    struct modlog_desc desc;
};

struct os_mempool modlog_mapping_pool;
static os_membuf_t modlog_mapping_buf[
    OS_MEMPOOL_SIZE(MYNEWT_VAL(MODLOG_MAX_MAPPINGS),
                    sizeof (struct modlog_mapping))
];

static struct rwlock modlog_rwl;

SLIST_HEAD(modlog_list, modlog_mapping);

/** List of configured mappings; sorted by module. */
static struct modlog_list modlog_mappings =
    SLIST_HEAD_INITIALIZER(&modlog_mappings);

/**
 * Points to the first default mapping in the list.  Since 255 is the default
 * module, the default mappings are guaranteed to come last.
 */
static struct modlog_mapping *modlog_first_dflt;

static struct modlog_mapping *
modlog_alloc(void)
{
    struct modlog_mapping *mm;

    mm = os_memblock_get(&modlog_mapping_pool);
    if (mm != NULL) {
        *mm = (struct modlog_mapping) { 0 };
    }

    return mm;
}

static void
modlog_free(struct modlog_mapping *mm)
{
    os_memblock_put(&modlog_mapping_pool, mm);
}

static uint8_t
modlog_infer_handle(const struct modlog_mapping *mm)
{
    uintptr_t off;
    size_t elem_sz;
    int idx;

    elem_sz = sizeof modlog_mapping_buf / MYNEWT_VAL(MODLOG_MAX_MAPPINGS);

    off = (uintptr_t)mm - (uintptr_t)modlog_mapping_buf;
    idx = off / elem_sz;

    assert(idx >= 0 && idx < MYNEWT_VAL(MODLOG_MAX_MAPPINGS));
    assert(off % elem_sz == 0);

    return idx;
}

static struct modlog_mapping *
modlog_find(uint8_t handle, struct modlog_mapping **out_prev)
{
    struct modlog_mapping *prev;
    struct modlog_mapping *cur;

    prev = NULL;
    SLIST_FOREACH(cur, &modlog_mappings, next) {
        if (cur->desc.handle == handle) {
            break;
        }

        prev = cur;
    }

    if (out_prev != NULL) {
        *out_prev = prev;
    }

    return cur;
}

static struct modlog_mapping *
modlog_find_by_module(uint8_t module, struct modlog_mapping **out_prev)
{
    struct modlog_mapping *prev;
    struct modlog_mapping *cur;

    prev = NULL;
    SLIST_FOREACH(cur, &modlog_mappings, next) {
        if (cur->desc.module == module) {
            break;
        }

        if (cur->desc.module > module) {
            cur = NULL;
            break;
        }

        prev = cur;
    }

    if (out_prev != NULL) {
        *out_prev = prev;
    }

    return cur;
}

static void
modlog_insert(struct modlog_mapping *mm)
{
    struct modlog_mapping *prev;

    modlog_find_by_module(mm->desc.module, &prev);
    if (prev == NULL) {
        SLIST_INSERT_HEAD(&modlog_mappings, mm, next);
    } else {
        SLIST_INSERT_AFTER(prev, mm, next);
    }

    if (mm->desc.module == MODLOG_MODULE_DFLT) {
        modlog_first_dflt = mm;
    }
}

static void
modlog_remove(struct modlog_mapping *mm, struct modlog_mapping *prev)
{
    if (mm == modlog_first_dflt) {
        modlog_first_dflt = SLIST_NEXT(mm, next);
    }
        
    if (prev == NULL) {
        SLIST_REMOVE_HEAD(&modlog_mappings, next);
    } else {
        SLIST_NEXT(prev, next) = SLIST_NEXT(mm, next);
    }
}

static int
modlog_register_no_lock(uint8_t module, struct log *log, uint8_t min_level,
                        uint8_t *out_handle)
{
    struct modlog_mapping *mm;

    if (log == NULL) {
        return SYS_EINVAL;
    }

    mm = modlog_alloc();
    if (mm == NULL) {
        return SYS_ENOMEM;
    }

    mm->desc = (struct modlog_desc) {
        .log = log,
        .handle = modlog_infer_handle(mm),
        .module = module,
        .min_level = min_level,
    };

    modlog_insert(mm);

    if (out_handle != NULL) {
        *out_handle = mm->desc.handle;
    }

    return 0;
}

static int
modlog_delete_no_lock(uint8_t handle)
{
    struct modlog_mapping *prev;
    struct modlog_mapping *mm;

    mm = modlog_find(handle, &prev);
    if (mm == NULL) {
        return SYS_ENOENT;
    }

    modlog_remove(mm, prev);
    modlog_free(mm);

    return 0;
}

static int
modlog_append_one(struct modlog_mapping *mm, uint8_t module, uint8_t level,
                  uint8_t etype, void *data, uint16_t len)
{
    int rc;

    if (level >= mm->desc.min_level) {
        rc = log_append_body(mm->desc.log, module, level, etype, data, len);
        if (rc != 0) {
            return SYS_EIO;
        }
    } else {
        LOG_STATS_INC(mm->desc.log, writes);
        LOG_STATS_INC(mm->desc.log, drops);
    }

    return 0;
}

static int
modlog_append_no_lock(uint8_t module, uint8_t level, uint8_t etype,
                      void *data, uint16_t len)
{
    struct modlog_mapping *mm;
    int rc;

    if (module == MODLOG_MODULE_DFLT) {
        return SYS_EINVAL;
    }

    mm = modlog_find_by_module(module, NULL);
    if (mm != NULL) {
        while (mm != NULL && mm->desc.module == module) {
            rc = modlog_append_one(mm, module, level, etype, data, len);
            if (rc != 0) {
                return rc;
            }

            mm = SLIST_NEXT(mm, next);
        }
        return 0;
    }

    /* No mappings match the specified module; write to the default set. */
    for (mm = modlog_first_dflt;
         mm != NULL;
         mm = SLIST_NEXT(mm, next)) {

        rc = modlog_append_one(mm, module, level, etype, data, len);
        if (rc != 0) {
            return rc;
        }
    }

    return 0;
}

static int
modlog_append_mbuf_one(struct modlog_mapping *mm, uint8_t module,
                       uint8_t level, uint8_t etype, struct os_mbuf *om)
{
    int rc;

    if (level >= mm->desc.min_level) {
        rc = log_append_mbuf_body_no_free(mm->desc.log, module,
                                          level, etype, om);
        if (rc != 0) {
            return SYS_EIO;
        }
    } else {
        LOG_STATS_INC(mm->desc.log, writes);
        LOG_STATS_INC(mm->desc.log, drops);
    }

    return 0;
}

static int
modlog_append_mbuf_no_lock(uint8_t module, uint8_t level, uint8_t etype,
                           struct os_mbuf *om)
{
    struct modlog_mapping *mm;
    bool found;
    int rc;

    rc = 0;

    found = false;
    SLIST_FOREACH(mm, &modlog_mappings, next) {
        if (mm->desc.module == module) {
            found = true;

            rc = modlog_append_mbuf_one(mm, module, level, etype, om);
            if (rc != 0) {
                break;
            }
        } else if (mm->desc.module > module) {
            break;
        }
    }

    /* If no mappings match the specified module, write to the default set. */
    if (!found) {
        for (mm = modlog_first_dflt;
             mm != NULL;
             mm = SLIST_NEXT(mm, next)) {

            rc = modlog_append_mbuf_one(mm, module, level, etype, om);
            if (rc != 0) {
                break;
            }
        }
    }

    os_mbuf_free_chain(om);

    return rc;
}

static int
modlog_foreach_no_lock(modlog_foreach_fn *fn, void *arg)
{
    struct modlog_mapping *next;
    struct modlog_mapping *cur;
    int rc;

    cur = SLIST_FIRST(&modlog_mappings);
    while (cur != NULL) {
        next = SLIST_NEXT(cur, next);

        rc = fn(&cur->desc, arg);
        if (rc != 0) {
            return rc;
        }

        cur = next;
    }

    return 0;
}

int
modlog_get(uint8_t handle, struct modlog_desc *out_desc)
{
    struct modlog_mapping *mm;
    int rc;

    rwlock_acquire_read(&modlog_rwl);

    mm = modlog_find(handle, NULL);
    if (mm == NULL) {
        rc = SYS_ENOENT;
    } else {
        if (out_desc != NULL) {
            *out_desc = mm->desc;
        }
        rc = 0;
    }

    rwlock_release_read(&modlog_rwl);

    return rc;
}

int
modlog_register(uint8_t module, struct log *log, uint8_t min_level,
                uint8_t *out_handle)
{
    int rc;

    rwlock_acquire_write(&modlog_rwl);
    rc = modlog_register_no_lock(module, log, min_level, out_handle);
    rwlock_release_write(&modlog_rwl);

    return rc;
}

int
modlog_delete(uint8_t handle)
{
    int rc;

    rwlock_acquire_write(&modlog_rwl);
    rc = modlog_delete_no_lock(handle);
    rwlock_release_write(&modlog_rwl);

    return rc;
}

void
modlog_clear(void)
{
    struct modlog_mapping *mm;

    rwlock_acquire_write(&modlog_rwl);

    while ((mm = SLIST_FIRST(&modlog_mappings)) != NULL) {
        modlog_remove(mm, NULL);
        modlog_free(mm);
    }

    rwlock_release_write(&modlog_rwl);
}

int
modlog_append(uint8_t module, uint8_t level, uint8_t etype,
              void *data, uint16_t len)
{
    int rc;

    rwlock_acquire_read(&modlog_rwl);
    rc = modlog_append_no_lock(module, level, etype, data, len);
    rwlock_release_read(&modlog_rwl);

    return rc;
}

int
modlog_append_mbuf(uint8_t module, uint8_t level, uint8_t etype,
                   struct os_mbuf *om)
{
    int rc;

    rwlock_acquire_read(&modlog_rwl);
    rc = modlog_append_mbuf_no_lock(module, level, etype, om);
    rwlock_release_read(&modlog_rwl);

    return rc;
}

int
modlog_foreach(modlog_foreach_fn *fn, void *arg)
{
    int rc;

    rwlock_acquire_read(&modlog_rwl);
    rc = modlog_foreach_no_lock(fn, arg);
    rwlock_release_read(&modlog_rwl);

    return rc;
}

void
modlog_printf(uint8_t module, uint8_t level, const char *msg, ...)
{
    va_list args;
    char buf[MYNEWT_VAL(MODLOG_MAX_PRINTF_LEN)];
    int len;

    va_start(args, msg);
    len = vsnprintf(buf, MYNEWT_VAL(MODLOG_MAX_PRINTF_LEN), msg, args);
    va_end(args);

    if (len >= MYNEWT_VAL(MODLOG_MAX_PRINTF_LEN)) {
        len = MYNEWT_VAL(MODLOG_MAX_PRINTF_LEN) - 1;
    }

    modlog_append(module, level, LOG_ETYPE_STRING, buf, len);
}

void
modlog_hexdump(uint8_t module, uint8_t level,
               const void *data_ptr, uint16_t len, uint16_t line_break)
{
    int i;
    const uint8_t *data;

    assert(len > 0);

    data = data_ptr;

    for (i = 0; i < len; i++) {
        if ((i != 0) && (line_break != 0) && (i % line_break == 0)) {
            modlog_printf(module, level, "\n");
        }

        modlog_printf(module, level, "0x%02x ", data[i]);
    }

    modlog_printf(module, level, "\n");
}

void
modlog_init(void)
{
    int rc;

    SYSINIT_ASSERT_ACTIVE();

    rc = os_mempool_init(&modlog_mapping_pool, MYNEWT_VAL(MODLOG_MAX_MAPPINGS),
                         sizeof (struct modlog_mapping), modlog_mapping_buf,
                         "modlog_mapping_pool");
    SYSINIT_PANIC_ASSERT(rc == 0);

    SLIST_INIT(&modlog_mappings);
    modlog_first_dflt = NULL;

    rc = rwlock_init(&modlog_rwl);
    SYSINIT_PANIC_ASSERT(rc == 0);

    /* Register the default console mapping if configured. */
#if MYNEWT_VAL(MODLOG_CONSOLE_DFLT)
    rc = modlog_register(MODLOG_MODULE_DFLT, log_console_get(),
                         LOG_LEVEL_DEBUG, NULL);
    SYSINIT_PANIC_ASSERT(rc == 0);
#endif
}

#else /* LOG_FULL */

void
modlog_init(void)
{ }

#endif /* LOG_FULL */
