use bincode::config::Configuration;
use bincode::enc::write::Writer;
use bincode::enc::Encode;
use bincode::enc::{Encoder, EncoderImpl};
use bincode::error::EncodeError;
use cu29_clock::RobotClock;
#[cfg(debug_assertions)]
use cu29_intern_strs::read_interned_strings;
use cu29_log::CuLogEntry;
use cu29_traits::{CuResult, WriteStream};
#[cfg(debug_assertions)]
use log::{Log, Record};
use std::fmt::{Debug, Formatter};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
#[cfg(debug_assertions)]
use std::sync::Arc;
use std::sync::{Mutex, OnceLock};
static WRITER: OnceLock<(Mutex<Box<dyn WriteStream<CuLogEntry>>>, RobotClock)> = OnceLock::new();
#[cfg(debug_assertions)]
static EXTRA_TEXT_LOGGER: OnceLock<Option<ExtraTextLogger>> = OnceLock::new();
pub struct LoggerRuntime {}
impl LoggerRuntime {
    pub fn init(
        clock: RobotClock,
        destination: impl WriteStream<CuLogEntry> + 'static,
        #[allow(unused_variables)] extra_text_logger: Option<ExtraTextLogger>,
    ) -> Self {
        let runtime = LoggerRuntime {};
        if let Some((writer, _)) = WRITER.get() {
            let mut writer_guard = writer.lock().unwrap();
            *writer_guard = Box::new(destination);
        } else {
            WRITER
                .set((Mutex::new(Box::new(destination)), clock))
                .unwrap();
        }
        #[cfg(debug_assertions)]
        if let Some(logger) = extra_text_logger {
            let _ = EXTRA_TEXT_LOGGER.set(Some(logger));
        }
        runtime
    }
    pub fn flush(&self) {
        if let Some((writer, _clock)) = WRITER.get() {
            if let Ok(mut writer) = writer.lock() {
                if let Err(err) = writer.flush() {
                    eprintln!("cu29_log: Failed to flush writer: {}", err);
                }
            } else {
                eprintln!("cu29_log: Failed to lock writer.");
            }
        } else {
            eprintln!("cu29_log: Logger not initialized.");
        }
    }
}
impl Drop for LoggerRuntime {
    fn drop(&mut self) {
        self.flush();
    }
}
pub struct ExtraTextLogger {
    #[cfg(debug_assertions)]
    all_strings: Vec<String>,
    #[cfg(debug_assertions)]
    inner: Arc<dyn Log>,
}
#[cfg(debug_assertions)]
impl ExtraTextLogger {
    pub fn new(path_to_index: PathBuf, logger: Box<dyn Log>) -> Self {
        let all_strings = read_interned_strings(&path_to_index).unwrap();
        ExtraTextLogger {
            all_strings,
            inner: Arc::new(logger),
        }
    }
}
#[inline(always)]
pub fn log(mut entry: CuLogEntry) -> CuResult<()> {
    let d = WRITER.get().map(|(writer, clock)| (writer, clock));
    if d.is_none() {
        return Err("Logger not initialized.".into());
    }
    let (writer, clock) = d.unwrap();
    entry.time = clock.now();
    if let Err(err) = writer.lock().unwrap().log(&entry) {
        eprintln!("Failed to log data: {}", err);
    }
    #[cfg(debug_assertions)]
    {
        let guarded_logger = EXTRA_TEXT_LOGGER.get();
        if guarded_logger.is_none() {
            return Ok(());
        }
        if let Some(logger) = guarded_logger.unwrap() {
            let stringified = cu29_log::rebuild_logline(&logger.all_strings, &entry);
            match stringified {
                Ok(s) => {
                    let s = format!("[{}] {}", entry.time, s);
                    logger.inner.log(
                        &Record::builder()
                            .level(log::Level::Debug)
                            .args(format_args!("{}", s))
                            .build(),
                    ); }
                Err(e) => {
                    eprintln!("Failed to rebuild log line: {}", e);
                }
            }
        }
    }
    Ok(())
}
pub struct OwningIoWriter<W: Write> {
    writer: BufWriter<W>,
    bytes_written: usize,
}
impl<'a, W: Write> OwningIoWriter<W> {
    pub fn new(writer: W) -> Self {
        Self {
            writer: BufWriter::new(writer),
            bytes_written: 0,
        }
    }
    pub fn bytes_written(&self) -> usize {
        self.bytes_written
    }
    pub fn flush(&mut self) -> Result<(), EncodeError> {
        self.writer.flush().map_err(|inner| EncodeError::Io {
            inner,
            index: self.bytes_written,
        })
    }
}
impl<W: Write> Writer for OwningIoWriter<W> {
    #[inline(always)]
    fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
        self.writer
            .write_all(bytes)
            .map_err(|inner| EncodeError::Io {
                inner,
                index: self.bytes_written,
            })?;
        self.bytes_written += bytes.len();
        Ok(())
    }
}
pub struct SimpleFileWriter {
    path: PathBuf,
    encoder: EncoderImpl<OwningIoWriter<File>, Configuration>,
}
impl SimpleFileWriter {
    pub fn new(path: &PathBuf) -> CuResult<Self> {
        let file = std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .open(path)
            .map_err(|e| format!("Failed to open file: {:?}", e))?;
        let writer = OwningIoWriter::new(file);
        let encoder = EncoderImpl::new(writer, bincode::config::standard());
        Ok(SimpleFileWriter {
            path: path.clone(),
            encoder,
        })
    }
}
impl Debug for SimpleFileWriter {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "SimpleFileWriter for path {:?}", self.path)
    }
}
impl WriteStream<CuLogEntry> for SimpleFileWriter {
    #[inline(always)]
    fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
        obj.encode(&mut self.encoder)
            .map_err(|e| format!("Failed to write to file: {:?}", e))?;
        Ok(())
    }
    fn flush(&mut self) -> CuResult<()> {
        self.encoder
            .writer()
            .flush()
            .map_err(|e| format!("Failed to flush file: {:?}", e))?;
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use crate::CuLogEntry;
    use bincode::config::standard;
    use cu29_log::value::Value;
    use smallvec::smallvec;
    #[test]
    fn test_encode_decode_structured_log() {
        let log_entry = CuLogEntry {
            time: 0.into(),
            msg_index: 1,
            paramname_indexes: smallvec![2, 3],
            params: smallvec![Value::String("test".to_string())],
        };
        let encoded = bincode::encode_to_vec(&log_entry, standard()).unwrap();
        println!("{:?}", encoded);
        let decoded_tuple: (CuLogEntry, usize) =
            bincode::decode_from_slice(&encoded, standard()).unwrap();
        assert_eq!(log_entry, decoded_tuple.0);
    }
}