1use super::settings::get_devalang_homedir;
2use crate::config::driver::ProjectConfig;
3use crate::core::{
4 parser::statement::{Statement, StatementKind},
5 store::global::GlobalStore,
6};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::{
10 fs,
11 io::Write,
12 path::Path,
13 path::PathBuf,
14 sync::{Mutex, OnceLock},
15};
16
17#[derive(Debug, Deserialize, Clone, Serialize)]
18pub struct StatsCounts {
19 pub nb_files: usize,
20 pub nb_modules: usize,
21 pub nb_lines: usize,
22 pub nb_banks: usize,
23 pub nb_plugins: usize,
24}
25
26#[derive(Debug, Deserialize, Clone, Serialize)]
27pub struct StatsFeatures {
28 pub uses_imports: bool,
29 pub uses_functions: bool,
30 pub uses_groups: bool,
31 pub uses_automations: bool,
32 pub uses_loops: bool,
33}
34
35#[derive(Debug, Deserialize, Clone, Serialize)]
36pub struct StatsAudio {
37 pub avg_bpm: Option<u32>,
38 pub has_synths: bool,
39 pub has_samples: bool,
40}
41
42#[derive(Debug, Deserialize, Clone, Serialize)]
43pub struct ProjectStats {
44 pub counts: StatsCounts,
45 pub features: StatsFeatures,
46 pub audio: StatsAudio,
47}
48
49impl ProjectStats {
50 pub fn new() -> Self {
51 ProjectStats {
52 counts: StatsCounts {
53 nb_files: 0,
54 nb_modules: 0,
55 nb_lines: 0,
56 nb_banks: 0,
57 nb_plugins: 0,
58 },
59 features: StatsFeatures {
60 uses_imports: false,
61 uses_functions: false,
62 uses_groups: false,
63 uses_automations: false,
64 uses_loops: false,
65 },
66 audio: StatsAudio {
67 avg_bpm: None,
68 has_synths: false,
69 has_samples: false,
70 },
71 }
72 }
73
74 pub fn get() -> Result<Self, String> {
77 if let Some(stats) = get_memory_stats() {
78 return Ok(stats);
79 }
80 if let Some(stats) = load_from_file() {
81 return Ok(stats);
82 }
83 Ok(Self::new())
84 }
85
86 pub fn persist(&self) -> Result<(), String> {
88 save_to_file(self)
89 }
90}
91
92static STATS_MEMORY: OnceLock<Mutex<ProjectStats>> = OnceLock::new();
97
98fn stats_file_path() -> PathBuf {
99 get_devalang_homedir().join("stats.json")
100}
101
102pub fn set_memory_stats(stats: ProjectStats) {
103 let m = STATS_MEMORY.get_or_init(|| Mutex::new(ProjectStats::new()));
104 if let Ok(mut guard) = m.lock() {
105 *guard = stats;
106 }
107}
108
109pub fn get_memory_stats() -> Option<ProjectStats> {
110 let m = STATS_MEMORY.get_or_init(|| Mutex::new(ProjectStats::new()));
111 if let Ok(guard) = m.lock() {
112 return Some(guard.clone());
114 }
115 None
116}
117
118pub fn load_from_file() -> Option<ProjectStats> {
119 let path = stats_file_path();
120 if !path.exists() {
121 return None;
122 }
123 match fs::read_to_string(&path) {
124 Ok(content) => match serde_json::from_str::<ProjectStats>(&content) {
125 Ok(stats) => Some(stats),
126 Err(_) => None,
127 },
128 Err(_) => None,
129 }
130}
131
132pub fn save_to_file(stats: &ProjectStats) -> Result<(), String> {
133 let path = stats_file_path();
134 let dir = path
135 .parent()
136 .unwrap_or(&get_devalang_homedir())
137 .to_path_buf();
138 if !dir.exists() {
139 fs::create_dir_all(&dir).map_err(|e| format!("failed to create stats dir: {}", e))?;
140 }
141 let json = serde_json::to_string_pretty(stats)
142 .map_err(|e| format!("failed to serialize stats: {}", e))?;
143 let mut file =
144 fs::File::create(&path).map_err(|e| format!("failed to create stats file: {}", e))?;
145 file.write_all(json.as_bytes())
146 .map_err(|e| format!("failed to write stats file: {}", e))?;
147 Ok(())
148}
149
150pub fn compute_from(
152 statements_by_module: &HashMap<String, Vec<Statement>>,
153 global_store: &GlobalStore,
154 config: &Option<ProjectConfig>,
155 _output_dir: Option<&str>,
156) -> ProjectStats {
157 let mut stats = ProjectStats::new();
158
159 stats.counts.nb_modules = statements_by_module.len();
161 stats.counts.nb_files = stats.counts.nb_modules; let mut total_lines = 0usize;
164 for (path, _stmts) in statements_by_module.iter() {
165 let p = Path::new(path);
166 if let Ok(content) = fs::read_to_string(p) {
167 total_lines += content.lines().count();
168 }
169 }
170 stats.counts.nb_lines = total_lines;
171
172 if let Some(cfg) = config {
174 stats.counts.nb_banks = cfg.banks.as_ref().map(|v| v.len()).unwrap_or(0);
175 stats.counts.nb_plugins = cfg.plugins.as_ref().map(|v| v.len()).unwrap_or(0);
176 }
177
178 let mut bpm_sum: f32 = 0.0;
180 let mut bpm_count: usize = 0;
181
182 fn visit(
183 stmts: &[Statement],
184 acc: &mut ProjectStats,
185 bpm_sum: &mut f32,
186 bpm_count: &mut usize,
187 ) {
188 for s in stmts {
189 match &s.kind {
190 StatementKind::Import { .. } => acc.features.uses_imports = true,
191 StatementKind::Function { body, .. } => {
192 acc.features.uses_functions = true;
193 visit(body, acc, bpm_sum, bpm_count);
194 }
195 StatementKind::Group => {
196 acc.features.uses_groups = true;
197 if let Some(body) = extract_body_block(&s.value) {
198 visit(body, acc, bpm_sum, bpm_count);
199 }
200 }
201 StatementKind::Automate { .. } => acc.features.uses_automations = true,
202 StatementKind::Loop => {
203 acc.features.uses_loops = true;
204 if let Some(body) = extract_body_block(&s.value) {
205 visit(body, acc, bpm_sum, bpm_count);
206 }
207 }
208 StatementKind::Synth => acc.audio.has_synths = true,
209 StatementKind::Tempo => {
210 if let crate::core::shared::value::Value::Number(bpm) = &s.value {
211 *bpm_sum += *bpm as f32;
212 *bpm_count += 1;
213 }
214 }
215 StatementKind::Trigger { entity, .. } => {
216 let e = entity.to_lowercase();
217 if e.ends_with(".wav") || e.ends_with(".mp3") || e.contains("devalang://bank/")
218 {
219 acc.audio.has_samples = true;
220 }
221 }
222 _ => {}
223 }
224 }
225 }
226
227 fn extract_body_block(v: &crate::core::shared::value::Value) -> Option<&[Statement]> {
228 use crate::core::shared::value::Value;
229 if let Value::Map(map) = v {
230 if let Some(Value::Block(stmts)) = map.get("body") {
231 return Some(stmts.as_slice());
232 }
233 }
234 None
235 }
236
237 for (_path, stmts) in statements_by_module.iter() {
238 visit(stmts, &mut stats, &mut bpm_sum, &mut bpm_count);
239 }
240
241 if !stats.audio.has_samples {
242 for (_k, v) in global_store.variables.variables.iter() {
243 if let crate::core::shared::value::Value::String(s) = v {
244 if s.starts_with("devalang://bank/") {
245 stats.audio.has_samples = true;
246 break;
247 }
248 }
249 }
250 }
251
252 if bpm_count > 0 {
253 stats.audio.avg_bpm = Some((bpm_sum / bpm_count as f32).round() as u32);
254 }
255
256 stats
257}