flashpoint_archive/game/
search.rs

1use std::{collections::HashMap, fmt::Display, rc::Rc, hash::Hash};
2
3use fancy_regex::{Captures, Regex};
4use rusqlite::{
5    params,
6    types::{ToSqlOutput, Value, ValueRef},
7    Connection, OptionalExtension, Result, ToSql,
8};
9
10use crate::{debug_println, game::{ext::ExtSearchableType, get_game_add_apps}};
11
12use super::{ext::ExtSearchableRegistered, find_ext_data, get_game_data, get_game_platforms, get_game_tags, Game};
13
14#[derive(Debug, Clone)]
15pub enum SearchParam {
16    Boolean(bool),
17    String(String),
18    StringVec(Vec<String>),
19    Integer64(i64),
20    Float64(f64),
21    Value(serde_json::Value),
22}
23
24#[derive(Debug, Clone)]
25pub struct TagFilterInfo {
26    pub key: String,
27    pub dirty: bool,
28}
29
30impl ToSql for SearchParam {
31    fn to_sql(&self) -> Result<rusqlite::types::ToSqlOutput<'_>> {
32        match self {
33            SearchParam::Boolean(b) => Ok(ToSqlOutput::from(b.clone())),
34            SearchParam::String(s) => Ok(ToSqlOutput::from(s.as_str())),
35            SearchParam::StringVec(m) => {
36                let v: Rc<Vec<Value>> = Rc::new(
37                    m.iter()
38                        .map(|v| Value::from(v.clone()))
39                        .collect::<Vec<Value>>(),
40                );
41                Ok(ToSqlOutput::Array(v))
42            }
43            SearchParam::Integer64(i) => Ok(ToSqlOutput::from(i.clone())),
44            SearchParam::Float64(f) => Ok(ToSqlOutput::from(f.clone())),
45            SearchParam::Value(v) => match v {
46                serde_json::Value::Null => Ok(ToSqlOutput::Borrowed(ValueRef::Null)),
47                serde_json::Value::Number(n) if n.is_i64() => Ok(ToSqlOutput::from(n.as_i64().unwrap())),
48                serde_json::Value::Number(n) if n.is_f64() => Ok(ToSqlOutput::from(n.as_f64().unwrap())),
49                _ => serde_json::to_string(v)
50                    .map(ToSqlOutput::from)
51                    .map_err(|err| rusqlite::Error::ToSqlConversionFailure(err.into())),
52            },
53        }
54    }
55}
56
57impl Display for SearchParam {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            SearchParam::Boolean(b) => f.write_str(b.to_string().as_str()),
61            SearchParam::String(s) => f.write_str(s),
62            SearchParam::StringVec(m) => f.write_str(format!("{}", m.join("', '")).as_str()),
63            SearchParam::Integer64(i) => f.write_str(i.to_string().as_str()),
64            SearchParam::Float64(nf) => f.write_str(nf.to_string().as_str()),
65            SearchParam::Value(v) => f.write_str(serde_json::to_string(v).unwrap_or_default().as_str()),
66        }
67    }
68}
69
70#[cfg_attr(feature = "napi", napi(object))]
71#[derive(Debug, Clone)]
72pub struct GameSearch {
73    pub filter: GameFilter,
74    pub load_relations: GameSearchRelations,
75    pub custom_id_order: Option<Vec<String>>,
76    pub order: GameSearchOrder,
77    pub ext_order: Option<GameSearchOrderExt>,
78    pub offset: Option<GameSearchOffset>,
79    pub limit: i64,
80    pub slim: bool,
81    pub with_tag_filter: Option<Vec<String>>,
82}
83
84#[cfg_attr(feature = "napi", napi(object))]
85#[derive(Debug, Clone)]
86pub struct GameSearchOffset {
87    pub value: serde_json::Value,
88    pub title: String, // Secondary sort always
89    pub game_id: String,
90}
91
92#[cfg_attr(feature = "napi", napi(object))]
93#[derive(Debug, Clone)]
94pub struct GameSearchOrder {
95    pub column: GameSearchSortable,
96    pub direction: GameSearchDirection,
97}
98
99#[cfg_attr(feature = "napi", napi(object))]
100#[derive(Debug, Clone)]
101pub struct GameSearchOrderExt {
102    pub ext_id: String,
103    pub key: String,
104    pub default: serde_json::Value,
105}
106
107#[cfg_attr(feature = "napi", napi)]
108#[cfg_attr(not(feature = "napi"), derive(Clone))]
109#[derive(Debug, PartialEq)]
110pub enum GameSearchSortable {
111    TITLE,
112    DEVELOPER,
113    PUBLISHER,
114    SERIES,
115    PLATFORM,
116    DATEADDED,
117    DATEMODIFIED,
118    RELEASEDATE,
119    LASTPLAYED,
120    PLAYTIME,
121    RANDOM,
122    CUSTOM,
123}
124
125#[cfg_attr(feature = "napi", napi)]
126#[cfg_attr(not(feature = "napi"), derive(Clone))]
127#[derive(Debug)]
128pub enum GameSearchDirection {
129    ASC,
130    DESC,
131}
132
133#[cfg_attr(feature = "napi", napi(object))]
134#[derive(Debug, Clone)]
135pub struct GameSearchRelations {
136    pub tags: bool,
137    pub platforms: bool,
138    pub game_data: bool,
139    pub add_apps: bool,
140    pub ext_data: bool,
141}
142
143#[cfg_attr(feature = "napi", napi(object))]
144#[derive(Debug, Clone)]
145pub struct GameFilter {
146    pub subfilters: Vec<GameFilter>,
147    pub whitelist: FieldFilter,
148    pub blacklist: FieldFilter,
149    pub exact_whitelist: FieldFilter,
150    pub exact_blacklist: FieldFilter,
151    pub lower_than: SizeFilter,
152    pub higher_than: SizeFilter,
153    pub equal_to: SizeFilter,
154    pub bool_comp: BoolFilter,
155    pub match_any: bool,
156}
157
158#[cfg_attr(feature = "napi", napi(object))]
159#[derive(Debug, Clone)]
160pub struct FieldFilter {
161    pub id: Option<Vec<String>>,
162    pub generic: Option<Vec<String>>,
163    pub library: Option<Vec<String>>,
164    pub title: Option<Vec<String>>,
165    pub developer: Option<Vec<String>>,
166    pub publisher: Option<Vec<String>>,
167    pub series: Option<Vec<String>>,
168    pub tags: Option<Vec<String>>,
169    pub platforms: Option<Vec<String>>,
170    pub play_mode: Option<Vec<String>>,
171    pub status: Option<Vec<String>>,
172    pub notes: Option<Vec<String>>,
173    pub source: Option<Vec<String>>,
174    pub original_description: Option<Vec<String>>,
175    pub language: Option<Vec<String>>,
176    pub application_path: Option<Vec<String>>,
177    pub launch_command: Option<Vec<String>>,
178    pub ruffle_support: Option<Vec<String>>,
179    pub owner: Option<Vec<String>>,
180    pub ext: Option<HashMap<String, HashMap<String, Vec<String>>>>,
181}
182
183#[cfg_attr(feature = "napi", napi(object))]
184#[derive(Debug, Clone)]
185pub struct BoolFilter {
186    pub installed: Option<bool>,
187    pub ext: Option<HashMap<String, HashMap<String, bool>>>,
188}
189
190#[cfg_attr(feature = "napi", napi(object))]
191#[derive(Debug, Clone)]
192pub struct SizeFilter {
193    pub tags: Option<i64>,
194    pub platforms: Option<i64>,
195    pub date_added: Option<String>,
196    pub date_modified: Option<String>,
197    pub release_date: Option<String>,
198    pub game_data: Option<i64>,
199    pub add_apps: Option<i64>,
200    pub playtime: Option<i64>,
201    pub playcount: Option<i64>,
202    pub last_played: Option<String>,
203    pub ext: Option<HashMap<String, HashMap<String, i64>>>,
204}
205
206#[derive(Debug, Clone)]
207struct ForcedGameFilter {
208    pub whitelist: ForcedFieldFilter,
209    pub blacklist: ForcedFieldFilter,
210    pub exact_whitelist: ForcedFieldFilter,
211    pub exact_blacklist: ForcedFieldFilter,
212    pub lower_than: SizeFilter,
213    pub higher_than: SizeFilter,
214    pub equal_to: SizeFilter,
215    pub bool_comp: BoolFilter,
216}
217
218#[derive(Debug, Clone)]
219struct ForcedFieldFilter {
220    pub id: Vec<String>,
221    pub generic: Vec<String>,
222    pub library: Vec<String>,
223    pub title: Vec<String>,
224    pub developer: Vec<String>,
225    pub publisher: Vec<String>,
226    pub series: Vec<String>,
227    pub tags: Vec<String>,
228    pub platforms: Vec<String>,
229    pub play_mode: Vec<String>,
230    pub status: Vec<String>,
231    pub notes: Vec<String>,
232    pub source: Vec<String>,
233    pub original_description: Vec<String>,
234    pub language: Vec<String>,
235    pub application_path: Vec<String>,
236    pub launch_command: Vec<String>,
237    pub ruffle_support: Vec<String>,
238    pub owner: Vec<String>,
239    pub ext: HashMap<String, HashMap<String, Vec<String>>>,
240}
241
242#[cfg_attr(feature = "napi", napi(object))]
243#[derive(Debug, Clone)]
244pub struct PageTuple {
245    pub id: String,
246    pub order_val: serde_json::Value,
247    pub title: String,
248}
249
250impl Default for GameSearch {
251    fn default() -> Self {
252        GameSearch {
253            filter: GameFilter::default(),
254            load_relations: GameSearchRelations::default(),
255            order: GameSearchOrder {
256                column: GameSearchSortable::TITLE,
257                direction: GameSearchDirection::ASC,
258            },
259            custom_id_order: None,
260            ext_order: None,
261            offset: None,
262            limit: 1000,
263            slim: false,
264            with_tag_filter: None,
265        }
266    }
267}
268
269impl Default for GameFilter {
270    fn default() -> Self {
271        GameFilter {
272            subfilters: vec![],
273            whitelist: FieldFilter::default(),
274            blacklist: FieldFilter::default(),
275            exact_whitelist: FieldFilter::default(),
276            exact_blacklist: FieldFilter::default(),
277            lower_than: SizeFilter::default(),
278            higher_than: SizeFilter::default(),
279            equal_to: SizeFilter::default(),
280            bool_comp: BoolFilter::default(),
281            match_any: false,
282        }
283    }
284}
285
286impl Default for GameSearchRelations {
287    fn default() -> Self {
288        GameSearchRelations {
289            tags: false,
290            platforms: false,
291            game_data: false,
292            add_apps: false,
293            ext_data: true,
294        }
295    }
296}
297
298impl Default for FieldFilter {
299    fn default() -> Self {
300        FieldFilter {
301            id: None,
302            generic: None,
303            library: None,
304            title: None,
305            developer: None,
306            publisher: None,
307            series: None,
308            tags: None,
309            platforms: None,
310            play_mode: None,
311            status: None,
312            notes: None,
313            source: None,
314            original_description: None,
315            language: None,
316            application_path: None,
317            launch_command: None,
318            ruffle_support: None,
319            owner: None,
320            ext: None,
321        }
322    }
323}
324
325impl Default for ForcedGameFilter {
326    fn default() -> Self {
327        ForcedGameFilter {
328            whitelist: ForcedFieldFilter::default(),
329            blacklist: ForcedFieldFilter::default(),
330            exact_whitelist: ForcedFieldFilter::default(),
331            exact_blacklist: ForcedFieldFilter::default(),
332            lower_than: SizeFilter::default(),
333            higher_than: SizeFilter::default(),
334            equal_to: SizeFilter::default(),
335            bool_comp: BoolFilter::default(),
336        }
337    }
338}
339
340impl Default for ForcedFieldFilter {
341    fn default() -> Self {
342        ForcedFieldFilter {
343            id: vec![],
344            generic: vec![],
345            library: vec![],
346            title: vec![],
347            developer: vec![],
348            publisher: vec![],
349            series: vec![],
350            tags: vec![],
351            platforms: vec![],
352            play_mode: vec![],
353            status: vec![],
354            notes: vec![],
355            source: vec![],
356            original_description: vec![],
357            language: vec![],
358            application_path: vec![],
359            launch_command: vec![],
360            ruffle_support: vec![],
361            owner: vec![],
362            ext: HashMap::default(),
363        }
364    }
365}
366
367impl Default for SizeFilter {
368    fn default() -> Self {
369        return SizeFilter {
370            tags: None,
371            platforms: None,
372            date_added: None,
373            date_modified: None,
374            release_date: None,
375            game_data: None,
376            add_apps: None,
377            playtime: None,
378            playcount: None,
379            last_played: None,
380            ext: None,
381        };
382    }
383}
384
385impl Default for BoolFilter {
386    fn default() -> Self {
387        return BoolFilter {
388            installed: None,
389            ext: None,
390        };
391    }
392}
393
394impl From<&ForcedGameFilter> for GameFilter {
395    fn from(value: &ForcedGameFilter) -> Self {
396        let mut search = GameFilter::default();
397
398        // Whitelist
399
400        if value.whitelist.id.len() > 0 {
401            search.whitelist.id = Some(value.whitelist.id.clone());
402        }
403        if value.whitelist.generic.len() > 0 {
404            search.whitelist.generic = Some(value.whitelist.generic.clone());
405        }
406        if value.whitelist.title.len() > 0 {
407            search.whitelist.title = Some(value.whitelist.title.clone());
408        }
409        if value.whitelist.developer.len() > 0 {
410            search.whitelist.developer = Some(value.whitelist.developer.clone());
411        }
412        if value.whitelist.publisher.len() > 0 {
413            search.whitelist.publisher = Some(value.whitelist.publisher.clone());
414        }
415        if value.whitelist.series.len() > 0 {
416            search.whitelist.series = Some(value.whitelist.series.clone());
417        }
418        if value.whitelist.tags.len() > 0 {
419            search.whitelist.tags = Some(value.whitelist.tags.clone());
420        }
421        if value.whitelist.platforms.len() > 0 {
422            search.whitelist.platforms = Some(value.whitelist.platforms.clone());
423        }
424        if value.whitelist.play_mode.len() > 0 {
425            search.whitelist.play_mode = Some(value.whitelist.play_mode.clone());
426        }
427        if value.whitelist.status.len() > 0 {
428            search.whitelist.status = Some(value.whitelist.status.clone());
429        }
430        if value.whitelist.notes.len() > 0 {
431            search.whitelist.notes = Some(value.whitelist.notes.clone());
432        }
433        if value.whitelist.source.len() > 0 {
434            search.whitelist.source = Some(value.whitelist.source.clone());
435        }
436        if value.whitelist.original_description.len() > 0 {
437            search.whitelist.original_description =
438                Some(value.whitelist.original_description.clone());
439        }
440        if value.whitelist.language.len() > 0 {
441            search.whitelist.language = Some(value.whitelist.language.clone());
442        }
443        if value.whitelist.application_path.len() > 0 {
444            search.whitelist.application_path = Some(value.whitelist.application_path.clone());
445        }
446        if value.whitelist.launch_command.len() > 0 {
447            search.whitelist.launch_command = Some(value.whitelist.launch_command.clone());
448        }
449        if value.whitelist.ruffle_support.len() > 0 {
450            search.whitelist.ruffle_support = Some(value.whitelist.ruffle_support.clone());
451        }
452        if value.whitelist.owner.len() > 0 {
453            search.whitelist.owner = Some(value.whitelist.owner.clone());
454        }
455        if value.whitelist.ext.len() > 0 {
456            search.whitelist.ext = Some(value.whitelist.ext.clone());
457        }
458
459        // Blacklist
460
461        if value.blacklist.id.len() > 0 {
462            search.blacklist.id = Some(value.blacklist.id.clone());
463        }
464        if value.blacklist.generic.len() > 0 {
465            search.blacklist.generic = Some(value.blacklist.generic.clone());
466        }
467        if value.blacklist.title.len() > 0 {
468            search.blacklist.title = Some(value.blacklist.title.clone());
469        }
470        if value.blacklist.developer.len() > 0 {
471            search.blacklist.developer = Some(value.blacklist.developer.clone());
472        }
473        if value.blacklist.publisher.len() > 0 {
474            search.blacklist.publisher = Some(value.blacklist.publisher.clone());
475        }
476        if value.blacklist.series.len() > 0 {
477            search.blacklist.series = Some(value.blacklist.series.clone());
478        }
479        if value.blacklist.tags.len() > 0 {
480            search.blacklist.tags = Some(value.blacklist.tags.clone());
481        }
482        if value.blacklist.platforms.len() > 0 {
483            search.blacklist.platforms = Some(value.blacklist.platforms.clone());
484        }
485        if value.blacklist.play_mode.len() > 0 {
486            search.blacklist.play_mode = Some(value.blacklist.play_mode.clone());
487        }
488        if value.blacklist.status.len() > 0 {
489            search.blacklist.status = Some(value.blacklist.status.clone());
490        }
491        if value.blacklist.notes.len() > 0 {
492            search.blacklist.notes = Some(value.blacklist.notes.clone());
493        }
494        if value.blacklist.source.len() > 0 {
495            search.blacklist.source = Some(value.blacklist.source.clone());
496        }
497        if value.blacklist.original_description.len() > 0 {
498            search.blacklist.original_description =
499                Some(value.blacklist.original_description.clone());
500        }
501        if value.blacklist.language.len() > 0 {
502            search.blacklist.language = Some(value.blacklist.language.clone());
503        }
504        if value.blacklist.application_path.len() > 0 {
505            search.blacklist.application_path = Some(value.blacklist.application_path.clone());
506        }
507        if value.blacklist.launch_command.len() > 0 {
508            search.blacklist.launch_command = Some(value.blacklist.launch_command.clone());
509        }
510        if value.blacklist.ruffle_support.len() > 0 {
511            search.blacklist.ruffle_support = Some(value.blacklist.ruffle_support.clone());
512        }
513        if value.blacklist.owner.len() > 0 {
514            search.blacklist.owner = Some(value.blacklist.owner.clone());
515        }
516        if value.blacklist.ext.len() > 0 {
517            search.blacklist.ext = Some(value.blacklist.ext.clone());
518        }
519
520        // Exact whitelist
521
522        if value.exact_whitelist.id.len() > 0 {
523            search.exact_whitelist.id = Some(value.exact_whitelist.id.clone());
524        }
525        if value.exact_whitelist.generic.len() > 0 {
526            search.exact_whitelist.generic = Some(value.exact_whitelist.generic.clone());
527        }
528        if value.exact_whitelist.title.len() > 0 {
529            search.exact_whitelist.title = Some(value.exact_whitelist.title.clone());
530        }
531        if value.exact_whitelist.developer.len() > 0 {
532            search.exact_whitelist.developer = Some(value.exact_whitelist.developer.clone());
533        }
534        if value.exact_whitelist.publisher.len() > 0 {
535            search.exact_whitelist.publisher = Some(value.exact_whitelist.publisher.clone());
536        }
537        if value.exact_whitelist.series.len() > 0 {
538            search.exact_whitelist.series = Some(value.exact_whitelist.series.clone());
539        }
540        if value.exact_whitelist.tags.len() > 0 {
541            search.exact_whitelist.tags = Some(value.exact_whitelist.tags.clone());
542        }
543        if value.exact_whitelist.platforms.len() > 0 {
544            search.exact_whitelist.platforms = Some(value.exact_whitelist.platforms.clone());
545        }
546        if value.exact_whitelist.play_mode.len() > 0 {
547            search.exact_whitelist.play_mode = Some(value.exact_whitelist.play_mode.clone());
548        }
549        if value.exact_whitelist.status.len() > 0 {
550            search.exact_whitelist.status = Some(value.exact_whitelist.status.clone());
551        }
552        if value.exact_whitelist.notes.len() > 0 {
553            search.exact_whitelist.notes = Some(value.exact_whitelist.notes.clone());
554        }
555        if value.exact_whitelist.source.len() > 0 {
556            search.exact_whitelist.source = Some(value.exact_whitelist.source.clone());
557        }
558        if value.exact_whitelist.original_description.len() > 0 {
559            search.exact_whitelist.original_description =
560                Some(value.exact_whitelist.original_description.clone());
561        }
562        if value.exact_whitelist.language.len() > 0 {
563            search.exact_whitelist.language = Some(value.exact_whitelist.language.clone());
564        }
565        if value.exact_whitelist.application_path.len() > 0 {
566            search.exact_whitelist.application_path =
567                Some(value.exact_whitelist.application_path.clone());
568        }
569        if value.exact_whitelist.launch_command.len() > 0 {
570            search.exact_whitelist.launch_command =
571                Some(value.exact_whitelist.launch_command.clone());
572        }
573        if value.exact_whitelist.ruffle_support.len() > 0 {
574            search.exact_whitelist.ruffle_support =
575                Some(value.exact_whitelist.ruffle_support.clone());
576        }
577        if value.exact_whitelist.owner.len() > 0 {
578            search.exact_whitelist.owner = Some(value.exact_whitelist.owner.clone());
579        }
580        if value.exact_whitelist.ext.len() > 0 {
581            search.exact_whitelist.ext = Some(value.exact_whitelist.ext.clone());
582        }
583
584        // Exact blacklist
585
586        if value.exact_blacklist.id.len() > 0 {
587            search.exact_blacklist.id = Some(value.exact_blacklist.id.clone());
588        }
589        if value.exact_blacklist.generic.len() > 0 {
590            search.exact_blacklist.generic = Some(value.exact_blacklist.generic.clone());
591        }
592        if value.exact_blacklist.title.len() > 0 {
593            search.exact_blacklist.title = Some(value.exact_blacklist.title.clone());
594        }
595        if value.exact_blacklist.developer.len() > 0 {
596            search.exact_blacklist.developer = Some(value.exact_blacklist.developer.clone());
597        }
598        if value.exact_blacklist.publisher.len() > 0 {
599            search.exact_blacklist.publisher = Some(value.exact_blacklist.publisher.clone());
600        }
601        if value.exact_blacklist.series.len() > 0 {
602            search.exact_blacklist.series = Some(value.exact_blacklist.series.clone());
603        }
604        if value.exact_blacklist.tags.len() > 0 {
605            search.exact_blacklist.tags = Some(value.exact_blacklist.tags.clone());
606        }
607        if value.exact_blacklist.platforms.len() > 0 {
608            search.exact_blacklist.platforms = Some(value.exact_blacklist.platforms.clone());
609        }
610        if value.exact_blacklist.play_mode.len() > 0 {
611            search.exact_blacklist.play_mode = Some(value.exact_blacklist.play_mode.clone());
612        }
613        if value.exact_blacklist.status.len() > 0 {
614            search.exact_blacklist.status = Some(value.exact_blacklist.status.clone());
615        }
616        if value.exact_blacklist.notes.len() > 0 {
617            search.exact_blacklist.notes = Some(value.exact_blacklist.notes.clone());
618        }
619        if value.exact_blacklist.source.len() > 0 {
620            search.exact_blacklist.source = Some(value.exact_blacklist.source.clone());
621        }
622        if value.exact_blacklist.original_description.len() > 0 {
623            search.exact_blacklist.original_description =
624                Some(value.exact_blacklist.original_description.clone());
625        }
626        if value.exact_blacklist.language.len() > 0 {
627            search.exact_blacklist.language = Some(value.exact_blacklist.language.clone());
628        }
629        if value.exact_blacklist.application_path.len() > 0 {
630            search.exact_blacklist.application_path =
631                Some(value.exact_blacklist.application_path.clone());
632        }
633        if value.exact_blacklist.launch_command.len() > 0 {
634            search.exact_blacklist.launch_command =
635                Some(value.exact_blacklist.launch_command.clone());
636        }
637        if value.exact_blacklist.ruffle_support.len() > 0 {
638            search.exact_blacklist.ruffle_support =
639                Some(value.exact_blacklist.ruffle_support.clone());
640        }
641        if value.exact_blacklist.owner.len() > 0 {
642            search.exact_blacklist.owner = Some(value.exact_blacklist.owner.clone());
643        }
644        if value.exact_blacklist.ext.len() > 0 {
645            search.exact_blacklist.ext = Some(value.exact_blacklist.ext.clone());
646        }
647
648        search.higher_than = value.higher_than.clone();
649        search.lower_than = value.lower_than.clone();
650        search.equal_to = value.equal_to.clone();
651        search.bool_comp = value.bool_comp.clone();
652
653        search
654    }
655}
656
657pub trait InsertOrGet<K: Eq + Hash, V: Default> {
658    fn insert_or_get(&mut self, item: K) -> &mut V;
659}
660
661impl<K: Eq + Hash, V: Default> InsertOrGet<K, V> for HashMap<K, V> {
662    fn insert_or_get(&mut self, item: K) -> &mut V {
663        return match self.entry(item) {
664            std::collections::hash_map::Entry::Occupied(o) => o.into_mut(),
665            std::collections::hash_map::Entry::Vacant(v) => v.insert(V::default()),
666        };
667    }
668}
669
670macro_rules! whitelist_clause {
671    ($func:ident, $field_name:expr, $filter:expr) => {
672        $func($field_name, $filter, false, false)
673    };
674}
675
676macro_rules! blacklist_clause {
677    ($func:ident, $field_name:expr, $filter:expr) => {
678        $func($field_name, $filter, false, true)
679    };
680}
681
682macro_rules! exact_whitelist_clause {
683    ($func:ident, $field_name:expr, $filter:expr) => {
684        $func($field_name, $filter, true, false)
685    };
686}
687
688macro_rules! exact_blacklist_clause {
689    ($func:ident, $field_name:expr, $filter:expr) => {
690        $func($field_name, $filter, true, true)
691    };
692}
693
694const COUNT_QUERY: &str = "SELECT COUNT(*) FROM game";
695
696const RESULTS_QUERY: &str =
697    "SELECT game.id, title, alternateTitles, series, developer, publisher, platformsStr, \
698platformName, dateAdded, dateModified, broken, extreme, playMode, status, notes, \
699tagsStr, source, applicationPath, launchCommand, releaseDate, version, \
700originalDescription, language, activeDataId, activeDataOnDisk, lastPlayed, playtime, \
701activeGameConfigId, activeGameConfigOwner, archiveState, library, playCounter, logoPath, screenshotPath, ruffleSupport, \
702owner FROM game";
703
704const SLIM_RESULTS_QUERY: &str =
705    "SELECT game.id, title, series, developer, publisher, platformsStr, 
706platformName, tagsStr, library, logoPath, screenshotPath, owner
707FROM game";
708
709const TAG_FILTER_INDEX_QUERY: &str = "INSERT INTO tag_filter_index (id) SELECT game.id FROM game";
710
711pub fn search_index(
712    conn: &Connection,
713    search: &mut GameSearch,
714    limit: Option<i64>,
715) -> Result<Vec<PageTuple>> {
716    // Allow use of rarray() in SQL queries
717    rusqlite::vtab::array::load_module(conn)?;
718
719    // Update tag filter indexing
720    if let Some(tags) = &search.with_tag_filter {
721        if tags.len() > 0 {
722            let mut filtered_search = GameSearch::default();
723            filtered_search.limit = 999999999;
724            filtered_search.filter.exact_blacklist.tags = Some(tags.to_vec());
725            filtered_search.filter.match_any = true;
726            new_tag_filter_index(conn, &mut filtered_search)?;
727        }
728    }
729
730    if search.order.column == GameSearchSortable::CUSTOM {
731        if let Some(custom_id_order) = &search.custom_id_order {
732            if custom_id_order.len() > 0 {
733                new_custom_id_order(conn, custom_id_order.clone())?;
734            }
735        }
736    }
737
738    let order_column = match search.order.column {
739        GameSearchSortable::TITLE => "game.title",
740        GameSearchSortable::DEVELOPER => "game.developer",
741        GameSearchSortable::PUBLISHER => "game.publisher",
742        GameSearchSortable::SERIES => "game.series",
743        GameSearchSortable::PLATFORM => "game.platformName",
744        GameSearchSortable::DATEADDED => "game.dateAdded",
745        GameSearchSortable::DATEMODIFIED => "game.dateModified",
746        GameSearchSortable::RELEASEDATE => "game.releaseDate",
747        GameSearchSortable::LASTPLAYED => "game.lastPlayed",
748        GameSearchSortable::PLAYTIME => "game.playtime",
749        GameSearchSortable::CUSTOM => "RowNum",
750        _ => "unknown",
751    };
752    let order_direction = match search.order.direction {
753        GameSearchDirection::ASC => "ASC",
754        GameSearchDirection::DESC => "DESC",
755    };
756    let page_size = search.limit;
757    search.limit = limit.or_else(|| Some(999999999)).unwrap();
758    let selection = match &search.ext_order {
759        Some(ext_order) => format!("
760            WITH OrderedExt AS (
761                SELECT
762                    gameId AS id,
763                    COALESCE(JSON_EXTRACT(data, '$.{}'), {}) AS ExtValue
764                FROM ext_data
765                WHERE extId = '{}'
766            )
767            SELECT 
768                game.id, 
769                OrderedExt.ExtValue, 
770                game.title, 
771                ROW_NUMBER() OVER (ORDER BY OrderedExt.ExtValue, game.title, game.id) AS rn 
772            FROM game", 
773            ext_order.key, ext_order.default.to_string(), ext_order.ext_id),
774        None => match search.order.column {
775            GameSearchSortable::CUSTOM => "
776            WITH OrderedIDs AS (
777                SELECT
778                id,
779                ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum
780                FROM custom_id_order
781            ) 
782            SELECT game.id, OrderedIDs.RowNum, game.title, ROW_NUMBER() OVER (ORDER BY OrderedIDs.RowNum, game.title, game.id) AS rn FROM game".to_owned(),
783            _ => format!("SELECT game.id, {}, game.title, ROW_NUMBER() OVER (ORDER BY {} COLLATE NOCASE {}, game.title {}, game.id) AS rn FROM game", order_column, order_column, order_direction, order_direction)
784        }
785    };
786
787    // Override ordering for ext sorts
788    let adjusted_order_column = match &search.ext_order {
789        Some(_) => "ExtValue",
790        None => order_column
791    };
792
793    let (mut query, mut params) = build_search_query(search, &selection);
794    
795    // Add the weirdness
796    query = format!(
797        "SELECT game.id, {}, game.title FROM ({}) game WHERE rn % ? = 0",
798        adjusted_order_column, query
799    );
800    params.push(SearchParam::String(page_size.to_string()));
801
802    let params_as_refs: Vec<&dyn rusqlite::ToSql> =
803        params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
804
805    let mut keyset = vec![];
806    debug_println!(
807        "search index query - \n{}",
808        format_query(&query, params.clone())
809    );
810    let mut stmt = conn.prepare(&query)?;
811    let page_tuple_iter = stmt.query_map(params_as_refs.as_slice(), |row| {
812        let order_val = match row.get::<_, Option<Value>>(1)? {
813            Some(value) => value,
814            None => Value::Text("".to_string()), // Handle NULL as you see fit
815        };
816        Ok(PageTuple {
817            id: row.get(0)?,
818            order_val: match order_val {
819                Value::Text(v) => serde_json::Value::String(v),
820                Value::Integer(v) => serde_json::Value::Number(v.into()),
821                Value::Real(v) => serde_json::Value::Number(
822                    serde_json::Number::from_f64(v).unwrap_or_else(|| serde_json::Number::from(0))
823                ),
824                _ => serde_json::Value::Null
825            },
826            title: row.get(2)?,
827        })
828    })?;
829    for page_tuple in page_tuple_iter {
830        keyset.push(page_tuple?);
831    }
832    Ok(keyset)
833}
834
835pub fn search_count(conn: &Connection, search: &GameSearch) -> Result<i64> {
836    // Allow use of rarray() in SQL queries
837    rusqlite::vtab::array::load_module(conn)?;
838
839    let mut selection = COUNT_QUERY.to_owned();
840    if let Some(ext_order) = &search.ext_order {
841        selection = format!("WITH OrderedExt AS (
842            SELECT
843                gameId AS id,
844                COALESCE(JSON_EXTRACT(data, '$.{}'), {}) AS ExtValue
845            FROM ext_data
846            WHERE extId = '{}'
847        ) ", ext_order.key, ext_order.default.to_string(), ext_order.ext_id)
848            + &selection;
849    } else if search.order.column == GameSearchSortable::CUSTOM {
850        selection = "WITH OrderedIDs AS (
851            SELECT
852            id,
853            ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum
854            FROM custom_id_order
855        ) "
856        .to_owned()
857            + &selection;
858    }
859    
860    let (query, params) = build_search_query(search, &selection);
861    debug_println!(
862        "search count query - \n{}",
863        format_query(&query, params.clone())
864    );
865
866    let params_as_refs: Vec<&dyn rusqlite::ToSql> =
867        params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
868
869    let count_result = conn
870        .query_row(&query, params_as_refs.as_slice(), |row| {
871            row.get::<_, i64>(0)
872        })
873        .optional()?;
874
875    match count_result {
876        Some(count) => Ok(count),
877        None => Ok(0),
878    }
879}
880
881pub fn search_custom<T, F>(
882    conn: &Connection,
883    search: &GameSearch,
884    selection: &str,
885    game_map_closure: F,
886) -> Result<Vec<T>>
887where
888    F: Fn(&rusqlite::Row<'_>) -> Result<T>,
889{
890    // Allow use of rarray() in SQL queries
891    rusqlite::vtab::array::load_module(conn)?;
892
893    let (query, params) = build_search_query(search, selection);
894    debug_println!("search query - \n{}", format_query(&query, params.clone()));
895
896    // Convert the parameters array to something rusqlite understands
897    let params_as_refs: Vec<&dyn rusqlite::ToSql> =
898        params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
899
900    let mut results = Vec::new();
901
902    let mut stmt = conn.prepare(query.as_str())?;
903    let row_iter = stmt.query_map(params_as_refs.as_slice(), game_map_closure)?;
904
905    for result in row_iter {
906        results.push(result?);
907    }
908
909    Ok(results)
910}
911
912// The search function that takes a connection and a GameSearch object
913pub fn search(conn: &Connection, search: &GameSearch) -> Result<Vec<Game>> {
914    let mut selection = match search.slim {
915        true => SLIM_RESULTS_QUERY.to_owned(),
916        false => RESULTS_QUERY.to_owned(),
917    };
918    if let Some(ext_order) = &search.ext_order {
919        selection = format!("WITH OrderedExt AS (
920            SELECT
921                gameId AS id,
922                COALESCE(JSON_EXTRACT(data, '$.{}'), {}) AS ExtValue
923            FROM ext_data
924            WHERE extId = '{}'
925        ) ", ext_order.key, ext_order.default.to_string(), ext_order.ext_id)
926            + &selection;
927    } else if search.order.column == GameSearchSortable::CUSTOM {
928        selection = "WITH OrderedIDs AS (
929            SELECT
930            id,
931            ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum
932            FROM custom_id_order
933        ) "
934        .to_owned()
935            + &selection;
936    }
937
938    let game_map_closure = match search.slim {
939        true => |row: &rusqlite::Row<'_>| -> Result<Game> {
940            Ok(Game {
941                id: row.get(0)?,
942                title: row.get(1)?,
943                series: row.get(2)?,
944                developer: row.get(3)?,
945                publisher: row.get(4)?,
946                platforms: row.get(5)?,
947                primary_platform: row.get(6)?,
948                tags: row.get(7)?,
949                library: row.get(8)?,
950                logo_path: row.get(9)?,
951                screenshot_path: row.get(10)?,
952                owner: row.get(11)?,
953                ..Default::default()
954            })
955        },
956        false => |row: &rusqlite::Row<'_>| -> Result<Game> {
957            Ok(Game {
958                id: row.get(0)?,
959                title: row.get(1)?,
960                alternate_titles: row.get(2)?,
961                series: row.get(3)?,
962                developer: row.get(4)?,
963                publisher: row.get(5)?,
964                platforms: row.get(6)?,
965                primary_platform: row.get(7)?,
966                date_added: row.get(8)?,
967                date_modified: row.get(9)?,
968                legacy_broken: row.get(10)?,
969                legacy_extreme: row.get(11)?,
970                play_mode: row.get(12)?,
971                status: row.get(13)?,
972                notes: row.get(14)?,
973                tags: row.get(15)?,
974                source: row.get(16)?,
975                legacy_application_path: row.get(17)?,
976                legacy_launch_command: row.get(18)?,
977                release_date: row.get(19)?,
978                version: row.get(20)?,
979                original_description: row.get(21)?,
980                language: row.get(22)?,
981                active_data_id: row.get(23)?,
982                active_data_on_disk: row.get(24)?,
983                last_played: row.get(25)?,
984                playtime: row.get(26)?,
985                active_game_config_id: row.get(27)?,
986                active_game_config_owner: row.get(28)?,
987                archive_state: row.get(29)?,
988                library: row.get(30)?,
989                play_counter: row.get(31)?,
990                detailed_platforms: None,
991                detailed_tags: None,
992                game_data: None,
993                add_apps: None,
994                logo_path: row.get(32)?,
995                screenshot_path: row.get(33)?,
996                ruffle_support: row.get(34)?,
997                owner: row.get(35)?,
998                ext_data: None,
999            })
1000        },
1001    };
1002
1003    let mut games = search_custom(conn, search, selection.as_str(), game_map_closure)?;
1004
1005    for game in &mut games {
1006        if search.load_relations.platforms {
1007            game.detailed_platforms = get_game_platforms(conn, &game.id)?.into();
1008        }
1009        if search.load_relations.tags {
1010            game.detailed_tags = get_game_tags(conn, &game.id)?.into();
1011        }
1012        if search.load_relations.game_data {
1013            game.game_data = Some(get_game_data(conn, &game.id)?);
1014        }
1015        if search.load_relations.add_apps {
1016            game.add_apps = Some(get_game_add_apps(conn, &game.id)?);
1017        }
1018        if search.load_relations.ext_data {
1019            game.ext_data = Some(find_ext_data(conn, &game.id)?);
1020        }
1021    }
1022
1023    Ok(games)
1024}
1025
1026pub fn search_random(conn: &Connection, mut s: GameSearch, count: i64) -> Result<Vec<Game>> {
1027    s.limit = count;
1028    s.order.column = GameSearchSortable::RANDOM;
1029
1030    // Update tag filter indexing
1031    if let Some(tags) = &s.with_tag_filter {
1032        if tags.len() > 0 {
1033            let mut filtered_search = GameSearch::default();
1034            filtered_search.limit = 999999999;
1035            filtered_search.filter.exact_blacklist.tags = Some(tags.to_vec());
1036            filtered_search.filter.match_any = true;
1037            new_tag_filter_index(conn, &mut filtered_search)?;
1038        }
1039    }
1040
1041    search(conn, &s)
1042}
1043
1044fn build_search_query(search: &GameSearch, selection: &str) -> (String, Vec<SearchParam>) {
1045    let mut query = String::from(selection);
1046
1047    if search.ext_order.is_some() {
1048        query.push_str(" INNER JOIN OrderedExt ON game.id = OrderedExt.id");
1049    } else if search.order.column == GameSearchSortable::CUSTOM {
1050        query.push_str(" INNER JOIN OrderedIDs ON game.id = OrderedIDs.id");
1051    }
1052
1053    // Ordering
1054    let order_column = match search.ext_order {
1055        Some(_) => "OrderedExt.ExtValue",
1056        None => match search.order.column {
1057            GameSearchSortable::TITLE => "game.title",
1058            GameSearchSortable::DEVELOPER => "game.developer",
1059            GameSearchSortable::PUBLISHER => "game.publisher",
1060            GameSearchSortable::SERIES => "game.series",
1061            GameSearchSortable::PLATFORM => "game.platformName",
1062            GameSearchSortable::DATEADDED => "game.dateAdded",
1063            GameSearchSortable::DATEMODIFIED => "game.dateModified",
1064            GameSearchSortable::RELEASEDATE => "game.releaseDate",
1065            GameSearchSortable::LASTPLAYED => "game.lastPlayed",
1066            GameSearchSortable::PLAYTIME => "game.playtime",
1067            GameSearchSortable::CUSTOM => "OrderedIDs.RowNum",
1068            _ => "unknown",
1069        }
1070    };
1071    let order_direction = match search.order.direction {
1072        GameSearchDirection::ASC => "ASC",
1073        GameSearchDirection::DESC => "DESC",
1074    };
1075
1076    // Build the inner WHERE clause
1077    let mut params: Vec<SearchParam> = vec![];
1078    let where_clause = build_filter_query(&search.filter, &mut params);
1079
1080    // Add tag filtering
1081    if let Some(tags) = &search.with_tag_filter {
1082        if tags.len() > 0 {
1083            query.push_str(" INNER JOIN tag_filter_index ON game.id = tag_filter_index.id");
1084        }
1085    }
1086
1087    // Add offset
1088    if let Some(offset) = search.offset.clone() {
1089        let offset_val = match offset.value {
1090            serde_json::Value::Number(number) => SearchParam::Float64(number.as_f64().unwrap_or(0.into())),
1091            val => SearchParam::String(val.as_str().unwrap_or("").to_owned()),
1092        };
1093        if search.order.column == GameSearchSortable::CUSTOM {
1094            let offset_clause = format!(" WHERE OrderedIDs.RowNum > ?");
1095            query.push_str(&offset_clause);
1096            params.insert(0, offset_val);
1097        } else {
1098            let offset_clause = match search.order.direction {
1099                GameSearchDirection::ASC => {
1100                    format!(
1101                        " WHERE ({} COLLATE NOCASE, game.title, game.id) > (?, ?, ?)",
1102                        order_column
1103                    )
1104                }
1105                GameSearchDirection::DESC => {
1106                    format!(
1107                        " WHERE ({} COLLATE NOCASE, game.title, game.id) < (?, ?, ?)",
1108                        order_column
1109                    )
1110                }
1111            };
1112            query.push_str(&offset_clause);
1113
1114            // Insert in reverse order
1115            params.insert(0, SearchParam::String(offset.game_id.clone()));
1116            params.insert(0, SearchParam::String(offset.title.clone()));
1117            params.insert(0, offset_val);
1118        }
1119    }
1120
1121    // Combine all where clauses
1122    if where_clause.len() > 0 && where_clause != "()" {
1123        // Offset will begin WHERE itself, otherwise we're ANDing the offset
1124        let start_clause = match search.offset {
1125            Some(_) => " AND (",
1126            None => " WHERE (",
1127        };
1128        query.push_str(start_clause);
1129        query.push_str(&where_clause);
1130        query.push_str(")");
1131    }
1132
1133    if search.order.column == GameSearchSortable::RANDOM {
1134        query.push_str(" ORDER BY RANDOM()");
1135        let limit_query = format!(" LIMIT {}", search.limit);
1136        query.push_str(&limit_query);
1137    } else {
1138        if search.order.column == GameSearchSortable::CUSTOM {
1139            query.push_str(" ORDER BY OrderedIDs.RowNum");
1140        } else {
1141            query.push_str(
1142                format!(
1143                    " ORDER BY {} COLLATE NOCASE {}, game.title {}",
1144                    order_column, order_direction, order_direction
1145                )
1146                .as_str(),
1147            );
1148        }
1149        let limit_query = format!(" LIMIT {}", search.limit);
1150        query.push_str(&limit_query);
1151    }
1152
1153    (query, params)
1154}
1155
1156fn build_filter_query(filter: &GameFilter, params: &mut Vec<SearchParam>) -> String {
1157    let mut where_clauses = Vec::new();
1158
1159    if filter.subfilters.len() > 0 {
1160        for subfilter in filter.subfilters.iter() {
1161            let new_clause = build_filter_query(subfilter, params);
1162            if new_clause != "" {
1163                where_clauses.push(format!("({})", new_clause));
1164            }
1165        }
1166    }
1167
1168    let mut add_clause =
1169        |field_name: &str, values: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1170            if let Some(value_list) = values {
1171                let comparator = match (blacklist, exact) {
1172                    (true, true) => "!=",
1173                    (true, false) => "NOT LIKE",
1174                    (false, true) => "=",
1175                    (false, false) => "LIKE",
1176                };
1177
1178                // Exact OR - else - Inexact OR / Inexact AND / Exact AND
1179                if exact && filter.match_any {
1180                    let comparator = match blacklist {
1181                        true => "NOT IN",
1182                        false => "IN",
1183                    };
1184                    where_clauses.push(format!("game.{} {} rarray(?)", field_name, comparator));
1185                    params.push(SearchParam::StringVec(value_list.clone()));
1186                } else if blacklist {
1187                    let mut inner_clauses = vec![];
1188                    for value in value_list {
1189                        inner_clauses.push(format!("game.{} {} ?", field_name, comparator));
1190                        if exact {
1191                            params.push(SearchParam::String(value.clone()));
1192                        } else {
1193                            let p = format!("%{}%", value);
1194                            params.push(SearchParam::String(p));
1195                        }
1196                    }
1197                    where_clauses.push(format!("({})", inner_clauses.join(" AND ")));
1198                } else {
1199                    for value in value_list {
1200                        where_clauses.push(format!("game.{} {} ?", field_name, comparator));
1201                        if exact {
1202                            params.push(SearchParam::String(value.clone()));
1203                        } else {
1204                            let p = format!("%{}%", value);
1205                            params.push(SearchParam::String(p));
1206                        }
1207                    }
1208                }
1209            }
1210        };
1211
1212    // exact whitelist
1213    exact_whitelist_clause!(add_clause, "library", &filter.exact_whitelist.library);
1214    exact_whitelist_clause!(add_clause, "developer", &filter.exact_whitelist.developer);
1215    exact_whitelist_clause!(add_clause, "publisher", &filter.exact_whitelist.publisher);
1216    exact_whitelist_clause!(add_clause, "series", &filter.exact_whitelist.series);
1217    exact_whitelist_clause!(add_clause, "playMode", &filter.exact_whitelist.play_mode);
1218    exact_whitelist_clause!(add_clause, "status", &filter.exact_whitelist.status);
1219    exact_whitelist_clause!(add_clause, "notes", &filter.exact_whitelist.notes);
1220    exact_whitelist_clause!(add_clause, "source", &filter.exact_whitelist.source);
1221    exact_whitelist_clause!(
1222        add_clause,
1223        "originalDescription",
1224        &filter.exact_whitelist.original_description
1225    );
1226    exact_whitelist_clause!(add_clause, "language", &filter.exact_whitelist.language);
1227    exact_whitelist_clause!(
1228        add_clause,
1229        "ruffleSupport",
1230        &filter.exact_whitelist.ruffle_support
1231    );
1232    exact_whitelist_clause!(add_clause, "owner", &filter.exact_whitelist.owner);
1233
1234    // exact blacklist
1235    exact_blacklist_clause!(add_clause, "library", &filter.exact_blacklist.library);
1236    exact_blacklist_clause!(add_clause, "developer", &filter.exact_blacklist.developer);
1237    exact_blacklist_clause!(add_clause, "publisher", &filter.exact_blacklist.publisher);
1238    exact_blacklist_clause!(add_clause, "series", &filter.exact_blacklist.series);
1239    exact_blacklist_clause!(add_clause, "playMode", &filter.exact_blacklist.play_mode);
1240    exact_blacklist_clause!(add_clause, "status", &filter.exact_blacklist.status);
1241    exact_blacklist_clause!(add_clause, "notes", &filter.exact_blacklist.notes);
1242    exact_blacklist_clause!(add_clause, "source", &filter.exact_blacklist.source);
1243    exact_blacklist_clause!(
1244        add_clause,
1245        "originalDescription",
1246        &filter.exact_blacklist.original_description
1247    );
1248    exact_blacklist_clause!(add_clause, "language", &filter.exact_blacklist.language);
1249    exact_blacklist_clause!(
1250        add_clause,
1251        "ruffleSupport",
1252        &filter.exact_blacklist.ruffle_support
1253    );
1254    exact_blacklist_clause!(add_clause, "owner", &filter.exact_blacklist.owner);
1255
1256    // whitelist
1257    whitelist_clause!(add_clause, "library", &filter.whitelist.library);
1258    whitelist_clause!(add_clause, "developer", &filter.whitelist.developer);
1259    whitelist_clause!(add_clause, "publisher", &filter.whitelist.publisher);
1260    whitelist_clause!(add_clause, "series", &filter.whitelist.series);
1261    whitelist_clause!(add_clause, "playMode", &filter.whitelist.play_mode);
1262    whitelist_clause!(add_clause, "status", &filter.whitelist.status);
1263    whitelist_clause!(add_clause, "notes", &filter.whitelist.notes);
1264    whitelist_clause!(add_clause, "source", &filter.whitelist.source);
1265    whitelist_clause!(
1266        add_clause,
1267        "originalDescription",
1268        &filter.whitelist.original_description
1269    );
1270    whitelist_clause!(add_clause, "language", &filter.whitelist.language);
1271    whitelist_clause!(
1272        add_clause,
1273        "ruffleSupport",
1274        &filter.whitelist.ruffle_support
1275    );
1276    whitelist_clause!(add_clause, "owner", &filter.whitelist.owner);
1277
1278    // blacklist
1279    blacklist_clause!(add_clause, "library", &filter.blacklist.library);
1280    blacklist_clause!(add_clause, "developer", &filter.blacklist.developer);
1281    blacklist_clause!(add_clause, "publisher", &filter.blacklist.publisher);
1282    blacklist_clause!(add_clause, "series", &filter.blacklist.series);
1283    blacklist_clause!(add_clause, "playMode", &filter.blacklist.play_mode);
1284    blacklist_clause!(add_clause, "status", &filter.blacklist.status);
1285    blacklist_clause!(add_clause, "notes", &filter.blacklist.notes);
1286    blacklist_clause!(add_clause, "source", &filter.blacklist.source);
1287    blacklist_clause!(
1288        add_clause,
1289        "originalDescription",
1290        &filter.blacklist.original_description
1291    );
1292    blacklist_clause!(add_clause, "language", &filter.blacklist.language);
1293    blacklist_clause!(
1294        add_clause,
1295        "ruffleSupport",
1296        &filter.blacklist.ruffle_support
1297    );
1298    blacklist_clause!(add_clause, "owner", &filter.blacklist.owner);
1299
1300    let mut id_clause = |values: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1301        if let Some(value_list) = values {
1302            if exact {
1303                // All game ids are exact, AND would be impossible to satisfy, treat as OR, always
1304                let comparator = match blacklist {
1305                    true => "NOT IN",
1306                    false => "IN",
1307                };
1308                where_clauses.push(format!("(game.id {} rarray(?) OR game.id {} (SELECT id FROM game_redirect WHERE sourceId IN rarray(?)))", comparator, comparator));
1309                params.push(SearchParam::StringVec(value_list.clone()));
1310                params.push(SearchParam::StringVec(value_list.clone()));
1311            } else {
1312                for value in value_list {
1313                    if value.len() == 36 {
1314                        let comparator = match blacklist {
1315                            true => "!=",
1316                            false => "=",
1317                        };
1318                        where_clauses.push(format!("(game.id {} ? OR game.id {} (SELECT id FROM game_redirect WHERE sourceId = ? LIMIT 1))", comparator, comparator));
1319
1320                        params.push(SearchParam::String(value.clone()));
1321                        params.push(SearchParam::String(value.clone()));
1322                    } else {
1323                        let comparator = match blacklist {
1324                            true => "NOT LIKE",
1325                            false => "LIKE",
1326                        };
1327                        where_clauses.push(format!("(game.id {} ?)", comparator));
1328                        let p = format!("%{}%", value);
1329                        params.push(SearchParam::String(p));
1330                    }
1331                }
1332            }
1333        }
1334    };
1335
1336    id_clause(&filter.exact_whitelist.id, true, false);
1337    id_clause(&filter.exact_blacklist.id, true, true);
1338    id_clause(&filter.whitelist.id, false, false);
1339    id_clause(&filter.blacklist.id, false, false);
1340
1341    let mut add_tagged_clause =
1342        |tag_name: &str, values: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1343            if let Some(value_list) = values {
1344                let comparator = match blacklist {
1345                    true => "NOT IN",
1346                    false => "IN",
1347                };
1348
1349                // Exact OR - else - Inexact OR / Inexact AND / Exact AND
1350                if exact && filter.match_any {
1351                    // Must be an exact OR
1352                    params.push(SearchParam::StringVec(value_list.clone()));
1353
1354                    let tag_query = format!(
1355                        "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1356                SELECT {}Id FROM {}_alias WHERE name IN rarray(?)))",
1357                        comparator, tag_name, tag_name, tag_name, tag_name, tag_name
1358                    );
1359
1360                    where_clauses.push(tag_query);
1361                } else {
1362                    let mut inner_tag_queries = vec![];
1363
1364                    // Add parameters
1365                    if exact {
1366                        for value in value_list {
1367                            inner_tag_queries.push("name = ?");
1368                            params.push(SearchParam::String(value.clone()));
1369                        }
1370                    } else {
1371                        for value in value_list {
1372                            inner_tag_queries.push("name LIKE ?");
1373                            let p = format!("%{}%", value);
1374                            params.push(SearchParam::String(p));
1375                        }
1376                    }
1377
1378                    // Add query
1379                    let tag_query = match (blacklist, filter.match_any) {
1380                        (false, false) => {
1381                            if inner_tag_queries.len() == 1 {
1382                                format!(
1383                                    "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1384                                SELECT {}Id FROM {}_alias WHERE {})
1385                            )",
1386                                    comparator,
1387                                    tag_name,
1388                                    tag_name,
1389                                    tag_name,
1390                                    tag_name,
1391                                    tag_name,
1392                                    inner_tag_queries[0]
1393                                )
1394                            } else {
1395                                let mut q = format!(
1396                                    "SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1397                                    SELECT {}Id FROM {}_alias WHERE {}
1398                                )",
1399                                    tag_name,
1400                                    tag_name,
1401                                    tag_name,
1402                                    tag_name,
1403                                    tag_name,
1404                                    inner_tag_queries[0]
1405                                );
1406                                for inner_tag_query in inner_tag_queries.iter().skip(1) {
1407                                    let part = format!(
1408                                        " AND gameId IN (
1409                                    SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1410                                        SELECT {}Id FROM {}_alias WHERE {}
1411                                    )
1412                                )",
1413                                        tag_name,
1414                                        tag_name,
1415                                        tag_name,
1416                                        tag_name,
1417                                        tag_name,
1418                                        inner_tag_query
1419                                    );
1420                                    q.push_str(&part);
1421                                }
1422                                format!("game.id {} ({})", comparator, q)
1423                            }
1424                        }
1425                        // Let blacklisted tags always use OR comparisons
1426                        // This needs to be changed to check for BOTH tags being on a game later!
1427                        (true, false) => format!(
1428                            "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1429                    SELECT {}Id FROM {}_alias WHERE ({})))",
1430                            comparator,
1431                            tag_name,
1432                            tag_name,
1433                            tag_name,
1434                            tag_name,
1435                            tag_name,
1436                            inner_tag_queries.join(" OR ")
1437                        ),
1438                        (true, true) | (false, true) => format!(
1439                            "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1440                    SELECT {}Id FROM {}_alias WHERE name IN {}))",
1441                            comparator,
1442                            tag_name,
1443                            tag_name,
1444                            tag_name,
1445                            tag_name,
1446                            tag_name,
1447                            inner_tag_queries.join(" OR ")
1448                        ),
1449                    };
1450
1451                    where_clauses.push(tag_query);
1452                }
1453            }
1454        };
1455
1456    // tag groups
1457    add_tagged_clause("tag", &filter.whitelist.tags, false, false);
1458    add_tagged_clause("tag", &filter.blacklist.tags, false, true);
1459    add_tagged_clause("tag", &filter.exact_whitelist.tags, true, false);
1460    add_tagged_clause("tag", &filter.exact_blacklist.tags, true, true);
1461
1462    add_tagged_clause("platform", &filter.whitelist.platforms, false, false);
1463    add_tagged_clause("platform", &filter.blacklist.platforms, false, true);
1464    add_tagged_clause("platform", &filter.exact_whitelist.platforms, true, false);
1465    add_tagged_clause("platform", &filter.exact_blacklist.platforms, true, true);
1466
1467    let mut add_multi_clause =
1468        |field_names: Vec<&str>, filter: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1469            if let Some(value_list) = filter {
1470                let comparator = match (blacklist, exact) {
1471                    (true, true) => "!=",
1472                    (true, false) => "NOT LIKE",
1473                    (false, true) => "=",
1474                    (false, false) => "LIKE",
1475                };
1476
1477                if blacklist {
1478                    let mut inner_clauses = vec![];
1479                    for value in value_list {
1480                        let mut value_clauses = vec![];
1481                        for field_name in field_names.clone() {
1482                            value_clauses.push(format!("game.{} {} ?", field_name, comparator));
1483                            if exact {
1484                                params.push(SearchParam::String(value.clone()));
1485                            } else {
1486                                let p = format!("%{}%", value);
1487                                params.push(SearchParam::String(p));
1488                            }
1489                        }
1490                        inner_clauses.push(format!("({})", &value_clauses.join(" OR ")));
1491                    }
1492                    where_clauses.push(format!("({})", inner_clauses.join(" OR ")));
1493                } else {
1494                    for value in value_list {
1495                        let mut value_clauses = vec![];
1496                        for field_name in field_names.clone() {
1497                            value_clauses.push(format!("game.{} {} ?", field_name, comparator));
1498                            if exact {
1499                                params.push(SearchParam::String(value.clone()));
1500                            } else {
1501                                let p = format!("%{}%", value);
1502                                params.push(SearchParam::String(p));
1503                            }
1504                        }
1505                        where_clauses.push(format!("({})", &value_clauses.join(" OR ")));
1506                    }
1507                }
1508            }
1509        };
1510
1511    // whitelist
1512    add_multi_clause(
1513        vec!["title", "alternateTitles"],
1514        &filter.whitelist.title,
1515        false,
1516        false,
1517    );
1518    add_multi_clause(
1519        vec![
1520            "title",
1521            "alternateTitles",
1522            "developer",
1523            "publisher",
1524            "series",
1525        ],
1526        &filter.whitelist.generic,
1527        false,
1528        false,
1529    );
1530
1531    // blacklist
1532    add_multi_clause(
1533        vec!["title", "alternateTitles"],
1534        &filter.blacklist.title,
1535        false,
1536        true,
1537    );
1538    add_multi_clause(
1539        vec![
1540            "title",
1541            "alternateTitles",
1542            "developer",
1543            "publisher",
1544            "series",
1545        ],
1546        &filter.blacklist.generic,
1547        false,
1548        true,
1549    );
1550
1551    let mut add_joint_game_data_clause =
1552        |field_name: &str,
1553         game_field_name: &str,
1554         filter: &Option<Vec<String>>,
1555         exact: bool,
1556         blacklist: bool| {
1557            if let Some(value_list) = filter {
1558                let comparator = match (blacklist, exact) {
1559                    (true, true) => "!=",
1560                    (true, false) => "NOT LIKE",
1561                    (false, true) => "=",
1562                    (false, false) => "LIKE",
1563                };
1564
1565                if blacklist {
1566                    let mut inner_clauses = vec![];
1567                    for value in value_list {
1568                        let mut value_clauses = vec![];
1569                        value_clauses.push(format!("game.{} {} ?", game_field_name, comparator));
1570                        if exact {
1571                            params.push(SearchParam::String(value.clone()));
1572                        } else {
1573                            let p = format!("%{}%", value);
1574                            params.push(SearchParam::String(p));
1575                        }
1576
1577                        value_clauses.push(format!(
1578                            "game.id IN (SELECT gameId FROM game_data WHERE {} {} ?)",
1579                            field_name, comparator
1580                        ));
1581                        if exact {
1582                            params.push(SearchParam::String(value.clone()));
1583                        } else {
1584                            let p = format!("%{}%", value);
1585                            params.push(SearchParam::String(p));
1586                        }
1587                        inner_clauses.push(format!("({})", &value_clauses.join(" AND ")));
1588                    }
1589                    where_clauses.push(format!("({})", inner_clauses.join(" OR ")));
1590                } else {
1591                    for value in value_list {
1592                        let mut value_clauses = vec![];
1593                        value_clauses.push(format!("game.{} {} ?", game_field_name, comparator));
1594                        if exact {
1595                            params.push(SearchParam::String(value.clone()));
1596                        } else {
1597                            let p = format!("%{}%", value);
1598                            params.push(SearchParam::String(p));
1599                        }
1600
1601                        value_clauses.push(format!(
1602                            "game.id IN (SELECT gameId FROM game_data WHERE {} {} ?)",
1603                            field_name, comparator
1604                        ));
1605                        if exact {
1606                            params.push(SearchParam::String(value.clone()));
1607                        } else {
1608                            let p = format!("%{}%", value);
1609                            params.push(SearchParam::String(p));
1610                        }
1611                        where_clauses.push(format!("({})", &value_clauses.join(" OR ")));
1612                    }
1613                }
1614            }
1615        };
1616
1617    add_joint_game_data_clause(
1618        "applicationPath",
1619        "applicationPath",
1620        &filter.whitelist.application_path,
1621        false,
1622        false,
1623    );
1624    add_joint_game_data_clause(
1625        "applicationPath",
1626        "applicationPath",
1627        &filter.blacklist.application_path,
1628        false,
1629        true,
1630    );
1631    add_joint_game_data_clause(
1632        "applicationPath",
1633        "applicationPath",
1634        &filter.exact_whitelist.application_path,
1635        true,
1636        false,
1637    );
1638    add_joint_game_data_clause(
1639        "applicationPath",
1640        "applicationPath",
1641        &filter.exact_blacklist.application_path,
1642        true,
1643        true,
1644    );
1645
1646    add_joint_game_data_clause(
1647        "launchCommand",
1648        "launchCommand",
1649        &filter.whitelist.launch_command,
1650        false,
1651        false,
1652    );
1653    add_joint_game_data_clause(
1654        "launchCommand",
1655        "launchCommand",
1656        &filter.blacklist.launch_command,
1657        false,
1658        true,
1659    );
1660    add_joint_game_data_clause(
1661        "launchCommand",
1662        "launchCommand",
1663        &filter.exact_whitelist.launch_command,
1664        true,
1665        false,
1666    );
1667    add_joint_game_data_clause(
1668        "launchCommand",
1669        "launchCommand",
1670        &filter.exact_blacklist.launch_command,
1671        true,
1672        true,
1673    );
1674
1675    // Tag and Platform comparisons
1676    let mut add_compare_tag_clause = |field_name: &str,
1677                                      comparator: KeyChar,
1678                                      filter: &Option<i64>| {
1679        if let Some(f) = filter {
1680            if *f == 0 {
1681                match comparator {
1682                    KeyChar::EQUALS => {
1683                        // Select games with exactly 0 additional apps
1684                        where_clauses.push(format!(
1685                            "game.id NOT IN (SELECT gameId FROM game_{}s_{})",
1686                            field_name, field_name
1687                        ));
1688                    }
1689                    KeyChar::LOWER => (),
1690                    KeyChar::HIGHER => {
1691                        // Select games with 1 or more additional apps
1692                        where_clauses.push(format!(
1693                            "game.id IN (SELECT gameId FROM game_{}s_{})",
1694                            field_name, field_name
1695                        ));
1696                    }
1697                    KeyChar::MATCHES => (),
1698                }
1699            } else {
1700                match comparator {
1701                    KeyChar::MATCHES => (),
1702                    KeyChar::LOWER => {
1703                        where_clauses.push(format!("game.id NOT IN (SELECT gameId FROM game_{}s_{} GROUP BY gameId HAVING COUNT(gameId) >= ?)", field_name, field_name));
1704                        params.push(SearchParam::Integer64(f.clone()));
1705                    }
1706                    KeyChar::HIGHER => {
1707                        where_clauses.push(format!("game.id IN (SELECT gameId FROM game_{}s_{} GROUP BY gameId HAVING COUNT(gameId) > ?)", field_name, field_name));
1708                        params.push(SearchParam::Integer64(f.clone()));
1709                    }
1710                    KeyChar::EQUALS => {
1711                        where_clauses.push(format!("game.id IN (SELECT gameId FROM game_{}s_{} GROUP BY gameId HAVING COUNT(gameId) = ?)", field_name, field_name));
1712                        params.push(SearchParam::Integer64(f.clone()));
1713                    }
1714                }
1715            }
1716        }
1717    };
1718
1719    add_compare_tag_clause("tag", KeyChar::LOWER, &filter.lower_than.tags);
1720    add_compare_tag_clause("tag", KeyChar::HIGHER, &filter.higher_than.tags);
1721    add_compare_tag_clause("tag", KeyChar::EQUALS, &filter.equal_to.tags);
1722
1723    add_compare_tag_clause("platform", KeyChar::LOWER, &filter.lower_than.platforms);
1724    add_compare_tag_clause("platform", KeyChar::HIGHER, &filter.higher_than.platforms);
1725    add_compare_tag_clause("platform", KeyChar::EQUALS, &filter.equal_to.platforms);
1726
1727    // Add app comparisons
1728    let mut add_compare_add_app_clause = |comparator: KeyChar, filter: &Option<i64>| {
1729        if let Some(f) = filter {
1730            if *f == 0 {
1731                match comparator {
1732                    KeyChar::EQUALS => {
1733                        // Select games with exactly 0 additional apps
1734                        where_clauses.push(
1735                            "game.id NOT IN (SELECT parentGameId FROM additional_app)".to_string(),
1736                        );
1737                    }
1738                    KeyChar::LOWER => (),
1739                    KeyChar::HIGHER => {
1740                        // Select games with 1 or more additional apps
1741                        where_clauses.push(
1742                            "game.id IN (SELECT parentGameId FROM additional_app)".to_string(),
1743                        );
1744                    }
1745                    KeyChar::MATCHES => (),
1746                }
1747            } else {
1748                match comparator {
1749                    KeyChar::MATCHES => (),
1750                    KeyChar::LOWER => {
1751                        where_clauses.push("game.id NOT IN (SELECT parentGameId FROM additional_app GROUP BY parentGameId HAVING COUNT(parentGameId) >= ?)".to_string());
1752                        params.push(SearchParam::Integer64(f.clone()));
1753                    }
1754                    KeyChar::HIGHER => {
1755                        where_clauses.push("game.id IN (SELECT parentGameId FROM additional_app GROUP BY parentGameId HAVING COUNT(parentGameId) > ?)".to_string());
1756                        params.push(SearchParam::Integer64(f.clone()));
1757                    }
1758                    KeyChar::EQUALS => {
1759                        where_clauses.push("game.id IN (SELECT parentGameId FROM additional_app GROUP BY parentGameId HAVING COUNT(parentGameId) = ?)".to_string());
1760                        params.push(SearchParam::Integer64(f.clone()));
1761                    }
1762                }
1763            }
1764        }
1765    };
1766
1767    add_compare_add_app_clause(KeyChar::LOWER, &filter.lower_than.add_apps);
1768    add_compare_add_app_clause(KeyChar::HIGHER, &filter.higher_than.add_apps);
1769    add_compare_add_app_clause(KeyChar::EQUALS, &filter.equal_to.add_apps);
1770
1771    let mut add_compare_game_data_clause = |comparator: KeyChar, filter: &Option<i64>| {
1772        if let Some(f) = filter {
1773            if *f <= 0 {
1774                match comparator {
1775                    KeyChar::EQUALS => {
1776                        // Select games with exactly 0 additional apps
1777                        where_clauses
1778                            .push("game.id NOT IN (SELECT gameId FROM game_data)".to_string());
1779                    }
1780                    KeyChar::LOWER => (),
1781                    KeyChar::HIGHER => {
1782                        // Select games with 1 or more additional apps
1783                        where_clauses.push("game.id IN (SELECT gameId FROM game_data)".to_string());
1784                    }
1785                    KeyChar::MATCHES => (),
1786                }
1787            } else {
1788                match comparator {
1789                    KeyChar::MATCHES => (),
1790                    KeyChar::LOWER => {
1791                        where_clauses.push("game.id NOT IN (SELECT gameId FROM game_data GROUP BY gameId HAVING COUNT(gameId) >= ?)".to_string());
1792                        params.push(SearchParam::Integer64(f.clone()));
1793                    }
1794                    KeyChar::HIGHER => {
1795                        where_clauses.push("game.id IN (SELECT gameId FROM game_data GROUP BY gameId HAVING COUNT(gameId) > ?)".to_string());
1796                        params.push(SearchParam::Integer64(f.clone()));
1797                    }
1798                    KeyChar::EQUALS => {
1799                        where_clauses.push("game.id IN (SELECT gameId FROM game_data GROUP BY gameId HAVING COUNT(gameId) = ?)".to_string());
1800                        params.push(SearchParam::Integer64(f.clone()));
1801                    }
1802                }
1803            }
1804        }
1805    };
1806
1807    add_compare_game_data_clause(KeyChar::LOWER, &filter.lower_than.game_data);
1808    add_compare_game_data_clause(KeyChar::HIGHER, &filter.higher_than.game_data);
1809    add_compare_game_data_clause(KeyChar::EQUALS, &filter.equal_to.game_data);
1810
1811    let mut add_compare_dates_clause =
1812        |date_field: &str, comparator: KeyChar, filter: &Option<String>| {
1813            if let Some(f) = filter {
1814                match comparator {
1815                    KeyChar::MATCHES => (),
1816                    KeyChar::LOWER => {
1817                        where_clauses.push(format!("date(game.{}) < ?", date_field));
1818                        params.push(SearchParam::String(f.clone()));
1819                    }
1820                    KeyChar::HIGHER => {
1821                        // e.g "2021-01" will generate >= "2021-01" and < "2021-02"
1822                        where_clauses.push(format!("date(game.{}) >= ?", date_field));
1823                        params.push(SearchParam::String(f.clone()));
1824                    }
1825                    KeyChar::EQUALS => {
1826                        where_clauses.push(format!("date(game.{}) LIKE ?", date_field));
1827                        let p = f.clone() + "%";
1828                        params.push(SearchParam::String(p));
1829                    }
1830                }
1831            }
1832        };
1833
1834    add_compare_dates_clause("dateAdded", KeyChar::LOWER, &filter.lower_than.date_added);
1835    add_compare_dates_clause("dateAdded", KeyChar::HIGHER, &filter.higher_than.date_added);
1836    add_compare_dates_clause("dateAdded", KeyChar::EQUALS, &filter.equal_to.date_added);
1837
1838    add_compare_dates_clause(
1839        "dateModified",
1840        KeyChar::LOWER,
1841        &filter.lower_than.date_modified,
1842    );
1843    add_compare_dates_clause(
1844        "dateModified",
1845        KeyChar::HIGHER,
1846        &filter.higher_than.date_modified,
1847    );
1848    add_compare_dates_clause(
1849        "dateModified",
1850        KeyChar::EQUALS,
1851        &filter.equal_to.date_modified,
1852    );
1853
1854    add_compare_dates_clause("lastPlayed", KeyChar::LOWER, &filter.lower_than.last_played);
1855    add_compare_dates_clause(
1856        "lastPlayed",
1857        KeyChar::HIGHER,
1858        &filter.higher_than.last_played,
1859    );
1860    add_compare_dates_clause("lastPlayed", KeyChar::EQUALS, &filter.equal_to.last_played);
1861
1862    let mut add_compare_dates_string_clause =
1863        |date_field: &str, comparator: KeyChar, filter: &Option<String>| {
1864            if let Some(f) = filter {
1865                match comparator {
1866                    KeyChar::MATCHES => (),
1867                    KeyChar::LOWER => {
1868                        where_clauses.push(format!("game.{} < ?", date_field));
1869                        params.push(SearchParam::String(f.clone()));
1870                    }
1871                    KeyChar::HIGHER => {
1872                        // e.g "2021-01" will generate >= "2021-01" and < "2021-02"
1873                        where_clauses.push(format!("game.{} >= ?", date_field));
1874                        params.push(SearchParam::String(f.clone()));
1875                    }
1876                    KeyChar::EQUALS => {
1877                        where_clauses.push(format!("game.{} LIKE ?", date_field));
1878                        let p = f.clone() + "%";
1879                        params.push(SearchParam::String(p));
1880                    }
1881                }
1882            }
1883        };
1884
1885    add_compare_dates_string_clause(
1886        "releaseDate",
1887        KeyChar::LOWER,
1888        &filter.lower_than.release_date,
1889    );
1890    add_compare_dates_string_clause(
1891        "releaseDate",
1892        KeyChar::HIGHER,
1893        &filter.higher_than.release_date,
1894    );
1895    add_compare_dates_string_clause(
1896        "releaseDate",
1897        KeyChar::EQUALS,
1898        &filter.equal_to.release_date,
1899    );
1900
1901    let mut add_compare_counter_clause =
1902        |counter: &str, comparator: KeyChar, filter: &Option<i64>| {
1903            if let Some(f) = filter {
1904                match comparator {
1905                    KeyChar::MATCHES => (),
1906                    KeyChar::LOWER => {
1907                        where_clauses.push(format!("game.{} < ?", counter));
1908                        params.push(SearchParam::Integer64(f.clone()));
1909                    }
1910                    KeyChar::HIGHER => {
1911                        where_clauses.push(format!("game.{} > ?", counter));
1912                        params.push(SearchParam::Integer64(f.clone()));
1913                    }
1914                    KeyChar::EQUALS => {
1915                        where_clauses.push(format!("game.{} = ?", counter));
1916                        params.push(SearchParam::Integer64(f.clone()));
1917                    }
1918                }
1919            }
1920        };
1921
1922    add_compare_counter_clause("playtime", KeyChar::LOWER, &filter.lower_than.playtime);
1923    add_compare_counter_clause("playtime", KeyChar::HIGHER, &filter.higher_than.playtime);
1924    add_compare_counter_clause("playtime", KeyChar::EQUALS, &filter.equal_to.playtime);
1925
1926    add_compare_counter_clause("playCounter", KeyChar::LOWER, &filter.lower_than.playcount);
1927    add_compare_counter_clause(
1928        "playCounter",
1929        KeyChar::HIGHER,
1930        &filter.higher_than.playcount,
1931    );
1932    add_compare_counter_clause("playCounter", KeyChar::EQUALS, &filter.equal_to.playcount);
1933
1934    // Installed clause
1935    if let Some(val) = filter.bool_comp.installed {
1936        where_clauses.push(
1937            "game.id IN (SELECT gameId FROM game_data WHERE game_data.presentOnDisk = ?)"
1938                .to_owned(),
1939        );
1940        params.push(SearchParam::Boolean(val));
1941    }
1942
1943    // Deal with complicated extension comparisons
1944
1945    let mut ext_add_clause = |values: &Option<HashMap<String, HashMap<String, Vec<String>>>>,
1946                              exact: bool,
1947                              blacklist: bool| {
1948        if let Some(value_list) = values {
1949            let comparator = match (blacklist, exact) {
1950                (true, true) => "!=",
1951                (true, false) => "NOT LIKE",
1952                (false, true) => "=",
1953                (false, false) => "LIKE",
1954            };
1955
1956            // Exact OR - else - Inexact OR / Inexact AND / Exact AND
1957            if exact && filter.match_any {
1958                let comparator = match blacklist {
1959                    true => "NOT IN",
1960                    false => "IN",
1961                };
1962                for (ext_id, comp) in value_list {
1963                    for (key, value_list) in comp {
1964                        where_clauses.push(
1965                            format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') {} rarray(?))", key, comparator)
1966                        );
1967                        params.push(SearchParam::String(ext_id.clone()));
1968                        params.push(SearchParam::StringVec(value_list.clone()));
1969                    }
1970                }
1971            } else if blacklist {
1972                let mut inner_clauses = vec![];
1973                for (ext_id, comp) in value_list {
1974                    for (key, value_list) in comp {
1975                        for value in value_list {
1976                            inner_clauses.push(
1977                                format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') {} ?)", key, comparator)
1978                            );
1979                            params.push(SearchParam::String(ext_id.clone()));
1980                            if exact {
1981                                params.push(SearchParam::String(value.clone()));
1982                            } else {
1983                                let p = format!("%{}%", value);
1984                                params.push(SearchParam::String(p));
1985                            }
1986                        }
1987                    }
1988                }
1989                where_clauses.push(format!("({})", inner_clauses.join(" AND ")));
1990            } else {
1991                for (ext_id, comp) in value_list {
1992                    for (key, value_list) in comp {
1993                        for value in value_list {
1994                            where_clauses.push(
1995                                format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') {} ?)", key, comparator)
1996                            );
1997                            params.push(SearchParam::String(ext_id.clone()));
1998                            if exact {
1999                                params.push(SearchParam::String(value.clone()));
2000                            } else {
2001                                let p = format!("%{}%", value);
2002                                params.push(SearchParam::String(p));
2003                            }
2004                        }
2005                    }
2006                }
2007            }
2008        }
2009    };
2010
2011    // Ext strings
2012
2013    ext_add_clause(&filter.whitelist.ext, false, false);
2014    ext_add_clause(&filter.blacklist.ext, false, true);
2015    ext_add_clause(&filter.exact_whitelist.ext, true, false);
2016    ext_add_clause(&filter.exact_blacklist.ext, true, true);
2017
2018    let mut ext_add_compare =
2019    |comparator: KeyChar, value: &Option<HashMap<String, HashMap<String, i64>>>| {
2020        if let Some(value_list) = value {
2021            for (ext_id, values) in value_list {
2022                for (key, f) in values {
2023                    match comparator {
2024                        KeyChar::EQUALS | KeyChar::MATCHES => {
2025                            where_clauses.push(format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') = ?)", key).to_owned());
2026                            params.push(SearchParam::String(ext_id.clone()));
2027                            params.push(SearchParam::Integer64(f.clone()));
2028                        },
2029                        KeyChar::LOWER => {
2030                            where_clauses.push(format!("game.id NOT IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') >= ?)", key).to_owned());
2031                            params.push(SearchParam::String(ext_id.clone()));
2032                            params.push(SearchParam::Integer64(f.clone()));
2033                        }
2034                        KeyChar::HIGHER => {
2035                            where_clauses.push(format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') > ?)", key).to_owned());
2036                            params.push(SearchParam::String(ext_id.clone()));
2037                            params.push(SearchParam::Integer64(f.clone()));
2038                        }
2039                    }
2040                }
2041            }
2042        }
2043    };
2044
2045    // Ext numericals
2046
2047    ext_add_compare(KeyChar::EQUALS, &filter.equal_to.ext);
2048    ext_add_compare(KeyChar::LOWER, &filter.lower_than.ext);
2049    ext_add_compare(KeyChar::HIGHER, &filter.higher_than.ext);
2050
2051    // Ext bools
2052
2053    if let Some(value_list) = &filter.bool_comp.ext {
2054        for (ext_id, comp) in value_list {
2055            for (key, value) in comp {
2056                where_clauses.push(
2057                    format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') = ?)", key).to_owned()
2058                );
2059                params.push(SearchParam::String(ext_id.clone()));
2060                params.push(SearchParam::Boolean(value.clone()));
2061            }
2062        }
2063    }
2064
2065    // Remove any cases of "()" from where_clauses
2066
2067    where_clauses = where_clauses.into_iter().filter(|s| s != "()").collect();
2068
2069    if filter.match_any {
2070        where_clauses.join(" OR ")
2071    } else {
2072        where_clauses.join(" AND ")
2073    }
2074}
2075
2076fn format_query(query: &str, substitutions: Vec<SearchParam>) -> String {
2077    let mut formatted_query = String::new();
2078    let mut trim_mode = false;
2079    let mut indent = 0;
2080    let mut substitution_iter = substitutions.iter();
2081    let mut skip_drop = false;
2082
2083    for (idx, ch) in query.chars().enumerate() {
2084        match ch {
2085            '(' => {
2086                if idx + 1 < query.len() {
2087                    let next: String = query.chars().skip(idx + 1).take(1).collect();
2088                    if vec![")", "*"].contains(&next.as_str()) {
2089                        formatted_query.push(ch);
2090                        skip_drop = true;
2091                        continue;
2092                    }
2093                }
2094                indent += 4;
2095                trim_mode = true;
2096                formatted_query.push(ch);
2097                formatted_query.push('\n');
2098            }
2099            ')' => {
2100                if skip_drop {
2101                    skip_drop = false;
2102                    formatted_query.push(ch);
2103                    continue;
2104                }
2105                trim_mode = false;
2106                indent -= 4;
2107                formatted_query.push('\n');
2108                let spaces = " ".repeat(indent);
2109                formatted_query.push_str(&spaces);
2110                formatted_query.push(ch);
2111            }
2112            '?' => {
2113                if let Some(subst) = substitution_iter.next() {
2114                    let wrapped_subst = format!("'{}'", subst);
2115                    formatted_query.push_str(&wrapped_subst);
2116                } else {
2117                    // If there are no more substitutions, keep the '?' or handle as needed
2118                    formatted_query.push(ch);
2119                }
2120            }
2121            ' ' => {
2122                if !trim_mode {
2123                    formatted_query.push(ch);
2124                }
2125            }
2126            '\n' => trim_mode = true,
2127            _ => {
2128                if trim_mode {
2129                    let spaces = " ".repeat(indent);
2130                    formatted_query.push_str(&spaces);
2131                    trim_mode = false;
2132                }
2133                formatted_query.push(ch);
2134            }
2135        }
2136    }
2137
2138    formatted_query
2139}
2140
2141pub fn new_custom_id_order(conn: &Connection, custom_id_order: Vec<String>) -> Result<()> {
2142    let new_order = custom_id_order.join(";");
2143    let current_order = conn.query_row("SELECT IFNULL(string_agg(id, ';'), ''),  ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum FROM custom_id_order ORDER BY RowNum", (), |row| row.get::<_, String>(0))?;
2144    if current_order != new_order {
2145        conn.execute("DELETE FROM custom_id_order", ())?;
2146        let mut stmt = conn.prepare("INSERT INTO custom_id_order (id) VALUES (?)")?;
2147        for id in custom_id_order {
2148            stmt.execute(params![id])?;
2149        }
2150    }
2151    Ok(())
2152}
2153
2154// Dumb replacment string to denote an 'empty' value
2155const REPLACEMENT: &str =
2156    "UIOWHDYUAWDGBAWYUODIGAWYUIDIAWGHDYUI8AWGHDUIAWDHNAWUIODHJNAWIOUDHJNAWOUIDAJNWMLDK";
2157
2158pub fn new_tag_filter_index(conn: &Connection, search: &mut GameSearch) -> Result<()> {
2159    // Allow use of rarray() in SQL queries
2160    rusqlite::vtab::array::load_module(conn)?;
2161
2162    search.limit = 9999999999999999;
2163    search.filter = GameFilter::default();
2164    search.filter.match_any = true;
2165
2166    if let Some(t) = search.with_tag_filter.clone() {
2167        if t.len() > 0 {
2168            search.filter.exact_blacklist.tags = Some(t);
2169            search.with_tag_filter = None;
2170        }
2171    }
2172
2173    if search.filter.exact_blacklist.tags.is_none()
2174        || search.filter.exact_blacklist.tags.clone().unwrap().len() == 0
2175    {
2176        return Ok(());
2177    }
2178
2179    let mut tags = search.filter.exact_blacklist.tags.clone().unwrap();
2180    tags.sort();
2181    let tags_key = tags.join(";");
2182
2183    // Check against existing key
2184    let tag_filter_info = conn
2185        .query_row("SELECT key, dirty FROM tag_filter_index_info", (), |row| {
2186            Ok(TagFilterInfo {
2187                key: row.get(0)?,
2188                dirty: row.get(1)?,
2189            })
2190        })
2191        .optional()?;
2192
2193    match tag_filter_info {
2194        Some(info) => {
2195            // Index already built and clean, return
2196            if !info.dirty && tags_key == info.key {
2197                return Ok(());
2198            }
2199        }
2200        None => {
2201            // No existing index, continue
2202        }
2203    }
2204
2205    debug_println!("filtering {} tags", tags.len());
2206
2207    conn.execute("DELETE FROM tag_filter_index", ())?; // Empty existing index
2208
2209    let (query, params) = build_search_query(search, TAG_FILTER_INDEX_QUERY);
2210
2211    // Convert the parameters array to something rusqlite understands
2212    let params_as_refs: Vec<&dyn rusqlite::ToSql> =
2213        params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
2214
2215    debug_println!(
2216        "new filtered tag query - \n{}",
2217        format_query(&query, params.clone())
2218    );
2219
2220    let mut stmt = conn.prepare(query.as_str())?;
2221    stmt.execute(params_as_refs.as_slice())?;
2222
2223    tags.sort();
2224
2225    conn.execute("DELETE FROM tag_filter_index_info", ())?; // Empty existing index info
2226    conn.execute(
2227        "INSERT INTO tag_filter_index_info (key, dirty) VALUES (?, 0)",
2228        params![tags_key],
2229    )?;
2230
2231    Ok(())
2232}
2233
2234pub fn mark_index_dirty(conn: &Connection) -> Result<()> {
2235    conn.execute("UPDATE tag_filter_index_info SET dirty = 1", ())?;
2236    Ok(())
2237}
2238
2239#[cfg_attr(feature = "napi", napi)]
2240#[cfg_attr(not(feature = "napi"), derive(Clone))]
2241#[derive(Debug)]
2242pub enum ElementType {
2243    MODIFIER,
2244    KEY,
2245    KEYCHAR,
2246    VALUE,
2247}
2248
2249#[cfg_attr(feature = "napi", napi(object))]
2250#[derive(Debug, Clone)]
2251pub struct ElementPosition {
2252    pub element: ElementType,
2253    pub value: String,
2254    pub start: i32,
2255    pub end: i32,
2256}
2257
2258#[cfg_attr(feature = "napi", napi(object))]
2259#[derive(Debug, Clone)]
2260pub struct ParsedInput {
2261    pub search: GameSearch,
2262    pub positions: Vec<ElementPosition>,
2263}
2264
2265pub fn parse_user_input(input: &str, ext_searchables: Option<&HashMap<String, ExtSearchableRegistered>>) -> ParsedInput {
2266    let ext_searchables = match ext_searchables {
2267        Some(e) => e,
2268        None => &HashMap::new()
2269    };
2270
2271    let mut search = GameSearch::default();
2272    let mut filter = ForcedGameFilter::default();
2273
2274    let mut capturing_quotes = false;
2275    let mut working_key = String::new();
2276    let mut working_value = String::new();
2277    let mut working_key_char: Option<KeyChar> = None;
2278    let mut negative = false;
2279
2280    let mut positions = Vec::new();
2281    let mut current_pos = 0;
2282
2283    for raw_token in input.split(" ") {
2284        // Value on the same scope as token to append to
2285        let mut token = raw_token.to_owned();
2286        let mut token_start = current_pos.try_into().unwrap_or(0);
2287        let mut _t = "".to_owned();
2288        debug_println!("token {}", token);
2289        // Handle continued value capture if needed
2290
2291        if !capturing_quotes && token.len() > 1 {
2292            // Not inside quotes, check for negation
2293            if token.starts_with("-") {
2294                negative = true;
2295
2296                token = token.strip_prefix("-").unwrap().to_owned();
2297                positions.push(ElementPosition {
2298                    element: ElementType::MODIFIER,
2299                    value: "-".to_owned(),
2300                    start: token_start,
2301                    end: token_start + 1,
2302                });
2303                token_start += 1;
2304            }
2305
2306            if token.len() > 1 {
2307                debug_println!("checking token start");
2308                // Check for quick search options preceding token
2309                let ch = token.chars().next().unwrap();
2310                debug_println!("start char: {}", ch);
2311                match ch {
2312                    '#' => {
2313                        token = token.strip_prefix('#').unwrap().to_owned();
2314                        working_key = "tag".to_owned();
2315                        positions.push(ElementPosition {
2316                            element: ElementType::MODIFIER,
2317                            value: "#".to_owned(),
2318                            start: token_start,
2319                            end: token_start + 1,
2320                        });
2321                        token_start += 1;
2322                    }
2323                    '!' => {
2324                        token = token.strip_prefix('!').unwrap().to_owned();
2325                        working_key = "platform".to_owned();
2326                        positions.push(ElementPosition {
2327                            element: ElementType::MODIFIER,
2328                            value: "!".to_owned(),
2329                            start: token_start,
2330                            end: token_start + 1,
2331                        });
2332                        token_start += 1;
2333                    }
2334                    '@' => {
2335                        token = token.strip_prefix('@').unwrap().to_owned();
2336                        working_key = "developer".to_owned();
2337                        positions.push(ElementPosition {
2338                            element: ElementType::MODIFIER,
2339                            value: "@".to_owned(),
2340                            start: token_start,
2341                            end: token_start + 1,
2342                        });
2343                        token_start += 1;
2344                    }
2345                    _ => (),
2346                }
2347            }
2348        }
2349
2350        if token.starts_with('"') {
2351            token = token.strip_prefix('"').unwrap().to_owned();
2352            // Opening quote
2353            capturing_quotes = true;
2354        }
2355
2356        if capturing_quotes {
2357            // Inside quotes, add to working value
2358            if working_value == "" {
2359                // Start of value
2360                working_value = token.to_owned();
2361            } else {
2362                // Continued value
2363                working_value.push_str(&format!(" {}", token));
2364            }
2365        }
2366
2367        if token.ends_with('"') && capturing_quotes {
2368            // Closing quote
2369            capturing_quotes = false;
2370            // Remove quote at end of working value, if doesn't exist then it's a broken quoted value
2371            working_value = working_value.strip_suffix('"').unwrap().to_owned();
2372        }
2373
2374        if capturing_quotes {
2375            // Still in capture mode, get next token
2376            current_pos += raw_token.len() + 1;
2377            continue;
2378        }
2379
2380        if working_value == "" {
2381            // No working input yet, check for key
2382            working_key_char = earliest_key_char(&token);
2383
2384            // Extract the working key
2385            if let Some(kc) = working_key_char.clone() {
2386                let s: String = kc.into();
2387                let token_parts = token.split(&s).collect::<Vec<&str>>();
2388                if token_parts.len() > 1 {
2389                    // Has a key
2390                    debug_println!("key {:?}", &token_parts[0]);
2391                    working_key = token_parts[0].to_owned();
2392                    token = token_parts
2393                        .into_iter()
2394                        .skip(1)
2395                        .collect::<Vec<&str>>()
2396                        .join(&s);
2397                    debug_println!("value {:?}", &token);
2398                    positions.push(ElementPosition {
2399                        element: ElementType::KEY,
2400                        value: working_key.clone(),
2401                        start: token_start,
2402                        end: token_start + working_key.len().try_into().unwrap_or(0),
2403                    });
2404                    token_start += working_key.len().try_into().unwrap_or(0);
2405                } else {
2406                    token = token_parts[0].to_owned();
2407                }
2408            }
2409
2410            // Single value, must be value
2411            if token.starts_with('"') && token.ends_with('"') {
2412                // Special case for empty value
2413                if token.len() == 2 {
2414                    if working_key != "" {
2415                        // Has a key, must be a deliberately empty value
2416                        working_value = REPLACEMENT.to_owned();
2417                    }
2418                } else {
2419                    // Fully inside quotes
2420                    token = token.strip_prefix('"').unwrap_or_else(|| "").to_owned();
2421                    token = token.strip_suffix('"').unwrap_or_else(|| "").to_owned();
2422                    working_value = token.to_owned();
2423                }
2424            } else {
2425                if token.starts_with('"') {
2426                    // Starts quotes
2427                    token = token.strip_prefix('"').unwrap().to_owned();
2428                    capturing_quotes = true;
2429                    working_value = token.to_owned();
2430                    continue;
2431                }
2432                working_value = token.to_owned();
2433            }
2434        }
2435
2436        if working_value != "" {
2437            let mut exact = false;
2438            if working_key != "" {
2439                if working_value == REPLACEMENT {
2440                    // Is an empty replacement value, swap it back in now we know it exists
2441                    working_value = "".to_owned();
2442                    exact = true;
2443                } else {
2444                    if let Some(kc) = &working_key_char {
2445                        match kc {
2446                            KeyChar::EQUALS => exact = true,
2447                            _ => (),
2448                        }
2449                    }
2450                }
2451            }
2452
2453            debug_println!(
2454                "key: {}, value: {}, negative: {}, exact: {}",
2455                working_key,
2456                working_value,
2457                negative,
2458                exact,
2459            );
2460
2461            let mut list = match (negative, exact) {
2462                (true, false) => filter.blacklist.clone(),
2463                (false, false) => filter.whitelist.clone(),
2464                (true, true) => filter.exact_blacklist.clone(),
2465                (false, true) => filter.exact_whitelist.clone(),
2466            };
2467            let value = working_value.clone();
2468
2469            if let Some(kc) = &working_key_char {
2470                positions.push(ElementPosition {
2471                    element: ElementType::KEYCHAR,
2472                    value: kc.to_owned().into(),
2473                    start: token_start,
2474                    end: token_start + 1,
2475                });
2476                token_start += 1;
2477            }
2478
2479            // Track position of the value
2480            positions.push(ElementPosition {
2481                element: ElementType::VALUE,
2482                value: working_value.clone(),
2483                start: token_start,
2484                end: token_start + working_value.len().try_into().unwrap_or(0),
2485            });
2486
2487            // Handle boolean comparisons
2488            let mut processed: bool = true;
2489            
2490            match working_key.to_lowercase().as_str() {
2491                "installed" => {
2492                    let mut value = !(working_value.to_lowercase() == "no"
2493                        && working_value.to_lowercase() == "false"
2494                        && working_value.to_lowercase() == "0");
2495                    if negative {
2496                        value = !value;
2497                    }
2498
2499                    filter.bool_comp.installed = Some(value);
2500                }
2501                _ => {
2502                    // Check if this is a searchable key registered by an extension
2503                    if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2504                        if ext_searchable.value_type == ExtSearchableType::Boolean {
2505                            let mut value = !(working_value.to_lowercase() == "no"
2506                                && working_value.to_lowercase() == "false"
2507                                && working_value.to_lowercase() == "0");
2508                            if negative {
2509                                value = !value;
2510                            }
2511
2512                            // Unwrap or create a new extensions filter
2513                            let mut inner_filter = filter.bool_comp.ext.unwrap_or_default();
2514                            // Insert a new map for the extension that owns this searchable if missing
2515                            let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2516                            ext_filter.insert(ext_searchable.key.clone(), value);
2517                            filter.bool_comp.ext = Some(inner_filter);
2518                        } else {
2519                            processed = false;
2520                        }
2521                    } else {
2522                        processed = false;
2523                    }            
2524                }
2525            }
2526
2527            // Handle numerical comparisons
2528            if !processed {
2529                if let Some(kc) = &working_key_char {
2530                    processed = true;
2531                    match kc {
2532                        KeyChar::LOWER => {
2533                            let value = coerce_to_i64(&working_value);
2534                            match working_key.to_lowercase().as_str() {
2535                                "tags" => filter.lower_than.tags = Some(value),
2536                                "platforms" => filter.lower_than.platforms = Some(value),
2537                                "dateadded" | "da" => {
2538                                    filter.lower_than.date_added = Some(working_value.clone())
2539                                }
2540                                "datemodified" | "dm" => {
2541                                    filter.lower_than.date_modified = Some(working_value.clone())
2542                                }
2543                                "releasedate" | "rd" => {
2544                                    filter.lower_than.release_date = Some(working_value.clone())
2545                                }
2546                                "gamedata" | "gd" => filter.lower_than.game_data = Some(value),
2547                                "addapps" | "aa" => filter.lower_than.add_apps = Some(value),
2548                                "playtime" | "pt" => filter.lower_than.playtime = Some(value),
2549                                "playcount" | "pc" => filter.lower_than.playcount = Some(value),
2550                                "lastplayed" | "lp" => {
2551                                    filter.lower_than.last_played = Some(working_value.clone())
2552                                }
2553                                _ => {
2554                                    // Check if this is a searchable key registered by an extension
2555                                    if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2556                                        if ext_searchable.value_type == ExtSearchableType::Number {
2557                                            // Unwrap or create a new extensions filter
2558                                            let mut inner_filter = filter.lower_than.ext.unwrap_or_default();
2559                                            // Insert a new map for the extension that owns this searchable if missing
2560                                            let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2561                                            ext_filter.insert(ext_searchable.key.clone(), value);
2562                                            filter.lower_than.ext = Some(inner_filter);
2563                                        } else {
2564                                            processed = false;
2565                                        }
2566                                    } else {
2567                                        processed = false;
2568                                    }                           
2569                                }
2570                            }
2571                        }
2572                        KeyChar::HIGHER => {
2573                            let value = coerce_to_i64(&working_value);
2574                            match working_key.to_lowercase().as_str() {
2575                                "tags" => filter.higher_than.tags = Some(value),
2576                                "platforms" => filter.higher_than.platforms = Some(value),
2577                                "dateadded" | "da" => {
2578                                    filter.higher_than.date_added = Some(working_value.clone())
2579                                }
2580                                "datemodified" | "dm" => {
2581                                    filter.higher_than.date_modified = Some(working_value.clone())
2582                                }
2583                                "releasedate" | "rd" => {
2584                                    filter.higher_than.release_date = Some(working_value.clone())
2585                                }
2586                                "gamedata" | "gd" => filter.higher_than.game_data = Some(value),
2587                                "addapps" | "aa" => filter.higher_than.add_apps = Some(value),
2588                                "playtime" | "pt" => filter.higher_than.playtime = Some(value),
2589                                "playcount" | "pc" => filter.higher_than.playcount = Some(value),
2590                                "lastplayed" | "lp" => {
2591                                    filter.higher_than.last_played = Some(working_value.clone())
2592                                }
2593                                _ => {
2594                                    // Check if this is a searchable key registered by an extension
2595                                    if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2596                                        if ext_searchable.value_type == ExtSearchableType::Number {
2597                                            // Unwrap or create a new extensions filter
2598                                            let mut inner_filter = filter.higher_than.ext.unwrap_or_default();
2599                                            // Insert a new map for the extension that owns this searchable if missing
2600                                            let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2601                                            ext_filter.insert(ext_searchable.key.clone(), value);
2602                                            filter.higher_than.ext = Some(inner_filter);
2603                                        } else {
2604                                            processed = false;
2605                                        }
2606                                    } else {
2607                                        processed = false;
2608                                    }
2609                                }
2610                            }
2611                        }
2612                        KeyChar::MATCHES | KeyChar::EQUALS => {
2613                            let value = coerce_to_i64(&working_value);
2614                            match working_key.to_lowercase().as_str() {
2615                                "tags" => filter.equal_to.tags = Some(value),
2616                                "platforms" => filter.equal_to.platforms = Some(value),
2617                                "dateadded" | "da" => {
2618                                    filter.equal_to.date_added = Some(working_value.clone())
2619                                }
2620                                "datemodified" | "dm" => {
2621                                    filter.equal_to.date_modified = Some(working_value.clone())
2622                                }
2623                                "releasedate" | "rd" => {
2624                                    filter.equal_to.release_date = Some(working_value.clone())
2625                                }
2626                                "gamedata" | "gd" => filter.equal_to.game_data = Some(value),
2627                                "addapps" | "aa" => filter.equal_to.add_apps = Some(value),
2628                                "playtime" | "pt" => filter.equal_to.playtime = Some(value),
2629                                "playcount" | "pc" => filter.equal_to.playcount = Some(value),
2630                                "lastplayed" | "lp" => {
2631                                    filter.equal_to.last_played = Some(working_value.clone())
2632                                }
2633                                _ => {
2634                                    // Check if this is a searchable key registered by an extension
2635                                    if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2636                                        if ext_searchable.value_type == ExtSearchableType::Number {
2637                                            // Unwrap or create a new extensions filter
2638                                            let mut inner_filter = filter.equal_to.ext.unwrap_or_default();
2639                                            // Insert a new map for the extension that owns this searchable if missing
2640                                            let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2641                                            ext_filter.insert(ext_searchable.key.clone(), value);
2642                                            filter.equal_to.ext = Some(inner_filter);
2643                                        } else {
2644                                            processed = false;
2645                                        }
2646                                    } else {
2647                                        processed = false;
2648                                    }   
2649                                }
2650                            }
2651                        }
2652                    }
2653                }
2654            }
2655
2656            // Handle generics and string matchers
2657            if !processed {
2658                // Has a complete value, add to filter
2659                match working_key.to_lowercase().as_str() {
2660                    "id" => list.id.push(value),
2661                    "lib" | "library" => list.library.push(value),
2662                    "title" => list.title.push(value),
2663                    "dev" | "developer" => list.developer.push(value),
2664                    "pub" | "publisher" => list.publisher.push(value),
2665                    "series" => list.series.push(value),
2666                    "tag" => list.tags.push(value),
2667                    "plat" | "platform" => list.platforms.push(value),
2668                    "mode" | "playmode" => list.play_mode.push(value),
2669                    "status" => list.status.push(value),
2670                    "note" | "notes" => list.notes.push(value),
2671                    "src" | "source" => list.source.push(value),
2672                    "od" | "desc" | "description" | "originaldescription" => {
2673                        list.original_description.push(value)
2674                    }
2675                    "lang" | "language" => list.language.push(value),
2676                    "ap" | "path" | "app" | "applicationpath" => list.application_path.push(value),
2677                    "lc" | "launchcommand" => list.launch_command.push(value),
2678                    "ruffle" | "rufflesupport" => list.ruffle_support.push(value.to_lowercase()),
2679                    "owner" => list.owner.push(value),
2680                    _ => {
2681                        let processed = if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2682                            if ext_searchable.value_type == ExtSearchableType::String {
2683                                // Insert a new map for the extension that owns this searchable if missing
2684                                let ext_filter = list.ext.insert_or_get(ext_searchable.ext_id.clone());
2685                                let ext_list = ext_filter.insert_or_get(ext_searchable.key.clone());
2686                                ext_list.push(value.clone());
2687
2688                                true
2689                            } else {
2690                                false
2691                            }
2692                        } else { 
2693                            false
2694                        };
2695                        if !processed {
2696                            // Reform the full search term if it contained a key character
2697                            let value = match &working_key_char {
2698                                Some(kc) => {
2699                                    let ks: String = kc.clone().into();
2700                                    let full_value = working_key.clone() + &ks + &value;
2701                                    full_value
2702                                }
2703                                None => value,
2704                            };
2705
2706                            list.generic.push(value);
2707                        }
2708                    },
2709                }
2710
2711                match (negative, exact) {
2712                    (true, false) => filter.blacklist = list,
2713                    (false, false) => filter.whitelist = list,
2714                    (true, true) => filter.exact_blacklist = list,
2715                    (false, true) => filter.exact_whitelist = list,
2716                }
2717            }
2718
2719            negative = false;
2720            working_value.clear();
2721            working_key.clear();
2722        }
2723        current_pos += raw_token.len() + 1;
2724    }
2725
2726    search.filter = (&filter).into();
2727
2728    ParsedInput { search, positions }
2729}
2730
2731#[derive(Debug, Clone, PartialEq)]
2732enum KeyChar {
2733    MATCHES,
2734    LOWER,
2735    HIGHER,
2736    EQUALS,
2737}
2738
2739impl Into<String> for KeyChar {
2740    fn into(self) -> String {
2741        match self {
2742            KeyChar::MATCHES => ":".to_owned(),
2743            KeyChar::LOWER => "<".to_owned(),
2744            KeyChar::HIGHER => ">".to_owned(),
2745            KeyChar::EQUALS => "=".to_owned(),
2746        }
2747    }
2748}
2749
2750const KEY_CHARS: [&str; 4] = [":", "<", ">", "="];
2751
2752fn earliest_key_char(s: &str) -> Option<KeyChar> {
2753    let mut earliest_pos = None;
2754    let mut earliest_key_char = None;
2755
2756    for key_char in KEY_CHARS {
2757        if let Some(pos) = s.find(key_char) {
2758            if earliest_pos.is_none() || pos < earliest_pos.unwrap() {
2759                earliest_pos = Some(pos);
2760                earliest_key_char = Some(key_char);
2761            }
2762        }
2763    }
2764
2765    match earliest_key_char {
2766        Some(ekc) => match ekc {
2767            ":" => Some(KeyChar::MATCHES),
2768            "<" => Some(KeyChar::LOWER),
2769            ">" => Some(KeyChar::HIGHER),
2770            "=" => Some(KeyChar::EQUALS),
2771            _ => None,
2772        },
2773        None => None,
2774    }
2775}
2776
2777fn coerce_to_i64(input: &str) -> i64 {
2778    // Substitute known replacements
2779    /* d - Seconds in a day
2780     * h - Seconds in an hour
2781     * m - seconds in a minute
2782     */
2783
2784    // Insert '+' between consecutive time values (e.g., "1h30m" becomes "1h+30m")
2785    let insert_plus_re = Regex::new(r"(\d+)([yMwdhm])(?=\d)").unwrap();
2786    let mut processed_input = insert_plus_re
2787        .replace_all(&input, |caps: &Captures| {
2788            format!("{}{}+", &caps[1], &caps[2])
2789        })
2790        .to_string();
2791
2792    let time_units = vec![
2793        ("y", 31_536_000), // years
2794        ("M", 2_592_000),  // months
2795        ("w", 604_800),    // weeks
2796        ("d", 86_400),     // days
2797        ("h", 3_600),      // hours
2798        ("m", 60),         // minutes
2799        ("s", 1),          // seconds
2800    ];
2801
2802    // Replace each unit eg 30m with their seconds integer value e.g 1800
2803    for (unit, seconds) in time_units {
2804        let pattern = format!(r"(\d+){}", unit);
2805        let re = Regex::new(&pattern).unwrap();
2806        processed_input = re
2807            .replace_all(&processed_input, |caps: &Captures| {
2808                let time_value: i64 = caps[1].parse().unwrap_or(0); // Convert the captured group to i64
2809                (time_value * seconds).to_string() // Replace with time_value * seconds per unit
2810            })
2811            .to_string();
2812    }
2813
2814    // Evaluate the mathematical expression we've made
2815    match meval::eval_str(&processed_input) {
2816        Ok(num) => num as i64,
2817        Err(_) => 0,
2818    }
2819}