#include <stdio.h>

#include <cleonos_syscall.h>

typedef unsigned long clio_size_t;

#define CLIO_SINK_FD 1
#define CLIO_SINK_BUF 2

struct clio_sink {
    int mode;
    int fd;
    char *buffer;
    clio_size_t capacity;
    clio_size_t count;
    int failed;
};

static clio_size_t clio_strlen(const char *text) {
    clio_size_t len = 0UL;

    if (text == (const char *)0) {
        return 0UL;
    }

    while (text[len] != '\0') {
        len++;
    }

    return len;
}

static int clio_write_all_fd(int fd, const char *text, clio_size_t len) {
    const char *cursor = text;
    clio_size_t left = len;

    if (fd < 0 || (text == (const char *)0 && len != 0UL)) {
        return EOF;
    }

    while (left > 0UL) {
        u64 wrote = cleonos_sys_fd_write((u64)fd, cursor, (u64)left);
        clio_size_t progressed;

        if (wrote == 0ULL || wrote == (u64)-1) {
            return EOF;
        }

        progressed = (wrote > (u64)left) ? left : (clio_size_t)wrote;
        cursor += progressed;
        left -= progressed;
    }

    if (len > 0x7FFFFFFFUL) {
        return 0x7FFFFFFF;
    }

    return (int)len;
}

static void clio_sink_init_fd(struct clio_sink *sink, int fd) {
    sink->mode = CLIO_SINK_FD;
    sink->fd = fd;
    sink->buffer = (char *)0;
    sink->capacity = 0UL;
    sink->count = 0UL;
    sink->failed = 0;
}

static void clio_sink_init_buffer(struct clio_sink *sink, char *out, clio_size_t out_size) {
    sink->mode = CLIO_SINK_BUF;
    sink->fd = -1;
    sink->buffer = out;
    sink->capacity = out_size;
    sink->count = 0UL;
    sink->failed = 0;

    if (out != (char *)0 && out_size > 0UL) {
        out[0] = '\0';
    }
}

static void clio_sink_finalize_buffer(struct clio_sink *sink) {
    if (sink->mode != CLIO_SINK_BUF || sink->buffer == (char *)0 || sink->capacity == 0UL) {
        return;
    }

    if (sink->count < sink->capacity) {
        sink->buffer[sink->count] = '\0';
    } else {
        sink->buffer[sink->capacity - 1UL] = '\0';
    }
}

static int clio_sink_emit(struct clio_sink *sink, const char *text, clio_size_t len) {
    if (sink == (struct clio_sink *)0 || text == (const char *)0 || len == 0UL) {
        return 1;
    }

    if (sink->mode == CLIO_SINK_FD) {
        if (clio_write_all_fd(sink->fd, text, len) == EOF) {
            sink->failed = 1;
            return 0;
        }
    } else {
        if (sink->buffer != (char *)0 && sink->capacity > 0UL) {
            clio_size_t i;
            clio_size_t start = sink->count;
            clio_size_t writable = 0UL;

            if (start + 1UL < sink->capacity) {
                writable = (sink->capacity - 1UL) - start;
            }

            if (len < writable) {
                writable = len;
            }

            for (i = 0UL; i < writable; i++) {
                sink->buffer[start + i] = text[i];
            }
        }
    }

    sink->count += len;
    return 1;
}

static clio_size_t clio_u64_to_base(unsigned long long value, unsigned int base, int uppercase, char *out,
                                    clio_size_t out_size) {
    char tmp[64];
    clio_size_t pos = 0UL;
    clio_size_t i = 0UL;

    if (out == (char *)0 || out_size < 2UL || base < 2U || base > 16U) {
        return 0UL;
    }

    if (value == 0ULL) {
        out[0] = '0';
        out[1] = '\0';
        return 1UL;
    }

    while (value != 0ULL && pos < (clio_size_t)sizeof(tmp)) {
        unsigned long long digit = value % (unsigned long long)base;

        if (digit < 10ULL) {
            tmp[pos++] = (char)('0' + digit);
        } else {
            char alpha = (uppercase != 0) ? 'A' : 'a';
            tmp[pos++] = (char)(alpha + (char)(digit - 10ULL));
        }

        value /= (unsigned long long)base;
    }

    if (pos + 1UL > out_size) {
        return 0UL;
    }

    while (pos > 0UL) {
        pos--;
        out[i++] = tmp[pos];
    }

    out[i] = '\0';
    return i;
}

