// Copyright (c) 2015 Baidu, Inc.
// 
// Licensed 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.

// Author: Ge,Jun (gejun@baidu.com)
// Date: Mon Jul 20 12:39:39 CST 2015

#include <com_log.h>
#include "butil/memory/singleton.h"
#include "butil/comlog_sink.h"
#include "butil/files/file_path.h"
#include "butil/fd_guard.h"
#include "butil/file_util.h"
#include "butil/endpoint.h"

namespace logging {
DECLARE_bool(log_year);
DECLARE_bool(log_hostname);

struct ComlogLayoutOptions {
    ComlogLayoutOptions() : shorter_log_level(true) {}
    
    bool shorter_log_level;
};

class ComlogLayout : public comspace::Layout {
public:
    explicit ComlogLayout(const ComlogLayoutOptions* options);
    ~ComlogLayout();
    int format(comspace::Event *evt);
private:
    ComlogLayoutOptions _options;
};

ComlogLayout::ComlogLayout(const ComlogLayoutOptions* options) {
    if (options) {
        _options = *options;
    }
}

ComlogLayout::~ComlogLayout() {
}

// Override Layout::format to have shorter prefixes. Patterns are just ignored.
int ComlogLayout::format(comspace::Event *evt) {
    const int bufsize = evt->_render_msgbuf_size;
    char* const buf = evt->_render_msgbuf;
    if (bufsize < 2){
        return -1;
    }

    time_t t = evt->_print_time.tv_sec;
    struct tm local_tm = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL};
#if _MSC_VER >= 1400
    localtime_s(&local_tm, &t);
#else
    localtime_r(&t, &local_tm);
#endif
    int len = 0;
    if (_options.shorter_log_level) {
        buf[len++] = *comspace::getLogName(evt->_log_level);
    } else {
        const char* const name = comspace::getLogName(evt->_log_level);
        int cp_len = std::min(bufsize - len, (int)strlen(name));
        memcpy(buf + len, name, cp_len);
        len += cp_len;
        if (len < bufsize - 1) {
            buf[len++] = ' ';
        }
    }
    if (len < bufsize - 1) {
        int ret = 0;
        if (FLAGS_log_year) {
            ret = snprintf(buf + len, bufsize - len,
                           "%04d%02d%02d %02d:%02d:%02d.%06d %5u ",
                           local_tm.tm_year + 1900,
                           local_tm.tm_mon + 1,
                           local_tm.tm_mday,
                           local_tm.tm_hour,
                           local_tm.tm_min,
                           local_tm.tm_sec,
                           (int)evt->_print_time.tv_usec,
                           (unsigned int)evt->_thread_id);
        } else {
            ret = snprintf(buf + len, bufsize - len,
                           "%02d%02d %02d:%02d:%02d.%06d %5u ",
                           local_tm.tm_mon + 1,
                           local_tm.tm_mday,
                           local_tm.tm_hour,
                           local_tm.tm_min,
                           local_tm.tm_sec,
                           (int)evt->_print_time.tv_usec,
                           (unsigned int)evt->_thread_id);
        }
        if (ret >= 0) {
            len += ret;
        } else {
            // older glibc may return negative which means the buffer is full.
            len = bufsize;
        }
    }
    if (len > 0 && len < bufsize - 1) {  // not truncated.
        // Although it's very stupid, we have to copy the message again due
        // to the design of comlog.
        int cp_len = std::min(bufsize - len, evt->_msgbuf_len);
        memcpy(buf + len, evt->_msgbuf, cp_len);
        len += cp_len;
    }
    if (len >= bufsize - 1) {
        len = bufsize - 2;
    }
    buf[len++] = '\n';
    buf[len] = 0;
    evt->_render_msgbuf_len = len;
    return 0;
}

ComlogSink* ComlogSink::GetInstance() {
    return Singleton<ComlogSink, LeakySingletonTraits<ComlogSink> >::get();
}

ComlogSinkOptions::ComlogSinkOptions()
    : async(false)
    , shorter_log_level(true)
    , log_dir("log")
    , max_log_length(2048)
    , print_vlog_as_warning(true)
    , split_type(COMLOG_SPLIT_TRUNCT)
    , cut_size_megabytes(2048)
    , quota_size(0)
    , cut_interval_minutes(60)
    , quota_day(0)
    , quota_hour(0)
    , quota_min(0)
    , enable_wf_device(false) {
}

ComlogSink::ComlogSink() 
    : _init(false), _dev(NULL) {
}

