use std::iter;

#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub(crate) struct BoolSettingIndex(usize);

#[derive(Hash, PartialEq, Eq)]
pub(crate) struct BoolSetting {
    pub default: bool,
    pub bit_offset: u8,
    pub predicate_number: u8,
}

#[derive(Hash, PartialEq, Eq)]
pub(crate) enum SpecificSetting {
    Bool(BoolSetting),
    Enum(Vec<&'static str>),
    Num(u8),
}

#[derive(Hash, PartialEq, Eq)]
pub(crate) struct Setting {
    pub name: &'static str,
    pub comment: &'static str,
    pub specific: SpecificSetting,
    pub byte_offset: u8,
}

impl Setting {
    pub fn default_byte(&self) -> u8 {
        match self.specific {
            SpecificSetting::Bool(BoolSetting {
                default,
                bit_offset,
                ..
            }) => {
                if default {
                    1 << bit_offset
                } else {
                    0
                }
            }
            SpecificSetting::Enum(_) => 0,
            SpecificSetting::Num(default) => default,
        }
    }

    fn byte_for_value(&self, v: bool) -> u8 {
        match self.specific {
            SpecificSetting::Bool(BoolSetting { bit_offset, .. }) => {
                if v {
                    1 << bit_offset
                } else {
                    0
                }
            }
            _ => panic!("byte_for_value shouldn't be used for non-boolean settings."),
        }
    }

    fn byte_mask(&self) -> u8 {
        match self.specific {
            SpecificSetting::Bool(BoolSetting { bit_offset, .. }) => 1 << bit_offset,
            _ => panic!("byte_for_value shouldn't be used for non-boolean settings."),
        }
    }
}

#[derive(Hash, PartialEq, Eq)]
pub(crate) struct PresetIndex(usize);

#[derive(Hash, PartialEq, Eq)]
pub(crate) enum PresetType {
    BoolSetting(BoolSettingIndex),
    OtherPreset(PresetIndex),
}

impl Into<PresetType> for BoolSettingIndex {
    fn into(self) -> PresetType {
        PresetType::BoolSetting(self)
    }
}
impl Into<PresetType> for PresetIndex {
    fn into(self) -> PresetType {
        PresetType::OtherPreset(self)
    }
}

#[derive(Hash, PartialEq, Eq)]
pub(crate) struct Preset {
    pub name: &'static str,
    values: Vec<BoolSettingIndex>,
}

impl Preset {
    pub fn layout(&self, group: &SettingGroup) -> Vec<(u8, u8)> {
        let mut layout: Vec<(u8, u8)> = iter::repeat((0, 0))
            .take(group.settings_size as usize)
            .collect();
        for bool_index in &self.values {
            let setting = &group.settings[bool_index.0];
            let mask = setting.byte_mask();
            let val = setting.byte_for_value(true);
            assert!((val & !mask) == 0);
            let (ref mut l_mask, ref mut l_val) =
                *layout.get_mut(setting.byte_offset as usize).unwrap();
            *l_mask |= mask;
            *l_val = (*l_val & !mask) | val;
        }
        layout
    }
}

pub(crate) struct SettingGroup {
    pub name: &'static str,
    pub settings: Vec<Setting>,
    pub bool_start_byte_offset: u8,
    pub settings_size: u8,
    pub presets: Vec<Preset>,
    pub predicates: Vec<Predicate>,
}

impl SettingGroup {
    fn num_bool_settings(&self) -> u8 {
        self.settings
            .iter()
            .filter(|s| {
                if let SpecificSetting::Bool(_) = s.specific {
                    true
                } else {
                    false
                }
            })
            .count() as u8
    }

    pub fn byte_size(&self) -> u8 {
        let num_predicates = self.num_bool_settings() + (self.predicates.len() as u8);
        self.bool_start_byte_offset + (num_predicates + 7) / 8
    }

    pub fn get_bool(&self, name: &'static str) -> (BoolSettingIndex, &Self) {
        for (i, s) in self.settings.iter().enumerate() {
            if let SpecificSetting::Bool(_) = s.specific {
                if s.name == name {
                    return (BoolSettingIndex(i), self);
                }
            }
        }
        panic!("Should have found bool setting by name.");
    }

    pub fn predicate_by_name(&self, name: &'static str) -> SettingPredicateNumber {
        self.predicates
            .iter()
            .find(|pred| pred.name == name)
            .unwrap_or_else(|| panic!("unknown predicate {}", name))
            .number
    }
}

/// This is the basic information needed to track the specific parts of a setting when building
/// them.
pub(crate) enum ProtoSpecificSetting {
    Bool(bool),
    Enum(Vec<&'static str>),
    Num(u8),
}

/// This is the information provided during building for a setting.
struct ProtoSetting {
    name: &'static str,
    comment: &'static str,
    specific: ProtoSpecificSetting,
}

#[derive(Hash, PartialEq, Eq)]
pub(crate) enum PredicateNode {
    OwnedBool(BoolSettingIndex),
    SharedBool(&'static str, &'static str),
    Not(Box<PredicateNode>),
    And(Box<PredicateNode>, Box<PredicateNode>),
}

impl Into<PredicateNode> for BoolSettingIndex {
    fn into(self) -> PredicateNode {
        PredicateNode::OwnedBool(self)
    }
}
impl<'a> Into<PredicateNode> for (BoolSettingIndex, &'a SettingGroup) {
    fn into(self) -> PredicateNode {
        let (index, group) = (self.0, self.1);
        let setting = &group.settings[index.0];
        PredicateNode::SharedBool(group.name, setting.name)
    }
}

impl PredicateNode {
    fn render(&self, group: &SettingGroup) -> String {
        match *self {
            PredicateNode::OwnedBool(bool_setting_index) => format!(
                "{}.{}()",
                group.name, group.settings[bool_setting_index.0].name
            ),
            PredicateNode::SharedBool(ref group_name, ref bool_name) => {
                format!("{}.{}()", group_name, bool_name)
            }
            PredicateNode::And(ref lhs, ref rhs) => {
                format!("{} && {}", lhs.render(group), rhs.render(group))
            }
            PredicateNode::Not(ref node) => format!("!({})", node.render(group)),
        }
    }
}

struct ProtoPredicate {
    pub name: &'static str,
    node: PredicateNode,
}

pub(crate) type SettingPredicateNumber = u8;

pub(crate) struct Predicate {
    pub name: &'static str,
    node: PredicateNode,
    pub number: SettingPredicateNumber,
}

impl Predicate {
    pub fn render(&self, group: &SettingGroup) -> String {
        self.node.render(group)
    }
}

pub(crate) struct SettingGroupBuilder {
    name: &'static str,
    settings: Vec<ProtoSetting>,
    presets: Vec<Preset>,
    predicates: Vec<ProtoPredicate>,
}

impl SettingGroupBuilder {
    pub fn new(name: &'static str) -> Self {
        Self {
            name,
            settings: Vec::new(),
            presets: Vec::new(),
            predicates: Vec::new(),
        }
    }