static clio_size_t clio_i64_to_dec(long long value, char *out, clio_size_t out_size) {
    unsigned long long magnitude;
    clio_size_t offset = 0UL;
    clio_size_t len;

    if (out == (char *)0 || out_size < 2UL) {
        return 0UL;
    }

    if (value < 0LL) {
        out[offset++] = '-';
        if (offset + 2UL > out_size) {
            return 0UL;
        }
        magnitude = (unsigned long long)(-(value + 1LL)) + 1ULL;
    } else {
        magnitude = (unsigned long long)value;
    }

    len = clio_u64_to_base(magnitude, 10U, 0, out + offset, out_size - offset);

    if (len == 0UL) {
        return 0UL;
    }

    return offset + len;
}

static int clio_is_digit(char ch) {
    return (ch >= '0' && ch <= '9') ? 1 : 0;
}

static int clio_emit_repeat(struct clio_sink *sink, char ch, clio_size_t count) {
    while (count > 0UL) {
        if (clio_sink_emit(sink, &ch, 1UL) == 0) {
            return 0;
        }
        count--;
    }

    return 1;
}

static int clio_emit_with_width(struct clio_sink *sink, const char *text, clio_size_t text_len, int left_align,
                                char pad_char, clio_size_t width) {
    if (left_align == 0 && width > text_len) {
        if (clio_emit_repeat(sink, pad_char, width - text_len) == 0) {
            return 0;
        }
    }

    if (text_len > 0UL && clio_sink_emit(sink, text, text_len) == 0) {
        return 0;
    }

    if (left_align != 0 && width > text_len) {
        if (clio_emit_repeat(sink, ' ', width - text_len) == 0) {
            return 0;
        }
    }

    return 1;
}

