1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
/*
 *   Copyright (c) 2022 R3BL LLC
 *   All rights reserved.
 *
 *   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.
 */

//! # How to log things, and simply use the logging facilities
//!
//! The simplest way to use this crate is to look at the [try_to_set_log_level] function
//! as the main entry point. By default, logging is disabled even if you call all the
//! functions in this module: [log_debug], [log_info], [log_trace], etc.
//!
//! Note that, although read and write methods require a `&mut File`, because of the
//! interfaces for `Read` and `Write`, the holder of a `&File` can still modify the file,
//! either through methods that take `&File` or by retrieving the underlying OS object and
//! modifying the file that way. Additionally, many operating systems allow concurrent
//! modification of files by different processes. Avoid assuming that holding a `&File`
//! means that the file will not change.
//!
//! # How to change how logging is implemented under the hood
//!
//! Under the hood the [`simplelog`](https://crates.io/crates/simplelog) crate is forked
//! and modified for use in the
//! [r3bl_simple_logger](https://crates.io/crates/r3bl_simple_logger) crate. For people
//! who want to work on changing the underlying behavior of the logging engine itself it
//! is best to look in that crate and make changes there.
//!

use std::{fs::File, sync::Once};

use chrono::Local;
use r3bl_simple_logger::*;
use time::UtcOffset;

use crate::*;

pub static mut LOG_LEVEL: LevelFilter = LevelFilter::Off;
pub static mut FILE_PATH: &str = "log.txt";
static mut FILE_LOGGER_INIT_OK: bool = false;
static FILE_LOGGER_INIT_FN: Once = Once::new();

const ENABLE_MULTITHREADED_LOG_WRITING: bool = false;

/// If you don't call this function w/ a value other than [LevelFilter::Off], then logging
/// is **DISABLED** by **default**. It won't matter if you call any of the other logging
/// functions in this module.
///
/// It does not matter how many times you call this function, it will only set the log
/// level once. If you want to change the default file that is used for logging take a
/// look at [try_to_set_log_file_path].
///
/// If you want to override the default log level [LOG_LEVEL], you can use this function. If the
/// logger has already been initialized, then it will return a [CommonErrorType::InvalidState]
/// error. To disable logging simply set the log level to [LevelFilter::Off].
///
/// If you would like to ignore the error just call `ok()` on the result that's returned. [More
/// info](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok).
pub fn try_to_set_log_level(level: LevelFilter) -> CommonResult<String> {
    unsafe {
        match FILE_LOGGER_INIT_OK {
            true => CommonError::new(
                CommonErrorType::InvalidState,
                "Logger already initialized, can't set log level",
            ),
            false => {
                LOG_LEVEL = level;
                Ok(level.to_string())
            }
        }
    }
}

/// Please take a look at [try_to_set_log_level] to enable or disable logging.
///
/// If you want to override the default log file path (stored in [FILE_PATH]), you can use this
/// function. If the logger has already been initialized, then it will return a
/// [CommonErrorType::InvalidState] error.
///
/// If you would like to ignore the error just call `ok()` on the result that's returned. [More
/// info](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok).
pub fn try_to_set_log_file_path(path: &'static str) -> CommonResult<String> {
    unsafe {
        match FILE_LOGGER_INIT_OK {
            true => CommonError::new(
                CommonErrorType::InvalidState,
                "Logger already initialized, can't set log file path",
            ),
            false => {
                FILE_PATH = path;
                Ok(path.to_string())
            }
        }
    }
}

/// Please take a look at [try_to_set_log_level] to enable or disable logging.
///
/// Log the message to the `INFO` log level using a file logger. There could be issues w/ accessing
/// this file; if it fails this function will not propagate the log error.
pub fn log_info(arg: String) {
    if init_file_logger_once().is_err() {
        eprintln!(
            "Error initializing file logger due to {}",
            init_file_logger_once().unwrap_err()
        );
    } else {
        match ENABLE_MULTITHREADED_LOG_WRITING {
            true => {
                std::thread::spawn(move || {
                    log::info!("{}", arg);
                });
            }
            false => {
                log::info!("{}", arg);
            }
        }
    }
}

/// Please take a look at [try_to_set_log_level] to enable or disable logging.
///
/// Log the message to the `DEBUG` log level using a file logger. There could be issues w/ accessing
/// this file; if it fails this function will not propagate the log error.
pub fn log_debug(arg: String) {
    if init_file_logger_once().is_err() {
        eprintln!(
            "Error initializing file logger due to {}",
            init_file_logger_once().unwrap_err()
        );
    } else {
        match ENABLE_MULTITHREADED_LOG_WRITING {
            true => {
                std::thread::spawn(move || {
                    log::debug!("{}", arg);
                });
            }
            false => {
                log::debug!("{}", arg);
            }
        }
    }
}

