use glam::Vec4;
use runmat_builtins::{CellArray, Tensor, Value};
use runmat_plot::plots::{
    ColorMap, LineMarkerAppearance, LineStyle, MarkerStyle as PlotMarkerStyle, ShadingMode,
    SurfacePlot,
};

use crate::BuiltinResult;

use super::plotting_error;

#[derive(Clone, Copy, Debug)]
pub struct BarStyleDefaults {
    pub face_color: Vec4,
    pub bar_width: f32,
}

impl BarStyleDefaults {
    pub fn new(face_color: Vec4, bar_width: f32) -> Self {
        Self {
            face_color,
            bar_width,
        }
    }
}

#[derive(Clone, Debug)]
pub struct BarStyle {
    pub face_color: Vec4,
    pub face_alpha: f32,
    pub edge_color: Option<Vec4>,
    pub edge_alpha: f32,
    pub line_width: f32,
    pub bar_width: f32,
    pub label: Option<String>,
    pub face_color_flat: bool,
    pub layout: BarLayout,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BarLayout {
    Grouped,
    Stacked,
}

impl BarStyle {
    pub fn face_rgba(&self) -> Vec4 {
        let mut color = self.face_color;
        color.w *= self.face_alpha;
        color
    }

    pub fn edge_rgba(&self) -> Option<Vec4> {
        self.edge_color.map(|mut color| {
            color.w *= self.edge_alpha;
            color
        })
    }
}

#[derive(Clone, Copy, Debug)]
pub struct LineStyleParseOptions {
    pub builtin_name: &'static str,
    pub forbid_leading_numeric: bool,
    pub forbid_interleaved_numeric: bool,
}

impl LineStyleParseOptions {
    pub const fn plot() -> Self {
        Self {
            builtin_name: "plot",
            forbid_leading_numeric: true,
            forbid_interleaved_numeric: false,
        }
    }

    pub const fn scatter() -> Self {
        Self {
            builtin_name: "scatter",
            forbid_leading_numeric: true,
            forbid_interleaved_numeric: true,
        }
    }

    pub const fn scatter3() -> Self {
        Self {
            builtin_name: "scatter3",
            forbid_leading_numeric: true,
            forbid_interleaved_numeric: true,
        }
    }

    pub const fn stairs() -> Self {
        Self {
            builtin_name: "stairs",
            forbid_leading_numeric: true,
            forbid_interleaved_numeric: true,
        }
    }