static int clio_vformat(struct clio_sink *sink, const char *fmt, va_list args) {
    const char *cursor = fmt;

    if (sink == (struct clio_sink *)0 || fmt == (const char *)0) {
        return EOF;
    }

    while (*cursor != '\0') {
        if (*cursor != '%') {
            if (clio_sink_emit(sink, cursor, 1UL) == 0) {
                return EOF;
            }
            cursor++;
            continue;
        }

        cursor++;

        if (*cursor == '%') {
            if (clio_sink_emit(sink, "%", 1UL) == 0) {
                return EOF;
            }
            cursor++;
            continue;
        }

        {
            int left_align = 0;
            int zero_pad = 0;
            clio_size_t width = 0UL;
            int has_precision = 0;
            clio_size_t precision = 0UL;
            int length_mode = 0; /* 0: default, 1: l, 2: ll, 3: z */
            char spec = *cursor;
            char number_buf[96];

            while (spec == '-' || spec == '0') {
                if (spec == '-') {
                    left_align = 1;
                } else if (spec == '0') {
                    zero_pad = 1;
                }
                cursor++;
                spec = *cursor;
            }

            while (clio_is_digit(spec) != 0) {
                width = (width * 10UL) + (clio_size_t)(unsigned long)(spec - '0');
                cursor++;
                spec = *cursor;
            }

            if (spec == '.') {
                has_precision = 1;
                precision = 0UL;
                cursor++;
                spec = *cursor;

                while (clio_is_digit(spec) != 0) {
                    precision = (precision * 10UL) + (clio_size_t)(unsigned long)(spec - '0');
                    cursor++;
                    spec = *cursor;
                }
            }

            if (spec == 'l') {
                cursor++;
                spec = *cursor;
                length_mode = 1;

                if (spec == 'l') {
                    cursor++;
                    spec = *cursor;
                    length_mode = 2;
                }
            } else if (spec == 'z') {
                cursor++;
                spec = *cursor;
                length_mode = 3;
            }

            if (spec == '\0') {
                break;
            }

            if (spec == 's') {
                const char *text = va_arg(args, const char *);
                clio_size_t text_len;

                if (text == (const char *)0) {
                    text = "(null)";
                }

                text_len = clio_strlen(text);
                if (has_precision != 0 && precision < text_len) {
                    text_len = precision;
                }

                if (clio_emit_with_width(sink, text, text_len, left_align, ' ', width) == 0) {
                    return EOF;
                }
            } else if (spec == 'c') {
                char out = (char)va_arg(args, int);
                char out_buf[1];

                out_buf[0] = out;
                if (clio_emit_with_width(sink, out_buf, 1UL, left_align, ' ', width) == 0) {
                    return EOF;
                }
            } else if (spec == 'd' || spec == 'i') {
                long long value;
                char sign_char = '\0';
                const char *digits = number_buf;
                clio_size_t digits_len;
                clio_size_t leading_zeros = 0UL;
                clio_size_t core_len;
                int sign_emitted = 0;

                if (length_mode == 2) {
                    value = va_arg(args, long long);
                } else if (length_mode == 1) {
                    value = (long long)va_arg(args, long);
                } else if (length_mode == 3) {
                    value = (long long)va_arg(args, long long);
                } else {
                    value = (long long)va_arg(args, int);
                }

                digits_len = clio_i64_to_dec(value, number_buf, (clio_size_t)sizeof(number_buf));
                if (digits_len == 0UL) {
                    return EOF;
                }

                if (number_buf[0] == '-') {
                    sign_char = '-';
                    digits = number_buf + 1;
                    digits_len--;
                }

                if (has_precision != 0 && precision == 0UL && value == 0LL) {
                    digits_len = 0UL;
                }

                if (has_precision != 0 && precision > digits_len) {
                    leading_zeros = precision - digits_len;
                }

                core_len = digits_len + leading_zeros + ((sign_char != '\0') ? 1UL : 0UL);

                if (left_align == 0 && width > core_len) {
                    if (zero_pad != 0 && has_precision == 0) {
                        if (sign_char != '\0') {
                            if (clio_sink_emit(sink, &sign_char, 1UL) == 0) {
                                return EOF;
                            }
                            sign_emitted = 1;
                        }
                        if (clio_emit_repeat(sink, '0', width - core_len) == 0) {
                            return EOF;
                        }
                    } else {
                        if (clio_emit_repeat(sink, ' ', width - core_len) == 0) {
                            return EOF;
                        }
                    }
                }

                if (sign_char != '\0' && sign_emitted == 0) {
                    if (clio_sink_emit(sink, &sign_char, 1UL) == 0) {
                        return EOF;
                    }
                }

                if (clio_emit_repeat(sink, '0', leading_zeros) == 0) {
                    return EOF;
                }

                if (digits_len > 0UL && clio_sink_emit(sink, digits, digits_len) == 0) {
                    return EOF;
                }

                if (left_align != 0 && width > core_len) {
                    if (clio_emit_repeat(sink, ' ', width - core_len) == 0) {
                        return EOF;
                    }
                }
            } else if (spec == 'u' || spec == 'x' || spec == 'X') {
                unsigned long long value;
                unsigned int base = (spec == 'u') ? 10U : 16U;
                int upper = (spec == 'X') ? 1 : 0;
                clio_size_t digits_len;
                clio_size_t leading_zeros = 0UL;
                clio_size_t core_len;
                char pad_char = ' ';

                if (length_mode == 2) {
                    value = va_arg(args, unsigned long long);
                } else if (length_mode == 1) {
                    value = (unsigned long long)va_arg(args, unsigned long);
                } else if (length_mode == 3) {
                    value = (unsigned long long)va_arg(args, unsigned long long);
                } else {
                    value = (unsigned long long)va_arg(args, unsigned int);
                }

                digits_len = clio_u64_to_base(value, base, upper, number_buf, (clio_size_t)sizeof(number_buf));
                if (digits_len == 0UL) {
                    return EOF;
                }

                if (has_precision != 0 && precision == 0UL && value == 0ULL) {
                    digits_len = 0UL;
                }

                if (has_precision != 0 && precision > digits_len) {
                    leading_zeros = precision - digits_len;
                }

                core_len = digits_len + leading_zeros;

                if (zero_pad != 0 && has_precision == 0 && left_align == 0) {
                    pad_char = '0';
                }

                if (left_align == 0 && width > core_len) {
                    if (clio_emit_repeat(sink, pad_char, width - core_len) == 0) {
                        return EOF;
                    }
                }

                if (clio_emit_repeat(sink, '0', leading_zeros) == 0) {
                    return EOF;
                }

                if (digits_len > 0UL && clio_sink_emit(sink, number_buf, digits_len) == 0) {
                    return EOF;
                }

                if (left_align != 0 && width > core_len) {
                    if (clio_emit_repeat(sink, ' ', width - core_len) == 0) {
                        return EOF;
                    }
                }
            } else if (spec == 'p') {
                const void *ptr = va_arg(args, const void *);
                unsigned long long value = (unsigned long long)(unsigned long)ptr;
                clio_size_t digits_len;
                clio_size_t leading_zeros = 0UL;
                clio_size_t core_len;

                digits_len = clio_u64_to_base(value, 16U, 0, number_buf, (clio_size_t)sizeof(number_buf));
                if (digits_len == 0UL) {
                    return EOF;
                }

                if (has_precision != 0 && precision == 0UL && value == 0ULL) {
                    digits_len = 0UL;
                }

                if (has_precision != 0 && precision > digits_len) {
                    leading_zeros = precision - digits_len;
                }

                core_len = 2UL + leading_zeros + digits_len;

                if (left_align == 0 && width > core_len) {
                    if (clio_emit_repeat(sink, ' ', width - core_len) == 0) {
                        return EOF;
                    }
                }

                if (clio_sink_emit(sink, "0x", 2UL) == 0) {
                    return EOF;
                }

                if (clio_emit_repeat(sink, '0', leading_zeros) == 0) {
                    return EOF;
                }

                if (digits_len > 0UL && clio_sink_emit(sink, number_buf, digits_len) == 0) {
                    return EOF;
                }

                if (left_align != 0 && width > core_len) {
                    if (clio_emit_repeat(sink, ' ', width - core_len) == 0) {
                        return EOF;
                    }
                }
            } else if (spec == 'n') {
                int *out_count = va_arg(args, int *);
                if (out_count != (int *)0) {
                    if (sink->count > 0x7FFFFFFFUL) {
                        *out_count = 0x7FFFFFFF;
                    } else {
                        *out_count = (int)sink->count;
                    }
                }
            } else {
                char fallback[2];
                fallback[0] = spec;
                fallback[1] = '\0';

                if (clio_sink_emit(sink, "%", 1UL) == 0 || clio_sink_emit(sink, fallback, 1UL) == 0) {
                    return EOF;
                }
            }

            cursor++;
        }
    }

    if (sink->mode == CLIO_SINK_BUF) {
        clio_sink_finalize_buffer(sink);
    }

    if (sink->failed != 0) {
        return EOF;
    }

    if (sink->count > 0x7FFFFFFFUL) {
        return 0x7FFFFFFF;
    }

    return (int)sink->count;
}

