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
//! Cache functions, serde + serde_json
use std::fmt;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

#[cfg(unix)]
use std::os::unix::fs::MetadataExt;

#[cfg(windows)]
use std::os::windows::fs::MetadataExt;

use crate::colors::Colors;
use crate::config::Config;

use anyhow::{Result, Context};

/// Used to manage cache, rather than passing arguments in main() a lot
pub struct Cache {
    /// Path of the cache
    pub path: PathBuf,
}

/// Simply print the path when trying to display the [`Cache`] struct
impl fmt::Display for Cache {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.path.display())
    }
}

pub const CACHE_VER: &str = "1.2";

impl Cache {
    /// # Cache directory structure
    ///   1. Root, determined by OS
    ///   2. "wallust"
    ///   3. backend
    ///   4. colorspace
    ///   5. filter
    ///   6. threshold
    ///   7. saturation percentage (OPTIONAL)
    /// # File structure:
    ///   1. filename (no extentions)
    ///   2. size
    ///   3. inode number on Linux, file attributes on Windows
    ///   4. check-contrast -> "C_" if true, "" if false
    ///   5. [`CACHE_VER`]
    pub fn new(filename: &Path, c: &Config, cache_path: &Path) -> Result<Self> {


        // A possible solution to caching a checked/unchecked contrast without cache duplication and
        // possible efficiency loss
        // enum Contrast {
        //     Checked,
        //     Unchecked,
        //     UncheckedAndGood,
        // }

        let Some(name) = filename.file_name() else {
            anyhow::bail!("Using '..' as a parameter is not supported");
        };

        let sat = if let Some(s) = c.saturation {
            format!("saturation-{s}")
        } else {
            "".to_string()
        };


        //format!("{root}/wallust/{back}/{th}/{cs}/{filter}",
        let cachepath = Path::new(cache_path)
            .join("wallust")
            .join(c.backend.to_string())
            .join(c.color_space.to_string())
            .join(c.filter.to_string())
            .join(c.threshold.to_string())
            .join(sat)
        ;

        // Create cache dir (with all of it's parents)
        fs::create_dir_all(&cachepath)?;

        // get medatada
        let md = fs::metadata(filename)?;

        // use the ino number on *nix systems, and the "magick file number" on windows
        #[cfg(unix)]
        let num = md.ino();
        #[cfg(windows)]
        let num = md.file_attributes() ;

        // The following generates a hash name from a filename and it's `stat` attrs
        let hash_name = format!("{base}_{size}_{magic}_{con}{version}.json",
            base = name.to_string_lossy(),
            size = md.len(),
            magic = num,
            con = if c.check_contrast.unwrap_or(false) { "C_" } else { "" },
            version = CACHE_VER,
        );

        Ok(Self {
            path:
                cachepath.join(hash_name)
        })
    }

    /// Fetches values from a file present in cache
    pub fn read(&self) -> Result<Colors> {
        let contents = std::fs::read_to_string(&self.path)?;
        Ok(serde_json::from_str(&contents)?)
    }

    /// Write values to cache
    pub fn write(&self, colors: &Colors) -> Result<()> {
        Ok(File::create(&self.path)?
            .write_all(
                serde_json::to_string(colors)
                    .with_context(|| format!("Failed to deserilize from the json cached file: '{}':", &self))?
                .as_bytes()
            )?
        )
    }

    /// To determine whether to read from cache or to generate the colors from scratch
    pub fn is_cached(&self) -> bool {
        Path::new(&self.path).exists()
    }
}