/*
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <string.h>
#include <stdio.h>
#include <stdarg.h>

#include "pico.h"
#if LIB_PICO_PRINTF_PICO
#include "pico/printf.h"
#endif
#include "pico/stdio.h"
#include "pico/stdio/driver.h"
#include "pico/time.h"
#if PICO_STDOUT_MUTEX
#include "pico/mutex.h"
#endif

#if LIB_PICO_STDIO_UART
#include "pico/stdio_uart.h"
#endif

#if LIB_PICO_STDIO_USB
#include "pico/stdio_usb.h"
#endif

#if LIB_PICO_STDIO_SEMIHOSTING
#include "pico/stdio_semihosting.h"
#endif

#if LIB_PICO_STDIO_RTT
#include "pico/stdio_rtt.h"
#endif

static stdio_driver_t *drivers;
static stdio_driver_t *filter;

#if PICO_STDOUT_MUTEX
auto_init_mutex(print_mutex);

bool stdout_serialize_begin(void) {
    return mutex_try_enter_block_until(&print_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS));
}

void stdout_serialize_end(void) {
    mutex_exit(&print_mutex);
}

#else
static bool stdout_serialize_begin(void) {
    return true;
}
static void stdout_serialize_end(void) {
}
#endif
static void stdio_out_chars_no_crlf(stdio_driver_t *driver, const char *s, int len) {
    driver->out_chars(s, len);
}

static void stdio_out_chars_crlf(stdio_driver_t *driver, const char *s, int len) {
#if PICO_STDIO_ENABLE_CRLF_SUPPORT
    if (!driver->crlf_enabled) {
        driver->out_chars(s, len);
        return;
    }
    int first_of_chunk = 0;
    static const char crlf_str[] = {'\r', '\n'};
    for (int i = 0; i < len; i++) {
        bool prev_char_was_cr = i > 0 ? s[i - 1] == '\r' : driver->last_ended_with_cr;
        if (s[i] == '\n' && !prev_char_was_cr) {
            if (i > first_of_chunk) {
                driver->out_chars(&s[first_of_chunk], i - first_of_chunk);
            }
            driver->out_chars(crlf_str, 2);
            first_of_chunk = i + 1;
        }
    }
    if (first_of_chunk < len) {
        driver->out_chars(&s[first_of_chunk], len - first_of_chunk);
    }
    if (len > 0) {
        driver->last_ended_with_cr = s[len - 1] == '\r';
    }
#else
    driver->out_chars(s, len);
#endif
}

int stdio_put_string(const char *s, int len, bool newline, bool cr_translation) {
    bool serialized = stdout_serialize_begin();
    if (!serialized) {
#if PICO_STDIO_IGNORE_NESTED_STDOUT
        return 0;
#endif
    }
    if (len == -1) len = (int)strlen(s);
    void (*out_func)(stdio_driver_t *, const char *, int) = cr_translation ? stdio_out_chars_crlf : stdio_out_chars_no_crlf;
    for (stdio_driver_t *driver = drivers; driver; driver = driver->next) {
        if (!driver->out_chars) continue;
        if (filter && filter != driver) continue;
        out_func(driver, s, len);
        if (newline) {
            const char c = '\n';
            out_func(driver, &c, 1);
        }
    }
    if (serialized) {
        stdout_serialize_end();
    }
    return len;
}

int stdio_get_until(char *buf, int len, absolute_time_t until) {
    do {
        // todo round robin might be nice on each call, but then again hopefully
        //  no source will starve the others
        for (stdio_driver_t *driver = drivers; driver; driver = driver->next) {
            if (filter && filter != driver) continue;
            if (driver->in_chars) {
                int read = driver->in_chars(buf, len);
                if (read > 0) {
                    return read;
                }
            }
        }
        if (time_reached(until)) {
            return PICO_ERROR_TIMEOUT;
        }
        // we sleep here in case the in_chars methods acquire mutexes or disable IRQs and
        // potentially starve out what they are waiting on (have seen this with USB)
        busy_wait_us(1);
    } while (true);
}

int stdio_putchar_raw(int c) {
    char cc = (char)c;
    stdio_put_string(&cc, 1, false, false);
    return c;
}

int stdio_puts_raw(const char *s) {
    int len = (int)strlen(s);
    stdio_put_string(s, len, true, false);
    stdio_flush();
    return len;
}

void stdio_set_driver_enabled(stdio_driver_t *driver, bool enable) {
    stdio_driver_t **prev = &drivers;
    while (*prev) {
        if (*prev == driver) {
            if (!enable) {
                *prev = driver->next;
                driver->next = NULL;
            }
            return;
        }
        prev = &(*prev)->next;
    }
    if (enable) {
        *prev = driver;
    }
}

void stdio_flush(void) {
    for (stdio_driver_t *d = drivers; d; d = d->next) {
        if (d->out_flush) d->out_flush();
    }
}

#if LIB_PICO_PRINTF_PICO
typedef struct stdio_stack_buffer {
    int used;
    char buf[PICO_STDIO_STACK_BUFFER_SIZE];
} stdio_stack_buffer_t;

static void stdio_stack_buffer_flush(stdio_stack_buffer_t *buffer) {
    if (buffer->used) {
        for (stdio_driver_t *d = drivers; d; d = d->next) {
            if (!d->out_chars) continue;
            if (filter && filter != d) continue;
            stdio_out_chars_crlf(d, buffer->buf, buffer->used);
        }
        buffer->used = 0;
    }
}

static void stdio_buffered_printer(char c, void *arg) {
    stdio_stack_buffer_t *buffer = (stdio_stack_buffer_t *)arg;
    // Invariant: buffer is never full at this point, as:
    // * It's initially empty.
    // * It's flushed if the following write makes it full.
    // Therefore it's safe to perform this write *before* the `if (full)`:
    buffer->buf[buffer->used++] = c;
    // Hoisting the buffer write above the flush check lets the compiler omit
    // stack setup/teardown in the early-out case. Savings add up because
    // this function is called per char and cannot be inlined.
    if (buffer->used == PICO_STDIO_STACK_BUFFER_SIZE) {
        stdio_stack_buffer_flush(buffer);
    }
}
#endif

bool stdio_init_all(void) {
    // todo add explicit custom, or registered although you can call stdio_enable_driver explicitly anyway
    // These are well known ones

    bool rc = false;
#if LIB_PICO_STDIO_UART
    stdio_uart_init();
    rc = true;
#endif

#if LIB_PICO_STDIO_SEMIHOSTING
    stdio_semihosting_init();
    rc = true;
#endif

#if LIB_PICO_STDIO_RTT
    stdio_rtt_init();
    rc = true;
#endif

#if LIB_PICO_STDIO_USB
    rc |= stdio_usb_init();
#endif
    return rc;
}

bool stdio_deinit_all(void) {
    // todo add explicit custom, or registered although you can call stdio_enable_driver explicitly anyway
    // These are well known ones

    // First flush, to make sure everything is printed
    stdio_flush();

    bool rc = false;
#if LIB_PICO_STDIO_UART
    stdio_uart_deinit();
    rc = true;
#endif

#if LIB_PICO_STDIO_SEMIHOSTING
    stdio_semihosting_deinit();
    rc = true;
#endif

#if LIB_PICO_STDIO_RTT
    stdio_rtt_deinit();
    rc = true;
#endif

#if LIB_PICO_STDIO_USB
    rc = stdio_usb_deinit();
#endif
    return rc;
}

int stdio_getchar_timeout_us(uint32_t timeout_us) {
    char buf[1];
    int rc = stdio_get_until(buf, sizeof(buf), make_timeout_time_us(timeout_us));
    if (rc < 0) return rc;
    assert(rc);
    return (uint8_t)buf[0];
}

void stdio_filter_driver(stdio_driver_t *driver) {
    filter = driver;
}

void stdio_set_translate_crlf(stdio_driver_t *driver, bool enabled) {
#if PICO_STDIO_ENABLE_CRLF_SUPPORT
    if (enabled && !driver->crlf_enabled) {
        driver->last_ended_with_cr = false;
    }
    driver->crlf_enabled = enabled;
#else
    // Suppress -Wunused-parameter
    (void)driver;
    (void)enabled;

    panic_unsupported();
#endif
}

void stdio_set_chars_available_callback(void (*fn)(void*), void *param) {
    for (stdio_driver_t *s = drivers; s; s = s->next) {
        if (s->set_chars_available_callback) s->set_chars_available_callback(fn, param);
    }
}

#if PICO_STDIO_SHORT_CIRCUIT_CLIB_FUNCS
#define PRIMARY_STDIO_FUNC(x) WRAPPER_FUNC(x)
#else
#define PRIMARY_STDIO_FUNC(x) stdio_ ## x
#endif

int PRIMARY_STDIO_FUNC(getchar)(void) {
    char buf[1];
    int len = stdio_get_until(buf, 1, at_the_end_of_time);
    if (len < 0) return len;
    assert(len == 1);
    return (uint8_t)buf[0];
}

int PRIMARY_STDIO_FUNC(putchar)(int c) {
    char cc = (char)c;
    stdio_put_string(&cc, 1, false, true);
    return c;
}

int PRIMARY_STDIO_FUNC(puts)(const char *s) {
    int len = (int)strlen(s);
    stdio_put_string(s, len, true, true);
    stdio_flush();
    return len;
}

int REAL_FUNC(vprintf)(const char *format, va_list va);

int PRIMARY_STDIO_FUNC(vprintf)(const char *format, va_list va) {
    bool serialized = stdout_serialize_begin();
    if (!serialized) {
#if PICO_STDIO_IGNORE_NESTED_STDOUT
        return 0;
#endif
    }
    int ret;
#if LIB_PICO_PRINTF_PICO
    struct stdio_stack_buffer buffer;
    buffer.used = 0;
    ret = vfctprintf(stdio_buffered_printer, &buffer, format, va);
    stdio_stack_buffer_flush(&buffer);
    stdio_flush();
#elif LIB_PICO_PRINTF_NONE
    ((void)format);
    ((void)va);
    extern void printf_none_assert(void);
    printf_none_assert();
    ret = 0;
#else
    ret = REAL_FUNC(vprintf)(format, va);
    // if we're using PICO_STDIO_SHORT_CIRCUIT_CLIB_FUNCS, then vprintf is short-circuited to here,
    // but we just go ahead and call the library function anyway as we're PICO_PRINTF_COMPILER.
    // therefore we should flush stdout here in case the next thing is a puts/putchar which DO still
    // short-circuit the c library completely.
    //
    // note the more correct place to handle this would be in the short-circuited functions
    // but that would cause infinite recursion as they are called during flush of stdout!
    // this is reasonable best-effort solution (if you really care you can set
    // PICO_STDIO_SHORT_CIRCUIT_CLIB_FUNCS=0 when using pico_printf `compiler`)
#if PICO_STDIO_SHORT_CIRCUIT_CLIB_FUNCS
    // llvm libc doesn't have fflush, nor does it buffer separately
#if !LIB_PICO_LLVM_LIBC_INTERFACE
    fflush(stdout);
#endif
#endif
#endif
    if (serialized) {
        stdout_serialize_end();
    }
    return ret;
}

int __printflike(1, 0) PRIMARY_STDIO_FUNC(printf)(const char* format, ...)
{
    va_list va;
    va_start(va, format);
    int ret = vprintf(format, va);
    va_end(va);
    return ret;
}

#if PICO_STDIO_SHORT_CIRCUIT_CLIB_FUNCS
// define the stdio_ versions to be the same as our wrappers
int stdio_getchar(void) __attribute__((alias(__XSTRING(WRAPPER_FUNC(getchar)))));
int stdio_putchar(int) __attribute__((alias(__XSTRING(WRAPPER_FUNC(putchar)))));
int stdio_puts(const char *s) __attribute__((alias(__XSTRING(WRAPPER_FUNC(puts)))));
int stdio_vprintf(const char *format, va_list va) __attribute__((alias(__XSTRING(WRAPPER_FUNC(vprintf)))));
int __printflike(1, 0) stdio_printf(const char* format, ...) __attribute__((alias(__XSTRING(WRAPPER_FUNC(printf)))));
#else
// todo there is no easy way to avoid the wrapper functions since they are in the CMake, so lets just forward for now

int REAL_FUNC(getchar)(void);
int REAL_FUNC(putchar)(int);
int REAL_FUNC(puts)(const char *s);
int __printflike(1, 0) REAL_FUNC(printf)(const char* format, ...);

int WRAPPER_FUNC(getchar)(void) {
    return REAL_FUNC(getchar)();
}
int WRAPPER_FUNC(putchar)(int c) {
    return REAL_FUNC(putchar)(c);
}
int WRAPPER_FUNC(puts)(const char *s) {
    return REAL_FUNC(puts)(s);
}
int WRAPPER_FUNC(vprintf)(const char *format, va_list va) {
    return REAL_FUNC(vprintf)(format, va);
}
int __printflike(1, 0) WRAPPER_FUNC(printf)(const char* format, ...) {
    va_list va;
    va_start(va, format);
    int ret = REAL_FUNC(vprintf)(format, va);
    va_end(va);
    return ret;
}
#endif


