/* 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.
 */

#define C_LUCY_ERR
#define C_LUCY_OBJ
#define C_LUCY_VTABLE
#define LUCY_USE_SHORT_NAMES
#define CHY_USE_SHORT_NAMES

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

#include "Lucy/Object/Err.h"
#include "Lucy/Object/CharBuf.h"
#include "Lucy/Object/VTable.h"
#include "Lucy/Util/Memory.h"

Err*
Err_new(CharBuf *mess) {
    Err *self = (Err*)VTable_Make_Obj(ERR);
    return Err_init(self, mess);
}

Err*
Err_init(Err *self, CharBuf *mess) {
    self->mess = mess;
    return self;
}

void
Err_destroy(Err *self) {
    DECREF(self->mess);
    SUPER_DESTROY(self, ERR);
}

Err*
Err_make(Err *self) {
    UNUSED_VAR(self);
    return Err_new(CB_new(0));
}

CharBuf*
Err_to_string(Err *self) {
    return (CharBuf*)INCREF(self->mess);
}

void
Err_cat_mess(Err *self, const CharBuf *mess) {
    CB_Cat(self->mess, mess);
}

// Fallbacks in case variadic macros aren't available.
#ifndef CHY_HAS_VARIADIC_MACROS
void
THROW(VTable *vtable, char *pattern, ...) {
    va_list args;
    Err_make_t make
        = (Err_make_t)METHOD(CERTIFY(vtable, VTABLE), Err, Make);
    Err *err = (Err*)CERTIFY(make(NULL), ERR);
    CharBuf *mess = Err_Get_Mess(err);

    va_start(args, pattern);
    CB_VCatF(mess, pattern, args);
    va_end(args);

    Err_do_throw(err);
}
void
CFISH_WARN(char *pattern, ...) {
    va_list args;
    CharBuf *const message = CB_new(strlen(pattern) + 10);

    va_start(args, pattern);
    CB_VCatF(message, pattern, args);
    va_end(args);

    Err_warn_mess(message);
}
CharBuf*
CFISH_MAKE_MESS(char *pattern, ...) {
    va_list args;
    CharBuf *const message = CB_new(strlen(pattern) + 10);

    va_start(args, pattern);
    CB_VCatF(message, pattern, args);
    va_end(args);

    return message;
}
#endif


static void
S_vcat_mess(CharBuf *message, const char *file, int line, const char *func,
            const char *pattern, va_list args) {
    size_t guess_len = strlen(file)
                       + func ? strlen(func) : 0
                       + strlen(pattern)
                       + 30;
    CB_Grow(message, guess_len);
    CB_VCatF(message, pattern, args);
    if (func != NULL) {
        CB_catf(message, "\n\t%s at %s line %i32\n", func, file, (int32_t)line);
    }
    else {
        CB_catf(message, "\n\t%s line %i32\n", file, (int32_t)line);
    }
}

CharBuf*
Err_make_mess(const char *file, int line, const char *func,
              const char *pattern, ...) {
    va_list args;
    size_t guess_len = strlen(pattern) + strlen(file) + 20;
    CharBuf *message = CB_new(guess_len);
    va_start(args, pattern);
    S_vcat_mess(message, file, line, func, pattern, args);
    va_end(args);
    return message;
}

void
Err_warn_at(const char *file, int line, const char *func,
            const char *pattern, ...) {
    va_list args;
    CharBuf *message = CB_new(0);
    va_start(args, pattern);
    S_vcat_mess(message, file, line, func, pattern, args);
    va_end(args);
    Err_warn_mess(message);
}

CharBuf*
Err_get_mess(Err *self) {
    return self->mess;
}

void
Err_add_frame(Err *self, const char *file, int line, const char *func) {
    if (CB_Ends_With_Str(self->mess, "\n", 1)) { CB_Chop(self->mess, 1); }

    if (func != NULL) {
        CB_catf(self->mess, "\n\t%s at %s line %i32\n", func, file,
                (int32_t)line);
    }
    else {
        CB_catf(self->mess, "\n\tat %s line %i32\n", file, (int32_t)line);
    }
}

void
Err_rethrow(Err *self, const char *file, int line, const char *func) {
    Err_add_frame(self, file, line, func);
    Err_do_throw(self);
}

void
Err_throw_at(VTable *vtable, const char *file, int line,
             const char *func, const char *pattern, ...) {
    va_list args;
    Err_make_t make
        = (Err_make_t)METHOD(CERTIFY(vtable, VTABLE), Err, Make);
    Err *err = (Err*)CERTIFY(make(NULL), ERR);
    CharBuf *mess = Err_Get_Mess(err);

    va_start(args, pattern);
    S_vcat_mess(mess, file, line, func, pattern, args);
    va_end(args);

    Err_do_throw(err);
}

// Inlined, slightly optimized version of Obj_is_a.
static INLINE bool_t
SI_obj_is_a(Obj *obj, VTable *target_vtable) {
    VTable *vtable = obj->vtable;

    while (vtable != NULL) {
        if (vtable == target_vtable) {
            return true;
        }
        vtable = vtable->parent;
    }

    return false;
}

Obj*
Err_downcast(Obj *obj, VTable *vtable, const char *file, int line,
             const char *func) {
    if (obj && !SI_obj_is_a(obj, vtable)) {
        Err_throw_at(ERR, file, line, func, "Can't downcast from %o to %o",
                     Obj_Get_Class_Name(obj), VTable_Get_Name(vtable));
    }
    return obj;
}

Obj*
Err_certify(Obj *obj, VTable *vtable, const char *file, int line,
            const char *func) {
    if (!obj) {
        Err_throw_at(ERR, file, line, func, "Object isn't a %o, it's NULL",
                     VTable_Get_Name(vtable));
    }
    else if (!SI_obj_is_a(obj, vtable)) {
        Err_throw_at(ERR, file, line, func, "Can't downcast from %o to %o",
                     Obj_Get_Class_Name(obj), VTable_Get_Name(vtable));
    }
    return obj;
}

#ifdef CHY_HAS_WINDOWS_H

#include <windows.h>

char*
Err_win_error() {
    size_t buf_size = 256;
    char *buf = (char*)MALLOCATE(buf_size);
    size_t message_len = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
                                       NULL,       // message source table
                                       GetLastError(),
                                       0,          // language id
                                       buf,
                                       buf_size,
                                       NULL        // empty va_list
                                      );
    if (message_len == 0) {
        char unknown[] = "Unknown error";
        size_t len = sizeof(unknown);
        strncpy(buf, unknown, len);
    }
    else if (message_len > 1) {
        // Kill stupid newline.
        buf[message_len - 2] = '\0';
    }
    return buf;
}

#else

char*
Err_win_error() {
    return NULL; // Never called.
}

#endif // CHY_HAS_WINDOWS_H