/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2026 metamuffin <metamuffin.org>
*/

use crate::{Filter, Value};
use jellyobject::{Object, Path};

/// Sorted list of components to bin objects by filtered values.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Binning(pub Vec<BinningComponent>);

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum BinningComponent {
    Has(Path),
    Match(Path),
}

impl Binning {
    pub fn new(mut comps: Vec<BinningComponent>) -> Self {
        comps.sort();
        Self(comps)
    }
    pub fn apply(&self, ob: Object<'_>, keys: &mut Vec<Vec<u8>>) {
        for f in &self.0 {
            f.apply(ob, keys);
        }
    }
}
impl BinningComponent {
    pub fn apply(&self, ob: Object<'_>, keys: &mut Vec<Vec<u8>>) {
        match self {
            BinningComponent::Has(path) => {
                if path.get_matching_value(ob).is_none() {
                    keys.clear();
                }
            }
            BinningComponent::Match(path) => {
                let mut new_out = Vec::new();
                for value in path.get_matching_values(ob) {
                    for mut co in keys.clone() {
                        co.extend((value.len() as u32).to_be_bytes());
                        co.extend(value);
                        new_out.push(co);
                    }
                }
                *keys = new_out;
            }
        }
    }
}

impl Filter<'_> {
    pub fn get_binnings(&self) -> Vec<Binning> {
        self.get_bins_inner()
            .into_iter()
            .map(|e| Binning(e.into_iter().map(|(e, _)| e).collect()))
            .collect()
    }
    pub fn get_bins(&self) -> Vec<(Binning, Vec<u8>)> {
        self.get_bins_inner()
            .into_iter()
            .map(|e| {
                let (a, b): (Vec<BinningComponent>, Vec<Vec<u8>>) = e.into_iter().unzip();
                (Binning(a), b.into_iter().flatten().collect())
            })
            .collect()
    }
    fn get_bins_inner(&self) -> Vec<Vec<(BinningComponent, Vec<u8>)>> {
        match self {
            Filter::True => vec![vec![]],
            Filter::All(filters) => {
                let mut o = vec![vec![]];
                for filter in filters {
                    let mut new_o = Vec::new();
                    for par in filter.get_bins_inner() {
                        for mut prev in o.clone() {
                            prev.extend(par.clone());
                            new_o.push(prev);
                        }
                    }
                    o = new_o;
                }
                o
            }
            Filter::Any(filters) => filters.iter().flat_map(|f| f.get_bins_inner()).collect(),
            Filter::Match(path, value) => {
                vec![vec![(BinningComponent::Match(path.to_owned()), {
                    let mut co = Vec::new();
                    write_value_with_len(value, &mut co);
                    co
                })]]
            }
            Filter::Has(path) => {
                vec![vec![(BinningComponent::Has(path.to_owned()), vec![])]]
            }
        }
    }
}

pub fn write_value_with_len(value: &Value, out: &mut Vec<u8>) {
    match value {
        Value::Tag(tag) => {
            out.extend(4u32.to_be_bytes());
            out.extend(tag.0.to_be_bytes());
        }
        Value::U32(x) => {
            out.extend(4u32.to_be_bytes());
            out.extend(x.to_be_bytes());
        }
        Value::U64(x) => {
            out.extend(8u32.to_be_bytes());
            out.extend(x.to_be_bytes());
        }
        Value::I64(x) => {
            out.extend(8u32.to_be_bytes());
            out.extend(x.to_be_bytes());
        }
        Value::String(s) => {
            out.extend((s.len() as u32).to_be_bytes());
            out.extend(s.as_bytes());
        }
        Value::Binary(s) => {
            out.extend((s.len() as u32).to_be_bytes());
            out.extend(&**s);
        }
    }
}

#[cfg(test)]
mod test {
    use jellyobject::{Path, Tag};

    use crate::{
        Filter,
        kv::binning::{Binning, BinningComponent},
    };

    #[test]
    fn all() {
        let f = Filter::All(vec![
            Filter::Has(Path(vec![Tag(0)])),
            Filter::Has(Path(vec![Tag(1)])),
        ]);
        let bins = vec![Binning(vec![
            BinningComponent::Has(Path(vec![Tag(0)])),
            BinningComponent::Has(Path(vec![Tag(1)])),
        ])];
        assert_eq!(f.get_binnings(), bins)
    }

    #[test]
    fn any() {
        let f = Filter::Any(vec![
            Filter::Has(Path(vec![Tag(0)])),
            Filter::Has(Path(vec![Tag(1)])),
        ]);
        let bins = vec![
            Binning::new(vec![BinningComponent::Has(Path(vec![Tag(0)]))]),
            Binning::new(vec![BinningComponent::Has(Path(vec![Tag(1)]))]),
        ];
        assert_eq!(f.get_binnings(), bins)
    }

    #[test]
    fn nested() {
        let f = Filter::All(vec![
            Filter::Any(vec![
                Filter::Has(Path(vec![Tag(0)])),
                Filter::Has(Path(vec![Tag(1)])),
            ]),
            Filter::Any(vec![
                Filter::Has(Path(vec![Tag(2)])),
                Filter::Has(Path(vec![Tag(3)])),
            ]),
        ]);
        let bins = vec![
            Binning::new(vec![
                BinningComponent::Has(Path(vec![Tag(0)])),
                BinningComponent::Has(Path(vec![Tag(2)])),
            ]),
            Binning::new(vec![
                BinningComponent::Has(Path(vec![Tag(1)])),
                BinningComponent::Has(Path(vec![Tag(2)])),
            ]),
            Binning::new(vec![
                BinningComponent::Has(Path(vec![Tag(0)])),
                BinningComponent::Has(Path(vec![Tag(3)])),
            ]),
            Binning::new(vec![
                BinningComponent::Has(Path(vec![Tag(1)])),
                BinningComponent::Has(Path(vec![Tag(3)])),
            ]),
        ];
        assert_eq!(f.get_binnings(), bins)
    }
}