int ComlogSink::SetupFromConfig(const std::string& conf_path_str) {
    Unload();
    butil::FilePath path(conf_path_str);
    if (com_loadlog(path.DirName().value().c_str(),
                    path.BaseName().value().c_str()) != 0) {
        LOG(ERROR) << "Fail to create ComlogSink from `" << conf_path_str << "'";
        return -1;
    }
    _init = true;
    return 0;
}

// This is definitely linux specific.
static std::string GetProcessName() {
    butil::fd_guard fd(open("/proc/self/cmdline", O_RDONLY));
    if (fd < 0) {
        return "unknown";
    }
    char buf[512];
    const ssize_t len = read(fd, buf, sizeof(buf) - 1);
    if (len <= 0) {
        return "unknown";
    }
    buf[len] = '\0';
    // Not string(buf, len) because we needs to buf to be truncated at first \0.
    // Under gdb, the first part of cmdline may include path.
    return butil::FilePath(std::string(buf)).BaseName().value();
}

int ComlogSink::SetupDevice(com_device_t* dev, const char* type, const char* file, bool is_wf) {
    butil::FilePath path(file);
    snprintf(dev->host, sizeof(dev->host), "%s", path.DirName().value().c_str());
    if (!is_wf) {
        snprintf(dev->name, sizeof(dev->name), "%s_0", type);
        COMLOG_SETSYSLOG(*dev);

        //snprintf(dev->file, COM_MAXFILENAME, "%s", file);
        snprintf(dev->file, sizeof(dev->file), "%s", path.BaseName().value().c_str());
    } else {
        snprintf(dev->name, sizeof(dev->name), "%s_1", type);
        dev->log_mask = 0;
        COMLOG_ADDMASK(*dev, COMLOG_WARNING);
        COMLOG_ADDMASK(*dev, COMLOG_FATAL);

        //snprintf(dev->file, COM_MAXFILENAME, "%s.wf", file);
        snprintf(dev->file, sizeof(dev->file), "%s.wf", path.BaseName().value().c_str());
    }
    
    snprintf(dev->type, COM_MAXAPPENDERNAME, "%s", type);
    dev->splite_type = static_cast<int>(_options.split_type);
    dev->log_size = _options.cut_size_megabytes; // SIZECUT precision in MB
    dev->compress = 0;
    dev->cuttime = _options.cut_interval_minutes; // DATECUT time precision in min

    // set quota conf
    int index = dev->reserved_num;
    if (dev->splite_type == COMLOG_SPLIT_SIZECUT) {
        if (_options.cut_size_megabytes <= 0) {
            LOG(ERROR) << "Invalid ComlogSinkOptions.cut_size_megabytes="
                       << _options.cut_size_megabytes;
            return -1;
        }
        if (_options.quota_size < 0) {
            LOG(ERROR) << "Invalid ComlogSinkOptions.quota_size="
                       << _options.quota_size;
            return -1;
        }
        snprintf(dev->reservedext[index].name, sizeof(dev->reservedext[index].name),
                 "%s_QUOTA_SIZE", dev->name);
        snprintf(dev->reservedext[index].value, sizeof(dev->reservedext[index].value),
                 "%d", _options.quota_size);
        index++;
    } else if (dev->splite_type == COMLOG_SPLIT_DATECUT) {
        if (_options.quota_day < 0) {
            LOG(ERROR) << "Invalid ComlogSinkOptions.quota_day=" << _options.quota_day;
            return -1;
        }
        if (_options.quota_hour < 0) {
            LOG(ERROR) << "Invalid ComlogSinkOptions.quota_hour=" << _options.quota_hour;
            return -1;
        }
        if (_options.quota_min < 0) {
            LOG(ERROR) << "Invalid ComlogSinkOptions.quota_min=" << _options.quota_min;
            return -1;
        }
        if (_options.quota_day > 0) {
            snprintf(dev->reservedext[index].name, sizeof(dev->reservedext[index].name),
                     "%s_QUOTA_DAY", (char*)dev->name);
            snprintf(dev->reservedext[index].value, sizeof(dev->reservedext[index].value),
                     "%d", _options.quota_day);
            index++;
        }
        if (_options.quota_hour > 0) {
            snprintf(dev->reservedext[index].name, sizeof(dev->reservedext[index].name),
                     "%s_QUOTA_HOUR", (char*)dev->name);
            snprintf(dev->reservedext[index].value, sizeof(dev->reservedext[index].value),
                     "%d", _options.quota_hour);
            index++;
        }
        if (_options.quota_min > 0) {
            snprintf(dev->reservedext[index].name, sizeof(dev->reservedext[index].name),
                     "%s_QUOTA_MIN", (char*)dev->name);
            snprintf(dev->reservedext[index].value, sizeof(dev->reservedext[index].value),
                     "%d", _options.quota_min);
            index++;
        }
    }
    dev->reserved_num = index;
    dev->reservedconf.item = &dev->reservedext[0];
    dev->reservedconf.num = dev->reserved_num;
    dev->reservedconf.size = dev->reserved_num;

    ComlogLayoutOptions layout_options;
    layout_options.shorter_log_level = _options.shorter_log_level;
    ComlogLayout* layout = new (std::nothrow) ComlogLayout(&layout_options);
    if (layout == NULL) {
        LOG(FATAL) << "Fail to new layout";
        return -1;
    }
    dev->layout = layout;

    return 0;
}

