MODE: INLINE
#include "util.h"
using namespace xs::xlog;

namespace {
    static bool no_format_warnings;

    struct PerlObjectFormatter : IFormatter, Backref {
        string format (std::string& msg, const Info& i) const override {
            if (!is_perl_thread()) throw std::logic_error("can't call pure-perl formatting callback: log() called from perl-foreign thread");
            Object o = xs::out(this);
            auto ret = o.call("format", xs::out(msg), xs::out(i.level), xs::out(i.module->name()), xs::out(i.file), xs::out(i.line), xs::out(i.func));
            return xs::in<string>(ret);
        }
        
        ~PerlObjectFormatter () { Backref::dtor(); }
    };
    
    struct PerlObjectLogger : ILogger, Backref {
        void log_format (std::string& msg, const Info& i, const IFormatter& fmt) override {
            if (!is_perl_thread()) throw std::logic_error("can't call pure-perl logging callback: log() called from perl-foreign thread");
            Object o = xs::out(this);
            auto sub = o.method("log_format");
            if (!sub) return ILogger::log_format(msg, i, fmt);
            sub.call(o.ref(), xs::out(msg), xs::out(i.level), xs::out(i.module->name()), xs::out(i.file), xs::out(i.line), xs::out(i.func), xs::out(&fmt));
        }
        
        void log (const string& msg, const Info& info) override {
            if (!is_perl_thread()) throw std::logic_error("can't call pure-perl logging callback: log() called from perl-foreign thread");
            Object o = xs::out(this);
            o.call("log", xs::out(msg), xs::out(info.level));
        }
        
        ~PerlObjectLogger () { Backref::dtor(); }
    };
    
    struct NoWarningsGuard {
        NoWarningsGuard (bool enabled) : _enabled(enabled), _cop_warnings(), _dowarn() {
            if (!_enabled) return;
            _cop_warnings = PL_curcop->cop_warnings;
            _dowarn       = PL_dowarn;
            PL_curcop->cop_warnings = pWARN_STD;
            PL_dowarn &= ~G_WARN_ON;
        }
        
        ~NoWarningsGuard () {
            if (!_enabled) return;
            PL_curcop->cop_warnings = _cop_warnings;
            PL_dowarn               = _dowarn;
        }
        
    private:
        bool                              _enabled;
        decltype(PL_curcop->cop_warnings) _cop_warnings;
        U8                                _dowarn;
    };
}

namespace xs {
    template <class TYPE> struct Typemap<PerlObjectLogger*, TYPE> : Typemap<ILogger*, TYPE> {};
    template <class TYPE> struct Typemap<PerlObjectFormatter*, TYPE> : Typemap<IFormatter*, TYPE> {};
}

#undef  PANDA_LOG_CODE_POINT
#define PANDA_LOG_CODE_POINT make_code_point()

static inline CV* get_context_sub () {
    int depth = 0;
    auto ctx = caller_cx(depth, nullptr);
    while (ctx) {
        if (CxTYPE(ctx) == CXt_SUB) return ctx->blk_sub.cv;
        ctx = caller_cx(++depth, nullptr);
    }
    return nullptr;
}

static inline CodePoint make_code_point () {
    auto cop = PL_curcop;
    
    string_view func;
    auto cv = get_context_sub();
    if (cv) {
        GV* gv = CvGV(cv);
        if (gv) func = string_view(GvNAME(gv), GvNAMELEN(gv));
    }
    
    return CodePoint{CopFILE(cop), CopLINE(cop), func};
}

static inline Sv format_args (SV** args, int items) {
    if (!items) return Simple(default_message);
    
    if (items == 1) {
        Sv arg(args[0]);
        if (arg.is_simple()) { return arg; }
        else                 { return Simple(SvPV_nolen(args[0]));  }
    }
    
    NoWarningsGuard g(no_format_warnings); (void)g;
    
    STRLEN patlen;
    auto pat = SvPV(args[0], patlen);
    Sv ret = Simple::create(patlen * 1.5);
    bool stub = false;
    sv_vcatpvfn(ret, pat, patlen, nullptr, args + 1, items - 1, &stub);
    return ret;
}

static inline void peep_args (SV**& args, I32& items, const Module*& module, Sub& sub) {
    if (items && SvROK(args[0])) {
        auto first = SvRV(args[0]);
        if (has_module(first)) {
            module = xs::in<Module*>(first);
            ++args;
            --items;
        }
        else if (SvTYPE(first) == SVt_PVCV) {
            if (items > 1) throw exception("no arguments should follow subref when logging");
            sub = first;
        }
    }

    if (!module) module = resolve_module(0); // auto detect module by namespace
}

template<typename Log>
void forward(Log&& log, SV**& args, I32& items, Sub& sub) {
    Simple msg;
    if (sub) {
        Sv ret = sub.call();
        SV* sv = ret;
        msg = format_args(&sv, 1);
    }
    else  {
        msg = format_args(args, items);
    }
    log << msg.as_string<string_view>();
}

static void xslog (Level level, SV** args, I32 items) {
    const Module* module = nullptr;
    Sub sub;
    peep_args(args, items, module, sub);
    panda_log(level, *module, [&]{ forward(log, args, items, sub); });
}

MODULE = XLog                PACKAGE = XLog
PROTOTYPES: DISABLE

BOOT {
    Stash stash(__PACKAGE__);
    
    xs::exp::create_constants(stash, {
        {"VERBOSE_DEBUG", (int)Level::VerboseDebug},
        {"DEBUG",         (int)Level::Debug},
        {"INFO",          (int)Level::Info},
        {"NOTICE",        (int)Level::Notice},
        {"WARNING",       (int)Level::Warning},
        {"ERROR",         (int)Level::Error},
        {"CRITICAL",      (int)Level::Critical},
        {"ALERT",         (int)Level::Alert},
        {"EMERGENCY",     (int)Level::Emergency},
    });
    
    auto root_module = xs::out(&::panda_log_module);
    root_module.readonly(1);
    stash.add_const_sub("default_format", xs::out(default_format));
    stash.add_const_sub("root", root_module);
    stash.store("root_module", root_module);
    
    xs::at_perl_destroy([]{
        if (dyn_cast<PerlObjectLogger*>(get_logger().get())) set_logger(nullptr);
        if (dyn_cast<PerlObjectFormatter*>(get_formatter().get())) set_formatter(nullptr);
    });
}

void set_level (Level level, string_view module = {})

void set_logger (ILoggerSP logger)

void set_formatter (IFormatterSP fmt) : ALIAS(set_format=1) {
    (void)ix;
    set_formatter(fmt);
}

ILoggerSP get_logger ()

IFormatterSP get_formatter ()

Module* get_module (string_view name)

void log (Level level, ...) {
    xslog(level, &ST(1), items-1);
    if (!(PL_op->op_spare & 1)) xlog::optimize();
}

Module* resolve_module (size_t depth = 0) {
    RETVAL = resolve_module(depth);
}

void disable_format_warnings () : ALIAS(enable_format_warnings=1) {
    no_format_warnings = !ix;
}

void __logXXX (...) : ALIAS(verbose_debug=0, debug=1, info=2, notice=3, warning=4, error=5, critical=6, alert=7, emergency=8) {
    Level level = Level((int)Level::VerboseDebug + ix);
    xslog(level, &ST(0), items);
    if (!(PL_op->op_spare & 1)) xlog::optimize(level);
}