/// Please take a look at [try_to_set_log_level] to enable or disable logging.
///
/// Log the message to the `WARN` log level using a file logger. There could be issues w/ accessing
/// this file; if it fails this function will not propagate the log error.
pub fn log_warn(arg: String) {
    if init_file_logger_once().is_err() {
        eprintln!(
            "Error initializing file logger due to {}",
            init_file_logger_once().unwrap_err()
        );
    } else {
        match ENABLE_MULTITHREADED_LOG_WRITING {
            true => {
                std::thread::spawn(move || {
                    log::warn!("{}", arg);
                });
            }
            false => {
                log::warn!("{}", arg);
            }
        }
    }
}

/// Please take a look at [try_to_set_log_level] to enable or disable logging.
///
/// Log the message to the `TRACE` log level using a file logger. There could be issues w/ accessing
/// this file; if it fails this function will not propagate the log error.
pub fn log_trace(arg: String) {
    if init_file_logger_once().is_err() {
        eprintln!(
            "Error initializing file logger due to {}",
            init_file_logger_once().unwrap_err()
        );
    } else {
        match ENABLE_MULTITHREADED_LOG_WRITING {
            true => {
                std::thread::spawn(move || {
                    log::trace!("{}", arg);
                });
            }
            false => {
                log::trace!("{}", arg);
            }
        }
    }
}

/// Please take a look at [try_to_set_log_level] to enable or disable logging.
///
/// Log the message to the `ERROR` log level using a file logger. There could be issues w/ accessing
/// this file; if it fails this function will not propagate the log error.
pub fn log_error(arg: String) {
    if init_file_logger_once().is_err() {
        eprintln!(
            "Error initializing file logger due to {}",
            init_file_logger_once().unwrap_err()
        );
    } else {
        match ENABLE_MULTITHREADED_LOG_WRITING {
            true => {
                std::thread::spawn(move || {
                    log::error!("{}", arg);
                });
            }
            false => {
                log::error!("{}", arg);
            }
        }
    }
}

/// Simply open the file (location stored in [FILE_PATH] static above) and write the log message to
/// it. This will be opened once per session (i.e. program execution). It is destructively opened,
/// meaning that it will be rewritten when used in the next session.
///
/// # Docs
///
/// - Log
///   - [`CombinedLogger`], [`WriteLogger`], [`ConfigBuilder`]
/// - `format_description!`: <https://time-rs.github.io/book/api/format-description.html>
fn init_file_logger_once() -> CommonResult<()> {
    unsafe {
        if LOG_LEVEL == LevelFilter::Off {
            FILE_LOGGER_INIT_OK = true;
            return Ok(());
        }
    }

    // Run the lambda once & save bool to static `FILE_LOGGER_INIT_OK`.
    FILE_LOGGER_INIT_FN.call_once(actually_init_file_logger);

    // Use saved bool in static `FILE_LOGGER_INIT_OK` to throw error if needed.
    unsafe {
        return match FILE_LOGGER_INIT_OK {
            true => Ok(()),
            false => {
                let msg = format!("Failed to initialize file logger {FILE_PATH}");
                return CommonError::new(CommonErrorType::IOError, &msg);
            }
        };
    }

    /// [FILE_LOGGER_INIT_OK] is `false` at the start. Only this function (if it succeeds) can set
    /// that to `true`. This function does *not* panic if there's a problem initializing the logger.
    /// It just prints a message to stderr & returns.
    fn actually_init_file_logger() {
        unsafe {
            let maybe_new_file = File::create(FILE_PATH);
            if let Ok(new_file) = maybe_new_file {
                let config = new_config();
                let level = LOG_LEVEL;
                let file_logger = WriteLogger::new(level, config, new_file);
                let maybe_logger_init_err = CombinedLogger::init(vec![file_logger]);
                if let Err(e) = maybe_logger_init_err {
                    eprintln!("Failed to initialize file logger {FILE_PATH} due to {e}");
                } else {
                    FILE_LOGGER_INIT_OK = true
                }
            }
        }
    }

    /// Try to make a [`Config`] with local timezone offset. If that fails, return a default. The
    /// implementation used here works w/ Tokio.
    fn new_config() -> Config {
        let mut builder = ConfigBuilder::new();

        let offset_in_sec = Local::now().offset().local_minus_utc();
        let utc_offset_result = UtcOffset::from_whole_seconds(offset_in_sec);
        if let Ok(utc_offset) = utc_offset_result {
            builder.set_time_offset(utc_offset);
        }

        builder.set_time_format_custom(format_description!(
            "[hour repr:12]:[minute] [period]"
        ));

        builder.build()
    }
}