//! Persisted user preferences — DYN, POLAR (+ increment), OTRACK, and assorted
//! app-level flags (backup, autosave, plugin lists, viewport background). These
//! are UI choices, not drawing data, so they live in the consolidated per-user
//! config ([`crate::app::config`], the "settings" section) and survive across
//! sessions. Drawing-scoped state (Ortho `$ORTHOMODE`, running OSNAP `$OSMODE`,
//! lineweight display `$LWDISPLAY`, …) belongs to the file, not here.
//!
//! Also home to the `$OSMODE` bit conversions ([`osmode_from_snaps`] /
//! [`snaps_from_osmode`]) that bridge the running-snap set and the drawing header.

use crate::snap::SnapType;
use serde::{Deserialize, Serialize};

/// Canonical order of the user-toggleable object-snap modes. Drives the
/// deterministic order when decoding the `$OSMODE` bitmask (see
/// [`snaps_from_osmode`]).
const SNAP_ORDER: &[SnapType] = &[
    SnapType::Endpoint,
    SnapType::Midpoint,
    SnapType::Center,
    SnapType::Node,
    SnapType::Quadrant,
    SnapType::Intersection,
    SnapType::Extension,
    SnapType::Insertion,
    SnapType::Perpendicular,
    SnapType::Tangent,
    SnapType::Nearest,
    SnapType::ApparentIntersection,
    SnapType::Parallel,
    // Grid snap (SNAPMODE) is a per-drawing view setting stored on the VPort,
    // not a global OSNAP preference, so it is deliberately excluded from the
    // persisted set. (#121)
];

/// `$OSMODE` bit for each running object-snap mode (standard AutoCAD bitmask).
/// `None` for OCS-only snaps (Grid, ObjectPick) that have no standard bit.
fn snap_bit(s: SnapType) -> Option<i32> {
    Some(match s {
        SnapType::Endpoint => 1,
        SnapType::Midpoint => 2,
        SnapType::Center => 4,
        SnapType::Node => 8,
        SnapType::Quadrant => 16,
        SnapType::Intersection => 32,
        SnapType::Insertion => 64,
        SnapType::Perpendicular => 128,
        SnapType::Tangent => 256,
        SnapType::Nearest => 512,
        SnapType::ApparentIntersection => 2048,
        SnapType::Extension => 4096,
        SnapType::Parallel => 8192,
        SnapType::Grid | SnapType::ObjectPick => return None,
    })
}

/// Bit 14 of `$OSMODE` — object snap turned off (master suppress).
const OSMODE_SUPPRESS: i32 = 16384;

/// Encode the running-snap set + master toggle into an `$OSMODE` bitmask for the
/// drawing header. OCS-only snaps (Grid, ObjectPick) have no bit and are
/// dropped. A cleared master toggle sets the suppress bit.
pub(crate) fn osmode_from_snaps<'a>(
    enabled: impl IntoIterator<Item = &'a SnapType>,
    snap_enabled: bool,
) -> i32 {
    let mut bits = 0;
    for t in enabled {
        if let Some(b) = snap_bit(*t) {
            bits |= b;
        }
    }
    if !snap_enabled {
        bits |= OSMODE_SUPPRESS;
    }
    bits
}

/// Decode an `$OSMODE` bitmask into `(running-snap set, master enabled)`. Only
/// the standard mappable modes are produced; the suppress bit maps (inverted) to
/// the master toggle.
pub(crate) fn snaps_from_osmode(osmode: i32) -> (Vec<SnapType>, bool) {
    let modes = SNAP_ORDER
        .iter()
        .copied()
        .filter(|t| snap_bit(*t).is_some_and(|b| osmode & b != 0))
        .collect();
    (modes, osmode & OSMODE_SUPPRESS == 0)
}