    fn add_setting(
        &mut self,
        name: &'static str,
        comment: &'static str,
        specific: ProtoSpecificSetting,
    ) {
        self.settings.push(ProtoSetting {
            name,
            comment,
            specific,
        })
    }

    pub fn add_bool(
        &mut self,
        name: &'static str,
        comment: &'static str,
        default: bool,
    ) -> BoolSettingIndex {
        assert!(
            self.predicates.is_empty(),
            "predicates must be added after the boolean settings"
        );
        self.add_setting(name, comment, ProtoSpecificSetting::Bool(default));
        BoolSettingIndex(self.settings.len() - 1)
    }

    pub fn add_enum(
        &mut self,
        name: &'static str,
        comment: &'static str,
        values: Vec<&'static str>,
    ) {
        self.add_setting(name, comment, ProtoSpecificSetting::Enum(values));
    }

    pub fn add_num(&mut self, name: &'static str, comment: &'static str, default: u8) {
        self.add_setting(name, comment, ProtoSpecificSetting::Num(default));
    }

    pub fn add_predicate(&mut self, name: &'static str, node: PredicateNode) {
        self.predicates.push(ProtoPredicate { name, node });
    }

    pub fn add_preset(&mut self, name: &'static str, args: Vec<PresetType>) -> PresetIndex {
        let mut values = Vec::new();
        for arg in args {
            match arg {
                PresetType::OtherPreset(index) => {
                    values.extend(self.presets[index.0].values.iter());
                }
                PresetType::BoolSetting(index) => values.push(index),
            }
        }
        self.presets.push(Preset { name, values });
        PresetIndex(self.presets.len() - 1)
    }

    /// Compute the layout of the byte vector used to represent this settings
    /// group.
    ///
    /// The byte vector contains the following entries in order:
    ///
    /// 1. Byte-sized settings like `NumSetting` and `EnumSetting`.
    /// 2. `BoolSetting` settings.
    /// 3. Precomputed named predicates.
    /// 4. Other numbered predicates, including parent predicates that need to be accessible by
    ///    number.
    ///
    /// Set `self.settings_size` to the length of the byte vector prefix that
    /// contains the settings. All bytes after that are computed, not
    /// configured.
    ///
    /// Set `self.boolean_offset` to the beginning of the numbered predicates,
    /// 2. in the list above.
    ///
    /// Assign `byte_offset` and `bit_offset` fields in all settings.
    pub fn build(self) -> SettingGroup {
        let mut group = SettingGroup {
            name: self.name,
            settings: Vec::new(),
            bool_start_byte_offset: 0,
            settings_size: 0,
            presets: Vec::new(),
            predicates: Vec::new(),
        };

        let mut byte_offset = 0;

        // Assign the non-boolean settings first.
        for s in &self.settings {
            let specific = match s.specific {
                ProtoSpecificSetting::Bool(..) => continue,
                ProtoSpecificSetting::Enum(ref values) => SpecificSetting::Enum(values.clone()),
                ProtoSpecificSetting::Num(default) => SpecificSetting::Num(default),
            };

            group.settings.push(Setting {
                name: s.name,
                comment: s.comment,
                byte_offset,
                specific,
            });

            byte_offset += 1;
        }

        group.bool_start_byte_offset = byte_offset;

        let mut predicate_number = 0;

        // Then the boolean settings.
        for s in &self.settings {
            let default = match s.specific {
                ProtoSpecificSetting::Bool(default) => default,
                ProtoSpecificSetting::Enum(_) | ProtoSpecificSetting::Num(_) => continue,
            };
            group.settings.push(Setting {
                name: s.name,
                comment: s.comment,
                byte_offset: byte_offset + predicate_number / 8,
                specific: SpecificSetting::Bool(BoolSetting {
                    default,
                    bit_offset: predicate_number % 8,
                    predicate_number,
                }),
            });
            predicate_number += 1;
        }

        assert!(
            group.predicates.is_empty(),
            "settings_size is the byte size before adding predicates"
        );
        group.settings_size = group.byte_size();

        // Sort predicates by name to ensure the same order as the Python code.
        let mut predicates = self.predicates;
        predicates.sort_by_key(|predicate| predicate.name);

        group
            .predicates
            .extend(predicates.into_iter().map(|predicate| {
                let number = predicate_number;
                predicate_number += 1;
                Predicate {
                    name: predicate.name,
                    node: predicate.node,
                    number,
                }
            }));

        group.presets.extend(self.presets);

        group
    }
}