int putchar(int ch) {
    char out = (char)(ch & 0xFF);
    return (clio_write_all_fd(1, &out, 1UL) == EOF) ? EOF : (int)(unsigned char)out;
}

int getchar(void) {
    char ch = '\0';

    for (;;) {
        u64 got = cleonos_sys_fd_read(0ULL, &ch, 1ULL);

        if (got == 1ULL) {
            return (int)(unsigned char)ch;
        }

        if (got == (u64)-1) {
            return EOF;
        }

        (void)cleonos_sys_yield();
    }
}

int fputc(int ch, int fd) {
    char out = (char)(ch & 0xFF);
    return (clio_write_all_fd(fd, &out, 1UL) == EOF) ? EOF : (int)(unsigned char)out;
}

int fgetc(int fd) {
    char ch = '\0';

    if (fd < 0) {
        return EOF;
    }

    for (;;) {
        u64 got = cleonos_sys_fd_read((u64)fd, &ch, 1ULL);

        if (got == 1ULL) {
            return (int)(unsigned char)ch;
        }

        if (got == (u64)-1) {
            return EOF;
        }

        (void)cleonos_sys_yield();
    }
}

int fputs(const char *text, int fd) {
    clio_size_t len;

    if (text == (const char *)0) {
        return EOF;
    }

    len = clio_strlen(text);
    return clio_write_all_fd(fd, text, len);
}

int puts(const char *text) {
    int wrote = fputs(text, 1);

    if (wrote == EOF) {
        return EOF;
    }

    if (putchar('\n') == EOF) {
        return EOF;
    }

    return wrote + 1;
}

int vsnprintf(char *out, unsigned long out_size, const char *fmt, va_list args) {
    struct clio_sink sink;
    va_list args_copy;
    int rc;

    clio_sink_init_buffer(&sink, out, out_size);
    va_copy(args_copy, args);
    rc = clio_vformat(&sink, fmt, args_copy);
    va_end(args_copy);

    return rc;
}

int snprintf(char *out, unsigned long out_size, const char *fmt, ...) {
    va_list args;
    int rc;

    va_start(args, fmt);
    rc = vsnprintf(out, out_size, fmt, args);
    va_end(args);

    return rc;
}

int vdprintf(int fd, const char *fmt, va_list args) {
    struct clio_sink sink;
    va_list args_copy;
    int rc;

    clio_sink_init_fd(&sink, fd);
    va_copy(args_copy, args);
    rc = clio_vformat(&sink, fmt, args_copy);
    va_end(args_copy);

    return rc;
}

int dprintf(int fd, const char *fmt, ...) {
    va_list args;
    int rc;

    va_start(args, fmt);
    rc = vdprintf(fd, fmt, args);
    va_end(args);

    return rc;
}

int vfprintf(int fd, const char *fmt, va_list args) {
    return vdprintf(fd, fmt, args);
}

int fprintf(int fd, const char *fmt, ...) {
    va_list args;
    int rc;

    va_start(args, fmt);
    rc = vdprintf(fd, fmt, args);
    va_end(args);

    return rc;
}

int vprintf(const char *fmt, va_list args) {
    return vdprintf(1, fmt, args);
}

int printf(const char *fmt, ...) {
    va_list args;
    int rc;

    va_start(args, fmt);
    rc = vdprintf(1, fmt, args);
    va_end(args);

    return rc;
}