    pub const fn generic(name: &'static str) -> Self {
        Self {
            builtin_name: name,
            forbid_leading_numeric: false,
            forbid_interleaved_numeric: false,
        }
    }
}

pub const DEFAULT_LINE_WIDTH: f32 = 2.0;
pub const DEFAULT_LINE_COLOR: Vec4 = Vec4::new(0.0, 0.4, 0.8, 1.0);
pub const DEFAULT_LINE_MARKER_SIZE: f32 = 6.0;

fn ctx_err(opts: &LineStyleParseOptions, msg: impl Into<String>) -> crate::RuntimeError {
    let message = format!("{}: {}", opts.builtin_name, msg.into());
    plotting_error(opts.builtin_name, message)
}

#[derive(Clone, Debug)]
pub struct LineAppearance {
    pub color: Vec4,
    pub line_width: f32,
    pub line_style: LineStyle,
    pub marker: Option<MarkerAppearance>,
}

impl Default for LineAppearance {
    fn default() -> Self {
        Self {
            color: DEFAULT_LINE_COLOR,
            line_width: DEFAULT_LINE_WIDTH,
            line_style: LineStyle::Solid,
            marker: None,
        }
    }
}

pub fn marker_metadata_from_appearance(
    appearance: &LineAppearance,
) -> Option<LineMarkerAppearance> {
    let marker = appearance.marker.as_ref()?;
    let size = marker.size.unwrap_or(DEFAULT_LINE_MARKER_SIZE);
    let edge_color = marker_color_to_vec4(&marker.edge_color, appearance.color);
    let face_color = match marker.face_color {
        MarkerColor::None => Vec4::new(edge_color.x, edge_color.y, edge_color.z, 0.0),
        MarkerColor::Flat => appearance.color,
        MarkerColor::Auto | MarkerColor::Color(_) => {
            marker_color_to_vec4(&marker.face_color, appearance.color)
        }
    };
    Some(LineMarkerAppearance {
        kind: marker.kind.to_plot_marker(),
        size,
        edge_color,
        face_color,
        filled: !matches!(marker.face_color, MarkerColor::None),
    })
}

pub fn marker_color_to_vec4(color: &MarkerColor, fallback: Vec4) -> Vec4 {
    match color {
        MarkerColor::Auto | MarkerColor::Flat => fallback,
        MarkerColor::None => Vec4::new(fallback.x, fallback.y, fallback.z, 0.0),
        MarkerColor::Color(value) => *value,
    }
}

#[derive(Clone, Debug)]
pub struct MarkerAppearance {
    pub kind: MarkerKind,
    pub size: Option<f32>,
    pub edge_color: MarkerColor,
    pub face_color: MarkerColor,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MarkerKind {
    Circle,
    Plus,
    Star,
    Point,
    Cross,
    TriangleUp,
    TriangleDown,
    TriangleLeft,
    TriangleRight,
    Square,
    Diamond,
    Pentagram,
    Hexagram,
}

impl MarkerKind {
    pub fn to_plot_marker(self) -> PlotMarkerStyle {
        match self {
            MarkerKind::Circle => PlotMarkerStyle::Circle,
            MarkerKind::Plus => PlotMarkerStyle::Plus,
            MarkerKind::Star => PlotMarkerStyle::Star,
            MarkerKind::Point => PlotMarkerStyle::Circle,
            MarkerKind::Cross => PlotMarkerStyle::Cross,
            MarkerKind::TriangleUp => PlotMarkerStyle::Triangle,
            MarkerKind::TriangleDown => PlotMarkerStyle::Triangle,
            MarkerKind::TriangleLeft => PlotMarkerStyle::Triangle,
            MarkerKind::TriangleRight => PlotMarkerStyle::Triangle,
            MarkerKind::Square => PlotMarkerStyle::Square,
            MarkerKind::Diamond => PlotMarkerStyle::Diamond,
            MarkerKind::Pentagram => PlotMarkerStyle::Star,
            MarkerKind::Hexagram => PlotMarkerStyle::Star,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum MarkerColor {
    Auto,
    None,
    Flat,
    Color(Vec4),
}

#[derive(Clone, Copy, Debug)]
pub struct SurfaceStyleDefaults {
    pub colormap: ColorMap,
    pub shading: ShadingMode,
    pub wireframe: bool,
    pub alpha: f32,
    pub flatten_z: bool,
    pub lighting_enabled: bool,
    pub visible: bool,
}

impl SurfaceStyleDefaults {
    pub fn new(
        colormap: ColorMap,
        shading: ShadingMode,
        wireframe: bool,
        alpha: f32,
        flatten_z: bool,
        lighting_enabled: bool,
    ) -> Self {
        Self {
            colormap,
            shading,
            wireframe,
            alpha,
            flatten_z,
            lighting_enabled,
            visible: true,
        }
    }
}

#[derive(Clone, Debug)]
pub struct SurfaceStyle {
    pub colormap: ColorMap,
    pub shading: ShadingMode,
    pub wireframe: bool,
    pub alpha: f32,
    pub flatten_z: bool,
    pub lighting_enabled: bool,
    pub label: Option<String>,
    pub visible: Option<bool>,
}

impl SurfaceStyle {
    fn from_defaults(defaults: SurfaceStyleDefaults) -> Self {
        Self {
            colormap: defaults.colormap,
            shading: defaults.shading,
            wireframe: defaults.wireframe,
            alpha: defaults.alpha,
            flatten_z: defaults.flatten_z,
            lighting_enabled: defaults.lighting_enabled,
            label: None,
            visible: Some(defaults.visible),
        }
    }

    pub fn apply_to_plot(&self, plot: &mut SurfacePlot) {
        plot.colormap = self.colormap;
        plot.shading_mode = self.shading;
        plot.wireframe = self.wireframe;
        plot.alpha = self.alpha;
        plot.flatten_z = self.flatten_z;
        plot.lighting_enabled = self.lighting_enabled;
        if let Some(label) = &self.label {
            plot.label = Some(label.clone());
        }
        if let Some(visible) = self.visible {
            plot.visible = visible;
        }
    }
}

#[derive(Default, Clone)]
struct LineStyleOptions {
    line_style: Option<LineStyle>,
    color: Option<Vec4>,
    line_width: Option<f32>,
    requires_cpu_fallback: bool,
    marker_kind: Option<MarkerKind>,
    marker_disabled: bool,
    marker_size: Option<f32>,
    marker_edge_color: Option<MarkerColor>,
    marker_face_color: Option<MarkerColor>,
    line_style_order: Option<Vec<LineStyle>>,
    label: Option<String>,
}

impl LineStyleOptions {
    fn merge(&mut self, other: LineStyleOptions) {
        if other.line_style.is_some() {
            self.line_style = other.line_style;
        }
        if other.color.is_some() {
            self.color = other.color;
        }
        if other.line_width.is_some() {
            self.line_width = other.line_width;
        }
        self.requires_cpu_fallback |= other.requires_cpu_fallback;
        if other.marker_disabled {
            self.marker_disabled = true;
            self.marker_kind = None;
        } else if other.marker_kind.is_some() {
            self.marker_kind = other.marker_kind;
            self.marker_disabled = false;
        }
        if other.marker_size.is_some() {
            self.marker_size = other.marker_size;
        }
        if other.marker_edge_color.is_some() {
            self.marker_edge_color = other.marker_edge_color;
        }
        if other.marker_face_color.is_some() {
            self.marker_face_color = other.marker_face_color;
        }
        if other.line_style_order.is_some() {
            self.line_style_order = other.line_style_order;
        }
        if other.label.is_some() {
            self.label = other.label;
        }
    }

    fn resolve(&self) -> LineAppearance {
        let mut appearance = LineAppearance::default();
        if let Some(color) = self.color {
            appearance.color = color;
        }
        if let Some(width) = self.line_width {
            appearance.line_width = width;
        }
        if let Some(style) = self.line_style {
            appearance.line_style = style;
        }
        if !self.marker_disabled {
            if let Some(kind) = self.marker_kind {
                appearance.marker = Some(MarkerAppearance {
                    kind,
                    size: self.marker_size,
                    edge_color: self.marker_edge_color.clone().unwrap_or(MarkerColor::Auto),
                    face_color: self.marker_face_color.clone().unwrap_or(MarkerColor::Auto),
                });
            }
        }
        appearance
    }
}

#[derive(Clone, Debug)]
pub struct ParsedLineStyle {
    pub appearance: LineAppearance,
    pub requires_cpu_fallback: bool,
    pub line_style_explicit: bool,
    pub line_style_order: Option<Vec<LineStyle>>,
    pub label: Option<String>,
}

pub fn parse_line_style_args(
    rest: &[Value],
    opts: &LineStyleParseOptions,
) -> BuiltinResult<ParsedLineStyle> {
    if rest.is_empty() {
        return Ok(ParsedLineStyle {
            appearance: LineAppearance::default(),
            requires_cpu_fallback: false,
            line_style_explicit: false,
            line_style_order: None,
            label: None,
        });
    }

    if opts.forbid_leading_numeric && begins_with_numeric(rest) {
        return Err(ctx_err(
            opts,
            "additional data series must precede style arguments",
        ));
    }

    let mut options = LineStyleOptions::default();
    let mut idx = 0usize;
    if let Some(token) = rest.first().and_then(value_as_string) {
        if is_style_token(&token) {
            options.merge(parse_style_string(opts, &token)?);
            idx = 1;
        }
    }

    let remaining = &rest[idx..];
    if !remaining.is_empty() {
        if opts.forbid_interleaved_numeric && begins_with_numeric(remaining) {
            return Err(ctx_err(
                opts,
                "per-series styles interleaved with data are not supported yet",
            ));
        }
        if !remaining.len().is_multiple_of(2) {
            return Err(ctx_err(opts, "name-value arguments must come in pairs"));
        }
        options.merge(parse_name_value_pairs(opts, remaining)?);
    }

    let appearance = options.resolve();
    Ok(ParsedLineStyle {
        requires_cpu_fallback: options.requires_cpu_fallback,
        appearance,
        line_style_explicit: options.line_style.is_some(),
        line_style_order: options.line_style_order.clone(),
        label: options.label.clone(),
    })
}

pub fn parse_surface_style_args(
    builtin: &'static str,
    rest: &[Value],
    defaults: SurfaceStyleDefaults,
) -> BuiltinResult<SurfaceStyle> {
    let opts = LineStyleParseOptions::generic(builtin);
    let mut style = SurfaceStyle::from_defaults(defaults);
    if rest.is_empty() {
        return Ok(style);
    }
    if !rest.len().is_multiple_of(2) {
        return Err(ctx_err(
            &opts,
            "name-value arguments must come in pairs for surface plots",
        ));
    }
    for pair in rest.chunks_exact(2) {
        let key = value_as_string(&pair[0])
            .ok_or_else(|| ctx_err(&opts, "option names must be char arrays or strings"))?;
        let lower = key.trim().to_ascii_lowercase();
        match lower.as_str() {
            "colormap" => {
                style.colormap = parse_colormap_option(&opts, &pair[1])?;
            }
            "shading" => {
                style.shading = parse_shading_option(&opts, &pair[1])?;
            }
            "facecolor" => {
                apply_face_color_option(&opts, &pair[1], &mut style)?;
            }
            "facealpha" | "alpha" => {
                style.alpha = parse_alpha_value(&opts, &pair[1])?;
            }
            "edgecolor" => {
                apply_edge_color_option(&opts, &pair[1], &mut style)?;
            }
            "flattenz" => {
                style.flatten_z = parse_surface_bool(&opts, "FlattenZ", &pair[1])?;
            }
            "displayname" | "label" => {
                let Some(name) = value_as_string(&pair[1]) else {
                    return Err(ctx_err(&opts, "DisplayName must be a char array or string"));
                };
                style.label = Some(name);
            }
            "lighting" => {
                style.lighting_enabled = parse_lighting_option(&opts, &pair[1])?;
            }
            "visible" => {
                let visible = parse_surface_bool(&opts, "Visible", &pair[1])?;
                style.visible = Some(visible);
            }
            other => {
                return Err(ctx_err(
                    &opts,
                    format!("unsupported surface option `{other}`"),
                ));
            }
        }
    }
    Ok(style)
}

fn begins_with_numeric(rest: &[Value]) -> bool {
    matches!(
        rest.first(),
        Some(
            Value::Tensor(_) | Value::GpuTensor(_) | Value::Num(_) | Value::Int(_) | Value::Bool(_)
        )
    )
}

fn parse_style_string(
    opts: &LineStyleParseOptions,
    token: &str,
) -> BuiltinResult<LineStyleOptions> {
    let mut options = LineStyleOptions::default();
    let mut chars = token.chars().peekable();
    while let Some(ch) = chars.next() {
        match ch {
            '-' => {
                if let Some(next) = chars.peek() {
                    match next {
                        '-' => {
                            chars.next();
                            options.line_style = Some(LineStyle::Dashed);
                        }
                        '.' => {
                            chars.next();
                            options.line_style = Some(LineStyle::DashDot);
                        }
                        _ => options.line_style = Some(LineStyle::Solid),
                    }
                } else {
                    options.line_style = Some(LineStyle::Solid);
                }
            }
            ':' => {
                options.line_style = Some(LineStyle::Dotted);
            }
            'o' | '+' | '*' | '.' | 'x' | '^' | 'v' | '<' | '>' | 's' | 'd' | 'p' | 'h' => {
                if let Some(kind) = marker_kind_from_token(ch) {
                    options.marker_kind = Some(kind);
                }
            }
            c if color_from_token(c).is_some() => {
                options.color = color_from_token(c);
            }
            _ => {
                return Err(ctx_err(opts, format!("unrecognised style token `{ch}`")));
            }
        }
    }
    Ok(options)
}

fn parse_name_value_pairs(
    opts: &LineStyleParseOptions,
    args: &[Value],
) -> BuiltinResult<LineStyleOptions> {
    let mut options = LineStyleOptions::default();
    let mut iter = args.chunks_exact(2);
    for pair in iter.by_ref() {
        let key = value_as_string(&pair[0])
            .ok_or_else(|| ctx_err(opts, "option names must be char arrays or strings"))?;
        let lower = key.trim().to_ascii_lowercase();
        match lower.as_str() {
            "linewidth" => {
                let width = value_as_f64(&pair[1])
                    .ok_or_else(|| ctx_err(opts, "LineWidth must be numeric"))?;
                if width <= 0.0 {
                    return Err(ctx_err(opts, "LineWidth must be positive"));
                }
                options.line_width = Some(width as f32);
            }
            "color" => {
                options.color = Some(parse_color_value(opts, &pair[1])?);
            }
            "linestyle" => {
                let style_text = value_as_string(&pair[1])
                    .ok_or_else(|| ctx_err(opts, "LineStyle must be a string"))?;
                options.line_style = Some(parse_line_style_name(opts, &style_text)?);
            }
            "marker" => {
                let marker_text = value_as_string(&pair[1])
                    .ok_or_else(|| ctx_err(opts, "Marker must be a string"))?;
                let parsed = parse_marker_string(opts, &marker_text)?;
                match parsed {
                    Some(kind) => {
                        options.marker_kind = Some(kind);
                        options.marker_disabled = false;
                    }
                    None => {
                        options.marker_kind = None;
                        options.marker_disabled = true;
                    }
                }
            }
            "markersize" => {
                let size = value_as_f64(&pair[1])
                    .ok_or_else(|| ctx_err(opts, "MarkerSize must be numeric"))?;
                if size <= 0.0 {
                    return Err(ctx_err(opts, "MarkerSize must be positive"));
                }
                options.marker_size = Some(size as f32);
            }
            "markeredgecolor" => {
                options.marker_edge_color = Some(parse_marker_color_value(opts, &pair[1])?);
            }
            "markerfacecolor" => {
                options.marker_face_color = Some(parse_marker_color_value(opts, &pair[1])?);
            }
            "linestyleorder" => {
                let order = parse_line_style_order_value(opts, &pair[1])?;
                options.line_style_order = Some(order);
            }
            "displayname" | "label" => {
                let Some(name) = value_as_string(&pair[1]) else {
                    return Err(ctx_err(opts, "DisplayName must be a char array or string"));
                };
                options.label = Some(name);
            }
            other => {
                return Err(ctx_err(opts, format!("unsupported option `{other}`")));
            }
        }
    }
    Ok(options)
}

fn parse_line_style_name(opts: &LineStyleParseOptions, value: &str) -> BuiltinResult<LineStyle> {
    match value.trim() {
        "-" => Ok(LineStyle::Solid),
        "--" => Ok(LineStyle::Dashed),
        ":" => Ok(LineStyle::Dotted),
        "-." => Ok(LineStyle::DashDot),
        other => Err(ctx_err(opts, format!("unsupported LineStyle `{other}`"))),
    }
}

fn parse_marker_string(
    opts: &LineStyleParseOptions,
    value: &str,
) -> BuiltinResult<Option<MarkerKind>> {
    let trimmed = value.trim();
    if trimmed.eq_ignore_ascii_case("none") {
        return Ok(None);
    }
    if trimmed.is_empty() {
        return Err(ctx_err(opts, "Marker cannot be empty"));
    }
    if let Some(kind) = trimmed.chars().next().and_then(marker_kind_from_token) {
        return Ok(Some(kind));
    }
    Err(ctx_err(opts, format!("unsupported Marker `{value}`")))
}

fn marker_kind_from_token(token: char) -> Option<MarkerKind> {
    match token {
        'o' | 'O' => Some(MarkerKind::Circle),
        '+' => Some(MarkerKind::Plus),
        '*' => Some(MarkerKind::Star),
        '.' => Some(MarkerKind::Point),
        'x' | 'X' => Some(MarkerKind::Cross),
        '^' => Some(MarkerKind::TriangleUp),
        'v' => Some(MarkerKind::TriangleDown),
        '<' => Some(MarkerKind::TriangleLeft),
        '>' => Some(MarkerKind::TriangleRight),
        's' | 'S' => Some(MarkerKind::Square),
        'd' | 'D' => Some(MarkerKind::Diamond),
        'p' | 'P' => Some(MarkerKind::Pentagram),
        'h' | 'H' => Some(MarkerKind::Hexagram),
        _ => None,
    }
}

fn parse_marker_color_value(
    opts: &LineStyleParseOptions,
    value: &Value,
) -> BuiltinResult<MarkerColor> {
    if let Some(name) = value_as_string(value) {
        let lower = name.trim().to_ascii_lowercase();
        return match lower.as_str() {
            "auto" => Ok(MarkerColor::Auto),
            "flat" => Ok(MarkerColor::Flat),
            "none" => Ok(MarkerColor::None),
            _ => Ok(MarkerColor::Color(parse_color_value(opts, value)?)),
        };
    }
    Ok(MarkerColor::Color(parse_color_value(opts, value)?))
}

fn parse_line_style_order_value(
    opts: &LineStyleParseOptions,
    value: &Value,
) -> BuiltinResult<Vec<LineStyle>> {
    let strings = match value {
        Value::StringArray(arr) => arr.data.clone(),
        Value::Cell(cell) => collect_cell_strings(cell, opts)?,
        _ => {
            if let Some(text) = value_as_string(value) {
                vec![text]
            } else {
                return Err(ctx_err(
                    opts,
                    "LineStyleOrder must be a string, string array, or cell array of strings",
                ));
            }
        }
    };

    if strings.is_empty() {
        return Err(ctx_err(opts, "LineStyleOrder cannot be empty"));
    }

    let mut styles = Vec::with_capacity(strings.len());
    for entry in strings {
        styles.push(parse_line_style_name(opts, &entry)?);
    }
    Ok(styles)
}

fn collect_cell_strings(
    cell: &CellArray,
    opts: &LineStyleParseOptions,
) -> BuiltinResult<Vec<String>> {
    let mut strings = Vec::with_capacity(cell.rows * cell.cols);
    for row in 0..cell.rows {
        for col in 0..cell.cols {
            let value = cell
                .get(row, col)
                .map_err(|e| ctx_err(opts, format!("LineStyleOrder: {e}")))?;
            if let Some(text) = value_as_string(&value) {
                strings.push(text);
            } else {
                return Err(ctx_err(
                    opts,
                    format!(
                        "LineStyleOrder: cell entry at ({row}, {col}) is not a char array or string"
                    ),
                ));
            }
        }
    }
    Ok(strings)
}

pub(crate) fn parse_color_value(
    opts: &LineStyleParseOptions,
    value: &Value,
) -> BuiltinResult<Vec4> {
    if let Some(name) = value_as_string(value) {
        if let Some(color) = name.trim().chars().next().and_then(color_from_token) {
            return Ok(color);
        }
        return Err(ctx_err(
            opts,
            format!("unsupported color specification `{name}`"),
        ));
    }
    let tensor = Tensor::try_from(value).map_err(|e| ctx_err(opts, e))?;
    if tensor.data.len() != 3 {
        return Err(ctx_err(opts, "color vectors must contain three elements"));
    }
    Ok(Vec4::new(
        tensor.data[0] as f32,
        tensor.data[1] as f32,
        tensor.data[2] as f32,
        1.0,
    ))
}

pub(crate) fn color_from_token(token: char) -> Option<Vec4> {
    match token {
        'r' | 'R' => Some(Vec4::new(1.0, 0.0, 0.0, 1.0)),
        'g' | 'G' => Some(Vec4::new(0.0, 1.0, 0.0, 1.0)),
        'b' | 'B' => Some(Vec4::new(0.0, 0.0, 1.0, 1.0)),
        'c' | 'C' => Some(Vec4::new(0.0, 1.0, 1.0, 1.0)),
        'm' | 'M' => Some(Vec4::new(1.0, 0.0, 1.0, 1.0)),
        'y' | 'Y' => Some(Vec4::new(1.0, 1.0, 0.0, 1.0)),
        'k' | 'K' => Some(Vec4::new(0.0, 0.0, 0.0, 1.0)),
        'w' | 'W' => Some(Vec4::new(1.0, 1.0, 1.0, 1.0)),
        _ => None,
    }
}

fn is_style_token(token: &str) -> bool {
    !token.trim().is_empty() && !looks_like_option_name(token)
}

pub fn looks_like_option_name(token: &str) -> bool {
    matches!(
        token.trim().to_ascii_lowercase().as_str(),
        "linewidth"
            | "color"
            | "linestyle"
            | "marker"
            | "markersize"
            | "markeredgecolor"
            | "markerfacecolor"
            | "linestyleorder"
            | "displayname"
            | "label"
    )
}

pub fn value_as_string(value: &Value) -> Option<String> {
    match value {
        Value::CharArray(chars) => Some(chars.data.iter().collect()),
        Value::String(s) => Some(s.clone()),
        _ => None,
    }
}

pub(crate) fn value_as_f64(value: &Value) -> Option<f64> {
    match value {
        Value::Num(v) => Some(*v),
        Value::Int(i) => Some(i.to_f64()),
        Value::Tensor(tensor) => tensor.data.first().copied(),
        _ => None,
    }
}

pub(crate) fn value_as_bool(value: &Value) -> Option<bool> {
    match value {
        Value::Bool(b) => Some(*b),
        Value::Num(v) => Some(*v != 0.0),
        Value::Int(i) => Some(!i.is_zero()),
        Value::CharArray(chars) => {
            let text: String = chars.data.iter().collect();
            bool_from_text(&text)
        }
        Value::String(text) => bool_from_text(text),
        _ => None,
    }
}

fn bool_from_text(text: &str) -> Option<bool> {
    match text.trim().to_ascii_lowercase().as_str() {
        "on" | "true" | "yes" | "y" => Some(true),
        "off" | "false" | "no" | "n" => Some(false),
        _ => None,
    }
}

fn parse_colormap_option(opts: &LineStyleParseOptions, value: &Value) -> BuiltinResult<ColorMap> {
    if let Some(name) = value_as_string(value) {
        if let Some(map) = parse_colormap_name(&name) {
            return Ok(map);
        }
        return Err(ctx_err(opts, format!("unsupported colormap `{name}`")));
    }
    let color = parse_color_value(opts, value)?;
    Ok(ColorMap::Custom(color, color))
}

fn parse_colormap_name(name: &str) -> Option<ColorMap> {
    match name.trim().to_ascii_lowercase().as_str() {
        "parula" => Some(ColorMap::Parula),
        "jet" => Some(ColorMap::Jet),
        "turbo" => Some(ColorMap::Turbo),
        "viridis" => Some(ColorMap::Viridis),
        "plasma" => Some(ColorMap::Plasma),
        "inferno" => Some(ColorMap::Inferno),
        "magma" => Some(ColorMap::Magma),
        "hot" => Some(ColorMap::Hot),
        "cool" => Some(ColorMap::Cool),
        "spring" => Some(ColorMap::Spring),
        "summer" => Some(ColorMap::Summer),
        "autumn" => Some(ColorMap::Autumn),
        "winter" => Some(ColorMap::Winter),
        "gray" | "grey" => Some(ColorMap::Gray),
        "bone" => Some(ColorMap::Bone),
        "copper" => Some(ColorMap::Copper),
        "pink" => Some(ColorMap::Pink),
        "lines" => Some(ColorMap::Lines),
        _ => None,
    }
}

fn parse_shading_option(opts: &LineStyleParseOptions, value: &Value) -> BuiltinResult<ShadingMode> {
    let Some(text) = value_as_string(value) else {
        return Err(ctx_err(opts, "Shading must be a string"));
    };
    match text.trim().to_ascii_lowercase().as_str() {
        "flat" => Ok(ShadingMode::Flat),
        "interp" | "gouraud" => Ok(ShadingMode::Smooth),
        "faceted" => Ok(ShadingMode::Faceted),
        "none" => Ok(ShadingMode::None),
        other => Err(ctx_err(opts, format!("unsupported Shading `{other}`"))),
    }
}

fn parse_alpha_value(opts: &LineStyleParseOptions, value: &Value) -> BuiltinResult<f32> {
    let alpha =
        value_as_f64(value).ok_or_else(|| ctx_err(opts, "Alpha/FacesAlpha must be numeric"))?;
    Ok(alpha.clamp(0.0, 1.0) as f32)
}

fn apply_face_color_option(
    opts: &LineStyleParseOptions,
    value: &Value,
    style: &mut SurfaceStyle,
) -> BuiltinResult<()> {
    if let Some(text) = value_as_string(value) {
        let lower = text.trim().to_ascii_lowercase();
        return match lower.as_str() {
            "none" => {
                style.alpha = 0.0;
                Ok(())
            }
            "flat" => {
                style.shading = ShadingMode::Flat;
                Ok(())
            }
            "interp" => {
                style.shading = ShadingMode::Smooth;
                Ok(())
            }
            "texturemap" => {
                style.flatten_z = true;
                Ok(())
            }
            _ => {
                let color = parse_color_value(opts, value)?;
                style.colormap = ColorMap::Custom(color, color);
                style.shading = ShadingMode::None;
                style.lighting_enabled = false;
                Ok(())
            }
        };
    }
    let color = parse_color_value(opts, value)?;
    style.colormap = ColorMap::Custom(color, color);
    style.shading = ShadingMode::None;
    style.lighting_enabled = false;
    Ok(())
}

fn apply_edge_color_option(
    opts: &LineStyleParseOptions,
    value: &Value,
    style: &mut SurfaceStyle,
) -> BuiltinResult<()> {
    if let Some(text) = value_as_string(value) {
        let lower = text.trim().to_ascii_lowercase();
        return match lower.as_str() {
            "none" => {
                style.wireframe = false;
                Ok(())
            }
            "auto" | "flat" | "interp" => {
                style.wireframe = true;
                Ok(())
            }
            _ => Err(ctx_err(
                opts,
                "EdgeColor only supports 'auto', 'flat', or 'none' in this build",
            )),
        };
    }
    Err(ctx_err(
        opts,
        "EdgeColor does not support custom RGB values yet",
    ))
}

fn parse_surface_bool(
    opts: &LineStyleParseOptions,
    field: &str,
    value: &Value,
) -> BuiltinResult<bool> {
    value_as_bool(value).ok_or_else(|| ctx_err(opts, format!("{field} must be logical (on/off)")))
}

fn parse_lighting_option(opts: &LineStyleParseOptions, value: &Value) -> BuiltinResult<bool> {
    if let Some(text) = value_as_string(value) {
        let lower = text.trim().to_ascii_lowercase();
        return match lower.as_str() {
            "none" => Ok(false),
            "flat" | "gouraud" | "phong" | "auto" => Ok(true),
            _ => Err(ctx_err(
                opts,
                format!("unsupported Lighting value `{lower}`"),
            )),
        };
    }
    parse_surface_bool(opts, "Lighting", value)
}

pub fn parse_bar_style_args(
    builtin: &'static str,
    rest: &[Value],
    defaults: BarStyleDefaults,
) -> BuiltinResult<BarStyle> {
    let opts = LineStyleParseOptions::generic(builtin);
    let mut style = BarStyle {
        face_color: defaults.face_color,
        face_alpha: 1.0,
        edge_color: None,
        edge_alpha: 1.0,
        line_width: DEFAULT_LINE_WIDTH,
        bar_width: defaults.bar_width.clamp(0.1, 1.0),
        label: None,
        face_color_flat: false,
        layout: BarLayout::Grouped,
    };

    if rest.is_empty() {
        return Ok(style);
    }

    let filtered = strip_bar_layout_tokens(rest, &mut style);
    let rest = filtered.as_slice();

    let mut idx = 0usize;
    if let Some(token) = rest.first().and_then(value_as_string) {
        let trimmed = token.trim();
        if !trimmed.is_empty() && !is_bar_option_name(trimmed) {
            style.face_color = parse_bar_color_literal(&opts, trimmed)?;
            idx = 1;
        }
    }

    let remaining = &rest[idx..];
    if remaining.is_empty() {
        return Ok(style);
    }
    if !remaining.len().is_multiple_of(2) {
        return Err(bar_ctx_err(
            builtin,
            "name-value arguments must come in pairs",
        ));
    }

    for pair in remaining.chunks_exact(2) {
        let key_value = pair[0].clone();
        let Some(key) = value_as_string(&pair[0]) else {
            return Err(bar_ctx_err(
                builtin,
                "option names must be char arrays or strings",
            ));
        };
        let lower = key.trim().to_ascii_lowercase();
        match lower.as_str() {
            "facecolor" | "color" => match parse_bar_face_color(&opts, &pair[1])? {
                FaceColorSpec::Auto => {
                    style.face_color_flat = false;
                }
                FaceColorSpec::Flat => {
                    style.face_color_flat = true;
                    style.face_alpha = 1.0;
                }
                FaceColorSpec::None => {
                    style.face_alpha = 0.0;
                    style.face_color_flat = false;
                }
                FaceColorSpec::Color(color) => {
                    style.face_color = color;
                    style.face_alpha = 1.0;
                    style.face_color_flat = false;
                }
            },
            "edgecolor" => {
                style.edge_color = match parse_bar_edge_color(&opts, &pair[1])? {
                    EdgeColorSpec::Auto | EdgeColorSpec::Flat => None,
                    EdgeColorSpec::None => None,
                    EdgeColorSpec::Color(color) => Some(color),
                };
            }
            "linewidth" => {
                let width = value_as_f64(&pair[1])
                    .ok_or_else(|| bar_ctx_err(builtin, "LineWidth must be numeric"))?;
                if width <= 0.0 {
                    return Err(bar_ctx_err(builtin, "LineWidth must be positive"));
                }
                style.line_width = width as f32;
            }
            "barwidth" => {
                let width = value_as_f64(&pair[1])
                    .ok_or_else(|| bar_ctx_err(builtin, "BarWidth must be numeric"))?;
                if width <= 0.0 {
                    return Err(bar_ctx_err(builtin, "BarWidth must be positive"));
                }
                style.bar_width = width as f32;
            }
            "facealpha" => {
                let alpha = value_as_f64(&pair[1])
                    .ok_or_else(|| bar_ctx_err(builtin, "FaceAlpha must be numeric"))?;
                style.face_alpha = alpha.clamp(0.0, 1.0) as f32;
            }
            "edgealpha" => {
                let alpha = value_as_f64(&pair[1])
                    .ok_or_else(|| bar_ctx_err(builtin, "EdgeAlpha must be numeric"))?;
                style.edge_alpha = alpha.clamp(0.0, 1.0) as f32;
            }
            "displayname" | "label" => {
                let Some(name) = value_as_string(&pair[1]) else {
                    return Err(bar_ctx_err(
                        builtin,
                        "DisplayName must be a char array or string",
                    ));
                };
                style.label = Some(name);
            }
            other => {
                return Err(bar_ctx_err(
                    builtin,
                    format!("unsupported option `{other}`"),
                ));
            }
        }
        drop(key_value);
    }

    Ok(style)
}

fn strip_bar_layout_tokens(rest: &[Value], style: &mut BarStyle) -> Vec<Value> {
    let mut filtered = Vec::with_capacity(rest.len());
    let mut expecting_value = false;
    for value in rest {
        if expecting_value {
            filtered.push(value.clone());
            expecting_value = false;
            continue;
        }
        if let Some(text) = value_as_string(value) {
            let lower = text.trim().to_ascii_lowercase();
            match lower.as_str() {
                "stacked" => {
                    style.layout = BarLayout::Stacked;
                    continue;
                }
                "grouped" => {
                    style.layout = BarLayout::Grouped;
                    continue;
                }
                _ => {
                    if is_bar_option_name(&lower) {
                        filtered.push(value.clone());
                        expecting_value = true;
                        continue;
                    }
                }
            }
        }
        filtered.push(value.clone());
    }
    filtered
}

fn is_bar_option_name(token: &str) -> bool {
    matches!(
        token.trim().to_ascii_lowercase().as_str(),
        "facecolor"
            | "color"
            | "edgecolor"
            | "linewidth"
            | "barwidth"
            | "facealpha"
            | "edgealpha"
            | "displayname"
            | "label"
    )
}

impl BarStyle {
    pub fn requires_cpu_path(&self) -> bool {
        self.face_color_flat
    }
}

enum FaceColorSpec {
    Auto,
    Flat,
    None,
    Color(Vec4),
}

enum EdgeColorSpec {
    Auto,
    Flat,
    None,
    Color(Vec4),
}

fn parse_bar_face_color(
    opts: &LineStyleParseOptions,
    value: &Value,
) -> BuiltinResult<FaceColorSpec> {
    if let Some(text) = value_as_string(value) {
        let lower = text.trim().to_ascii_lowercase();
        return match lower.as_str() {
            "auto" => Ok(FaceColorSpec::Auto),
            "none" => Ok(FaceColorSpec::None),
            "flat" => Ok(FaceColorSpec::Flat),
            _ => {
                let color = parse_bar_color_literal(opts, &text)?;
                Ok(FaceColorSpec::Color(color))
            }
        };
    }
    let color = parse_color_value(opts, value)?;
    Ok(FaceColorSpec::Color(color))
}

fn parse_bar_edge_color(
    opts: &LineStyleParseOptions,
    value: &Value,
) -> BuiltinResult<EdgeColorSpec> {
    if let Some(text) = value_as_string(value) {
        let lower = text.trim().to_ascii_lowercase();
        return match lower.as_str() {
            "auto" => Ok(EdgeColorSpec::Auto),
            "none" => Ok(EdgeColorSpec::None),
            "flat" => Ok(EdgeColorSpec::Flat),
            _ => {
                let color = parse_bar_color_literal(opts, &text)?;
                Ok(EdgeColorSpec::Color(color))
            }
        };
    }
    let color = parse_color_value(opts, value)?;
    Ok(EdgeColorSpec::Color(color))
}

fn parse_bar_color_literal(opts: &LineStyleParseOptions, token: &str) -> BuiltinResult<Vec4> {
    if let Some(ch) = token.trim().chars().next() {
        if let Some(color) = color_from_token(ch) {
            return Ok(color);
        }
    }
    Err(ctx_err(
        opts,
        format!("unsupported color specification `{token}`"),
    ))
}

fn bar_ctx_err(builtin: &str, msg: impl Into<String>) -> crate::RuntimeError {
    let message = format!("{builtin}: {}", msg.into());
    plotting_error(builtin, message)
}

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

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn style_string_with_marker_no_longer_forces_cpu_fallback() {
        let rest = vec![Value::String("o--".into())];
        let opts = LineStyleParseOptions::plot();
        let parsed = parse_line_style_args(&rest, &opts).expect("parsed");
        assert!(parsed.appearance.marker.is_some());
        assert!(!parsed.requires_cpu_fallback);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn marker_none_disables_marker_without_fallback() {
        let rest = vec![Value::String("Marker".into()), Value::String("none".into())];
        let opts = LineStyleParseOptions::plot();
        let parsed = parse_line_style_args(&rest, &opts).expect("parsed");
        assert!(parsed.appearance.marker.is_none());
        assert!(!parsed.requires_cpu_fallback);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn marker_color_flat_literal_is_supported() {
        let opts = LineStyleParseOptions::plot();
        let color = parse_marker_color_value(&opts, &Value::String("flat".into())).unwrap();
        assert_eq!(color, MarkerColor::Flat);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn bar_style_parses_face_and_edge_colors() {
        let defaults = BarStyleDefaults::new(Vec4::new(0.2, 0.6, 0.9, 1.0), 0.8);
        let rest = vec![
            Value::String("FaceColor".into()),
            Value::String("r".into()),
            Value::String("EdgeColor".into()),
            Value::String("k".into()),
            Value::String("BarWidth".into()),
            Value::from(0.5),
        ];
        let style = parse_bar_style_args("bar", &rest, defaults).expect("parsed");
        assert!((style.bar_width - 0.5).abs() < f32::EPSILON);
        assert_eq!(style.face_color, Vec4::new(1.0, 0.0, 0.0, 1.0));
        assert_eq!(style.edge_color, Some(Vec4::new(0.0, 0.0, 0.0, 1.0)));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn bar_style_accepts_label() {
        let defaults = BarStyleDefaults::new(Vec4::new(0.2, 0.6, 0.9, 1.0), 0.8);
        let rest = vec![
            Value::String("DisplayName".into()),
            Value::String("My Bars".into()),
        ];
        let style = parse_bar_style_args("bar", &rest, defaults).expect("parsed");
        assert_eq!(style.label.as_deref(), Some("My Bars"));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn bar_style_accepts_flat_facecolor() {
        let defaults = BarStyleDefaults::new(Vec4::new(0.2, 0.6, 0.9, 1.0), 0.8);
        let rest = vec![
            Value::String("FaceColor".into()),
            Value::String("flat".into()),
        ];
        let style = parse_bar_style_args("bar", &rest, defaults).expect("parsed");
        assert!(style.face_color_flat);
        assert!(style.requires_cpu_path());
    }
}