/// The "settings" section of the consolidated config ([`crate::app::config`]).
/// Field defaults mirror the app's in-code defaults so a missing key restores
/// the value the app boots with.
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct UserSettings {
    pub dyn_input: bool,
    pub polar: bool,
    pub polar_increment_deg: f32,
    pub otrack: bool,
    // Ortho ($ORTHOMODE) and the running OSNAP set ($OSMODE) are per-drawing —
    // stored in the document header, not here (they used to be persisted app-
    // globally, which duplicated the file's own state).
    /// Whether the one-time "make Open CAD Studio the default for .dwg/.dxf?"
    /// prompt has already been shown. Set once the user answers (either way),
    /// so we never nag again on subsequent launches.
    pub default_assoc_prompted: bool,
    /// Ids of plugins the user turned off in the Plugin Manager. Disabled
    /// plugins keep their manifest listed but drop their ribbon tab and command
    /// dispatch.
    pub disabled_plugins: Vec<String>,
    /// Linked plugin source repositories (`owner/repo`) the marketplace installs
    /// from.
    pub plugin_repos: Vec<String>,
    /// Command-line literal-space mode: when on, Space stays in the input
    /// instead of submitting (as if every line started with `>`), until the
    /// user toggles it back off.
    pub literal_spaces: bool,
    /// Running object-snap set + master toggle as an `$OSMODE`-style bitmask.
    /// App-level, not per-drawing: modern DWG (R2000+) has no file slot for
    /// OSMODE (it moved to the registry), so the set follows the user. A
    /// legacy R13/R14 or DXF file carrying a nonzero `$OSMODE` still overrides
    /// it on open (see `adopt_header_sysvars`).
    pub osmode: i32,
    /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
    pub texteditmode: bool,
    /// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
    pub textfill: bool,
    /// When true, saving over an existing file first copies it to a sibling
    /// `<name>.bak` so a faulty or accidental save can be recovered (#205).
    pub backup_on_save: bool,
    /// When true (default), the app (re)registers itself as a .dwg/.dxf/.bak
    /// handler on every launch. Toggle with the FILEASSOC command.
    pub file_assoc_enabled: bool,
    /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command).
    /// 0 disables autosave.
    pub savetime_min: i32,
    /// PICKADD (#226): `true` (default) = a plain click ADDS to the selection
    /// (Shift removes); `false` = OS-style — a click REPLACES the selection
    /// and Shift+click toggles membership.
    pub pick_add: bool,
    /// PICKDRAG (#226): `false` (default) = press-drag draws the freeform
    /// lasso; `true` = press-drag draws a rectangle marquee instead.
    pub pick_drag_rect: bool,
    /// Persisted viewport background colours (0–255 RGB); `None` = app default
    /// (dark grey model / off-white paper). Applied to every drawing tab on
    /// launch and to tabs opened later, so a chosen background survives restarts
    /// (#188).
    pub bg_color: Option<[u8; 3]>,
    pub paper_bg_color: Option<[u8; 3]>,
}

impl Default for UserSettings {
    fn default() -> Self {
        Self {
            dyn_input: true,
            polar: false,
            polar_increment_deg: 45.0,
            otrack: false,
            default_assoc_prompted: false,
            disabled_plugins: Vec::new(),
            plugin_repos: Vec::new(),
            literal_spaces: false,
            // Snapper::default(): END|MID|CEN|NODE|QUAD|INT|NEA (575), master
            // off (suppress bit 16384).
            osmode: 575 | OSMODE_SUPPRESS,
            texteditmode: false,
            textfill: true,
            backup_on_save: true,
            file_assoc_enabled: true,
            savetime_min: 10,
            pick_add: true,
            pick_drag_rect: false,
            bg_color: None,
            paper_bg_color: None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn osmode_encodes_bits_and_suppress() {
        // Endpoint(1) + Midpoint(2) + Intersection(32) = 35, master on.
        let on = [SnapType::Endpoint, SnapType::Midpoint, SnapType::Intersection];
        assert_eq!(osmode_from_snaps(on.iter(), true), 35);
        // Master off sets the suppress bit (16384).
        assert_eq!(osmode_from_snaps(on.iter(), false), 35 | 16384);
        // OCS-only snaps carry no bit and are dropped.
        assert_eq!(osmode_from_snaps([SnapType::Grid, SnapType::ObjectPick].iter(), true), 0);
    }

    #[test]
    fn osmode_decodes_bits_and_suppress() {
        let (modes, enabled) = snaps_from_osmode(35);
        let set: std::collections::HashSet<_> = modes.into_iter().collect();
        assert!(enabled);
        assert_eq!(set.len(), 3);
        assert!(set.contains(&SnapType::Endpoint));
        assert!(set.contains(&SnapType::Midpoint));
        assert!(set.contains(&SnapType::Intersection));
        // Suppress bit → master off; the mode bits still decode.
        let (_m, en) = snaps_from_osmode(35 | 16384);
        assert!(!en);
    }

    #[test]
    fn osmode_round_trips_every_mappable_mode() {
        // All 13 running snaps + master on encode and decode back to the same set.
        let all: Vec<SnapType> = SNAP_ORDER.to_vec();
        let bits = osmode_from_snaps(all.iter(), true);
        let (back, enabled) = snaps_from_osmode(bits);
        assert!(enabled);
        let a: std::collections::HashSet<_> = all.into_iter().collect();
        let b: std::collections::HashSet<_> = back.into_iter().collect();
        assert_eq!(a, b);
    }
}