int ComlogSink::Setup(const ComlogSinkOptions* options) {
    Unload();
    if (options) {
        _options = *options;
    }
    if (_options.max_log_length > 0) {
        comspace::Event::setMaxLogLength(_options.max_log_length);
    }
    if (_options.process_name.empty()) {
        _options.process_name = GetProcessName();
    }

    char type[COM_MAXAPPENDERNAME];
    if (_options.async) {
        snprintf(type, COM_MAXAPPENDERNAME, "AFILE");
    } else {
        snprintf(type, COM_MAXAPPENDERNAME, "FILE");
    }
    butil::FilePath cwd;
    if (!_options.log_dir.empty()) {
        butil::FilePath log_dir(_options.log_dir);
        if (log_dir.IsAbsolute()) {
            cwd = log_dir;
        } else {
            if (!butil::GetCurrentDirectory(&cwd)) {
                LOG(ERROR) << "Fail to get cwd";
                return -1;
            }
            cwd = cwd.Append(log_dir);
        }
    } else {
        if (!butil::GetCurrentDirectory(&cwd)) {
            LOG(ERROR) << "Fail to get cwd";
            return -1;
        }
    }
    butil::File::Error err;
    if (!butil::CreateDirectoryAndGetError(cwd, &err)) {
        LOG(ERROR) << "Fail to create directory, " << err;
        return -1;
    }
    char file[COM_MAXFILENAME];
    snprintf(file, COM_MAXFILENAME, "%s",
             cwd.Append(_options.process_name + ".log").value().c_str());

    int dev_num = (_options.enable_wf_device ? 2 : 1);
    _dev = new (std::nothrow) com_device_t[dev_num];
    if (NULL == _dev) {
        LOG(FATAL) << "Fail to new com_device_t";
        return -1;
    }
    if (0 != SetupDevice(&_dev[0], type, file, false)) {
        LOG(ERROR) << "Fail to setup first com_device_t";
        return -1;
    }
    if (dev_num == 2) {
        if (0 != SetupDevice(&_dev[1], type, file, true)) {
            LOG(ERROR) << "Fail to setup second com_device_t";
            return -1;
        }
    }
    if (com_openlog(_options.process_name.c_str(), _dev, dev_num, NULL) != 0) {
        LOG(ERROR) << "Fail to com_openlog";
        return -1;
    }
    _init = true;
    return 0;
}
        
void ComlogSink::Unload() {
    if (_init) {
        com_closelog(0);
        _init = false;
    }
    if (_dev) {
        // FIXME(gejun): Can't delete layout, somewhere in comlog may still
        // reference the layout after com_closelog.
        //delete _dev->layout;
        delete [] _dev;
        _dev = NULL;
    }
}

ComlogSink::~ComlogSink() {
    Unload();
}

int const comlog_levels[LOG_NUM_SEVERITIES] = {
    COMLOG_TRACE, COMLOG_NOTICE, COMLOG_WARNING, COMLOG_FATAL, COMLOG_FATAL };

bool ComlogSink::OnLogMessage(int severity, const char* file, int line,
                              const butil::StringPiece& content) {
    // Print warning for VLOG since many online servers do not enable COMLOG_TRACE.
    int comlog_level = 0;
    if (severity < 0) {
        comlog_level = _options.print_vlog_as_warning ? COMLOG_WARNING : COMLOG_TRACE;
    } else {
        comlog_level = comlog_levels[severity];
    }
    if (FLAGS_log_hostname) {
        butil::StringPiece hostname(butil::my_hostname());
        if (hostname.ends_with(".baidu.com")) { // make it shorter
            hostname.remove_suffix(10);
        }
        return com_writelog(comlog_level, "%.*s %s:%d] %.*s",
                            (int)hostname.size(), hostname.data(),
                            file, line,
                            (int)content.size(), content.data()) == 0;
    }
    // Using %.*s is faster than %s.
    return com_writelog(comlog_level, "%s:%d] %.*s", file, line,
                        (int)content.size(), content.data()) == 0;
}

}  // namespace logging
