1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::OnceLock;
4
5use crate::data::{BodyRow, EngineRow, EuModelRow, ModelRow};
6use crate::maps::{FstMap, FstSet, data_dir};
7use crate::types::{BodyType, DriveType, FuelType, Transmission};
8
9pub struct Catalog {
15 makes: FstSet,
16 make_models: FstMap<ModelRow>,
17 eu_brand_models: Option<FstMap<EuModelRow>>,
18 eu_engines: Option<FstMap<EngineRow>>,
19 eu_body: Option<FstMap<BodyRow>>,
20 model_to_makes: OnceLock<HashMap<String, Vec<String>>>,
22}
23
24impl Catalog {
25 pub fn new() -> crate::Result<Self> {
27 let dir = data_dir();
28 #[cfg(feature = "embedded")]
29 crate::embedded::ensure_installed(&dir).ok();
30 Self::open(&dir)
31 }
32
33 pub fn open(dir: &Path) -> crate::Result<Self> {
36 let eu_brand_models = if dir.join("eu_brand_models.fst").exists() {
37 Some(FstMap::open(dir)?)
38 } else {
39 None
40 };
41 let eu_engines = if dir.join("eu_engines.fst").exists() {
42 Some(FstMap::open(dir)?)
43 } else {
44 None
45 };
46 let eu_body = if dir.join("eu_body.fst").exists() {
47 Some(FstMap::open(dir)?)
48 } else {
49 None
50 };
51 Ok(Catalog {
52 makes: FstSet::open(&dir.join("makes.fst"))?,
53 make_models: FstMap::open(dir)?,
54 eu_brand_models,
55 eu_engines,
56 eu_body,
57 model_to_makes: OnceLock::new(),
58 })
59 }
60
61 pub fn all_makes(&self) -> Vec<String> {
63 self.makes.keys()
64 }
65
66 pub fn has_make(&self, make: &str) -> bool {
68 self.makes.contains(&make.to_ascii_uppercase())
69 }
70
71 pub fn resolve_make(&self, raw: &str) -> Option<String> {
82 resolve_make_with(raw, |k| self.makes.contains(k))
83 }
84
85 pub fn make_count(&self) -> u64 {
87 self.makes.len()
88 }
89
90 pub fn models_for_make(&self, make: &str) -> Vec<String> {
95 let key = crate::decoder::normalize_make(make);
96 let mut models: Vec<String> = self
97 .make_models
98 .get(&key)
99 .map(|rows| rows.into_iter().map(|r| r.name).collect())
100 .unwrap_or_default();
101 if let Some(eu) = &self.eu_brand_models {
102 if let Some(rows) = eu.get(&key) {
103 for r in rows {
104 models.push(r.name);
105 }
106 }
107 }
108 models.sort();
109 models.dedup();
110 models
111 }
112
113 pub fn make_for_model(&self, model: &str) -> Vec<String> {
120 self.model_to_makes
121 .get_or_init(|| self.build_model_index())
122 .get(&model.to_ascii_uppercase())
123 .cloned()
124 .unwrap_or_default()
125 }
126
127 fn build_model_index(&self) -> HashMap<String, Vec<String>> {
128 let mut idx: HashMap<String, Vec<String>> = HashMap::new();
129 for make in self.make_models.keys() {
130 if let Some(rows) = self.make_models.get(&make) {
131 for r in rows {
132 idx.entry(r.name.to_ascii_uppercase())
133 .or_default()
134 .push(make.clone());
135 }
136 }
137 }
138 if let Some(eu) = &self.eu_brand_models {
139 for make in eu.keys() {
140 if let Some(rows) = eu.get(&make) {
141 for r in rows {
142 idx.entry(r.name.to_ascii_uppercase())
143 .or_default()
144 .push(make.clone());
145 }
146 }
147 }
148 }
149 for makes in idx.values_mut() {
150 makes.sort();
151 makes.dedup();
152 }
153 idx
154 }
155
156 pub fn eu_models_for(&self, brand: &str) -> Vec<EuModelRow> {
160 let key = crate::decoder::normalize_make(brand);
161 self.eu_brand_models
162 .as_ref()
163 .and_then(|m| m.get(&key))
164 .unwrap_or_default()
165 }
166
167 pub fn engines_for_brand(&self, brand: &str) -> Vec<EngineRow> {
170 let key = crate::decoder::normalize_make(brand);
171 self.eu_engines
172 .as_ref()
173 .and_then(|m| m.get(&key))
174 .unwrap_or_default()
175 }
176
177 pub fn engines_for(&self, brand: &str, model: &str) -> Vec<EngineRow> {
180 let model_key = model.to_ascii_uppercase();
181 self.engines_for_brand(brand)
182 .into_iter()
183 .filter(|r| r.model == model_key)
184 .collect()
185 }
186
187 pub fn body_for(&self, make: &str, model: &str) -> Vec<BodyType> {
194 let key = crate::decoder::normalize_make(make);
195 let model_key = model.to_ascii_uppercase();
196 self.eu_body
197 .as_ref()
198 .and_then(|m| m.get(&key))
199 .unwrap_or_default()
200 .into_iter()
201 .find(|r| r.model == model_key)
202 .map(|r| r.bodies)
203 .unwrap_or_default()
204 }
205
206 pub fn body_types() -> &'static [BodyType] {
210 crate::types::BODY_TYPES
211 }
212
213 pub fn fuel_types() -> &'static [FuelType] {
215 &[
216 FuelType::Gasoline,
217 FuelType::Diesel,
218 FuelType::Electric,
219 FuelType::Hybrid,
220 FuelType::PluginHybrid,
221 FuelType::Ethanol,
222 FuelType::FlexFuel,
223 FuelType::Cng,
224 FuelType::Lng,
225 FuelType::Lpg,
226 FuelType::Hydrogen,
227 FuelType::FuelCell,
228 FuelType::Methanol,
229 FuelType::NaturalGas,
230 FuelType::Other,
231 ]
232 }
233
234 pub fn drive_types() -> &'static [DriveType] {
236 &[
237 DriveType::Fwd,
238 DriveType::Rwd,
239 DriveType::Awd,
240 DriveType::FourWheelDrive,
241 DriveType::Other,
242 ]
243 }
244
245 pub fn transmissions() -> &'static [Transmission] {
247 &[
248 Transmission::Manual,
249 Transmission::Automatic,
250 Transmission::DualClutch,
251 Transmission::Cvt,
252 Transmission::SingleSpeed,
253 Transmission::AutomatedManual,
254 Transmission::Other,
255 ]
256 }
257}
258
259fn make_alias(upper: &str) -> Option<&'static str> {
263 Some(match upper {
264 "KG" | "KG MOBILITY" => "KGM",
265 "LINK" | "LYNK" | "LINK & CO" => "LYNK & CO",
266 "LEOPARD" | "BAO" => "FANGCHENGBAO",
267 "MHERO" | "MENGSHI" => "M-HERO",
268 "ZUNJIE" => "MAEXTRO",
269 "DFM" => "DONGFENG",
270 _ => return None,
271 })
272}
273
274fn resolve_make_with(raw: &str, contains: impl Fn(&str) -> bool) -> Option<String> {
277 let up = raw.trim().to_ascii_uppercase();
278 if up.is_empty() {
279 return None;
280 }
281 let try_one = |s: &str| -> Option<String> {
282 if contains(s) {
283 return Some(s.to_string());
284 }
285 match make_alias(s) {
286 Some(canon) if contains(canon) => Some(canon.to_string()),
287 _ => None,
288 }
289 };
290 if let Some(hit) = try_one(&up) {
291 return Some(hit);
292 }
293 let toks: Vec<&str> = up.split_whitespace().collect();
295 for n in (1..toks.len()).rev() {
296 if let Some(hit) = try_one(&toks[..n].join(" ")) {
297 return Some(hit);
298 }
299 }
300 None
301}
302
303#[cfg(test)]
304mod resolve_tests {
305 use super::resolve_make_with;
306 use std::collections::HashSet;
307
308 fn fake(items: &[&str]) -> HashSet<String> {
309 items.iter().map(|s| s.to_string()).collect()
310 }
311
312 #[test]
313 fn resolve_make_exact_alias_and_glued() {
314 let makes = fake(&[
315 "KGM",
316 "LYNK & CO",
317 "FANGCHENGBAO",
318 "DONGFENG",
319 "M-HERO",
320 "LINKTOUR",
321 ]);
322 let c = |k: &str| makes.contains(k);
323 assert_eq!(resolve_make_with("dongfeng", c).as_deref(), Some("DONGFENG"));
325 assert_eq!(resolve_make_with("KG", c).as_deref(), Some("KGM"));
327 assert_eq!(resolve_make_with("KG Mobility", c).as_deref(), Some("KGM"));
328 assert_eq!(resolve_make_with("LINK", c).as_deref(), Some("LYNK & CO"));
329 assert_eq!(resolve_make_with("Leopard", c).as_deref(), Some("FANGCHENGBAO"));
330 assert_eq!(resolve_make_with("BAO", c).as_deref(), Some("FANGCHENGBAO"));
331 assert_eq!(
333 resolve_make_with("LEOPARD 5", c).as_deref(),
334 Some("FANGCHENGBAO")
335 );
336 assert_eq!(
337 resolve_make_with("LINKTOUR ALUMI-PLUS", c).as_deref(),
338 Some("LINKTOUR")
339 );
340 assert_eq!(resolve_make_with("M-HERO 917", c).as_deref(), Some("M-HERO"));
341 assert_eq!(resolve_make_with("WALDO", c), None);
343 assert_eq!(resolve_make_with("", c), None);
344 }
345}