bellframe 0.13.2

Fast and idiomatic primitives for Change Ringing.
Documentation
#![cfg(feature = "method_lib_serde")]

use std::collections::HashMap;

use itertools::Itertools;
use serde_crate::{Deserialize, Serialize};

use crate::{
    method::{
        class::{Class, FullClass},
        generate_title,
    },
    Stage,
};

use super::{CompactMethod, LibraryMap, MethodLib};

/// A version of `MethodLib` which can be serialized into a compact format
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(crate = "serde_crate")]
pub struct MethodLibSerde {
    groups: Vec<MethodGroup>,
}

impl From<&MethodLib> for MethodLibSerde {
    /// Converting from [`MethodLib`] -> `MethodLibSerde` (ready to be serialized)
    fn from(lib: &MethodLib) -> Self {
        let mut groups: HashMap<(Stage, FullClass), Vec<&CompactMethod>> = HashMap::new();

        // Group the methods by their stage and class
        for (stage, methods) in &lib.method_map {
            for method in methods.values() {
                groups
                    .entry((*stage, method.full_class))
                    .or_default()
                    .push(method);
            }
        }

        Self {
            groups: groups
                .into_iter()
                .map(|((stage, full_class), compact_methods)| {
                    // 'Uncompact' the `CompactMethod`s
                    let methods = compact_methods
                        .into_iter()
                        .map(|comp_method| CompactMethodSerde {
                            title: comp_method.omit_class.then(|| comp_method.title()),
                            name: comp_method.name.to_owned(),
                            place_notation: comp_method.place_notation.to_owned(),
                        })
                        .collect_vec();

                    MethodGroup {
                        stage: stage.num_bells_u8(),
                        is_jump: full_class.is_jump(),
                        is_little: full_class.is_little(),
                        is_differential: full_class.is_differential(),
                        class_id: to_class_id(full_class.class()),
                        methods,
                    }
                })
                .collect_vec(),
        }
    }
}

impl From<MethodLibSerde> for MethodLib {
    fn from(m: MethodLibSerde) -> MethodLib {
        let mut unpacked_method_map: LibraryMap = HashMap::with_capacity(25);

        for group in m.groups {
            // Unpack values from the group
            let stage = Stage::new(group.stage);
            let full_class = FullClass::new(
                group.is_jump,
                group.is_little,
                group.is_differential,
                from_class_id(group.class_id).expect("Unexpected type ID found"),
            );

            // Get the group of methods that correspond to this group's stage
            let method_map = unpacked_method_map.entry(stage).or_default();

            // Go through each method and unpack it into the `method_map`
            for m in group.methods {
                // Split the `CompactMethodSerde` struct into its individual fields (which can then
                // be consumed separately without angering the borrow checker)
                let CompactMethodSerde {
                    title,
                    name,
                    place_notation,
                } = m;

                let omit_class = title.is_some();
                method_map.insert(
                    title
                        .unwrap_or_else(|| generate_title(&name, full_class, false, stage))
                        .to_lowercase(),
                    CompactMethod {
                        name,
                        full_class,
                        omit_class,
                        place_notation,
                        stage,
                    },
                );
            }
        }

        MethodLib {
            method_map: unpacked_method_map,
        }
    }
}

/// A group of methods with shared properties
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(crate = "serde_crate")]
struct MethodGroup {
    #[serde(rename = "s")]
    stage: u8,

    // Classification
    #[serde(default, skip_serializing_if = "is_false", rename = "j")]
    is_jump: bool,
    #[serde(default, skip_serializing_if = "is_false", rename = "l")]
    is_little: bool,
    #[serde(default, skip_serializing_if = "is_false", rename = "d")]
    is_differential: bool,
    #[serde(rename = "c")]
    class_id: u8,

    #[serde(rename = "m")]
    methods: Vec<CompactMethodSerde>,
}

/// A version of `MethodLib` which can be serialized into a compact format
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(crate = "serde_crate")]
struct CompactMethodSerde {
    #[serde(skip_serializing_if = "Option::is_none", rename = "t")]
    title: Option<String>,
    #[serde(rename = "n")]
    name: String,
    #[serde(rename = "p")]
    place_notation: String,
}

/// Serialization helper required to remove tons of `is_*:false` from the saved JSON
fn is_false(b: &bool) -> bool {
    !(*b)
}

/// Converts a [`Class`] into a number, which can then be recovered with `from_class_id`.  This
/// will almost certainly get removed by the compiler, because these numbers are used as
/// [`Class`]'s in-memory representation.
fn to_class_id(class: Class) -> u8 {
    match class {
        Class::Principle => 0,

        Class::Place => 1,
        Class::Bob => 2,

        Class::TrebleBob => 3,
        Class::Delight => 4,
        Class::Surprise => 5,

        Class::TreblePlace => 6,
        Class::Alliance => 7,
        Class::Hybrid => 8,
    }
}

fn from_class_id(v: u8) -> Option<Class> {
    Some(match v {
        0 => Class::Principle,

        1 => Class::Place,
        2 => Class::Bob,

        3 => Class::TrebleBob,
        4 => Class::Delight,
        5 => Class::Surprise,

        6 => Class::TreblePlace,
        7 => Class::Alliance,
        8 => Class::Hybrid,

        _ => panic!("Invalid Class ID {}", v),
    })
}