use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
};
use runmat_builtins::{Tensor, Value};
use runmat_macros::runtime_builtin;

use super::op_common::current_axes_target;
use super::state::{set_view_for_axes, FigureError};
use crate::builtins::plotting::type_resolvers::get_type;
use crate::{build_runtime_error, RuntimeError};

const BUILTIN_NAME: &str = "view";

const VIEW_OUTPUT_ANGLES: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "angles",
    ty: BuiltinParamType::NumericArray,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Current or assigned view angles as [az el].",
}];

const VIEW_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];

const VIEW_INPUTS_AX: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "ax",
    ty: BuiltinParamType::AxesHandle,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Axes handle to query.",
}];

const VIEW_INPUTS_VECTOR: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "angles",
    ty: BuiltinParamType::NumericArray,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Two-element [az el] vector or preset value 2/3.",
}];

const VIEW_INPUTS_AZ_EL: [BuiltinParamDescriptor; 2] = [
    BuiltinParamDescriptor {
        name: "az",
        ty: BuiltinParamType::NumericScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Azimuth in degrees.",
    },
    BuiltinParamDescriptor {
        name: "el",
        ty: BuiltinParamType::NumericScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Elevation in degrees.",
    },
];

const VIEW_INPUTS_AX_VECTOR: [BuiltinParamDescriptor; 2] = [
    BuiltinParamDescriptor {
        name: "ax",
        ty: BuiltinParamType::AxesHandle,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Axes handle to set.",
    },
    BuiltinParamDescriptor {
        name: "angles",
        ty: BuiltinParamType::NumericArray,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Two-element [az el] vector or preset value 2/3.",
    },
];

const VIEW_INPUTS_AX_AZ_EL: [BuiltinParamDescriptor; 3] = [
    BuiltinParamDescriptor {
        name: "ax",
        ty: BuiltinParamType::AxesHandle,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Axes handle to set.",
    },
    BuiltinParamDescriptor {
        name: "az",
        ty: BuiltinParamType::NumericScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Azimuth in degrees.",
    },
    BuiltinParamDescriptor {
        name: "el",
        ty: BuiltinParamType::NumericScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Elevation in degrees.",
    },
];

const VIEW_SIGNATURES: [BuiltinSignatureDescriptor; 6] = [
    BuiltinSignatureDescriptor {
        label: "angles = view()",
        inputs: &VIEW_INPUTS_NONE,
        outputs: &VIEW_OUTPUT_ANGLES,
    },
    BuiltinSignatureDescriptor {
        label: "angles = view(ax)",
        inputs: &VIEW_INPUTS_AX,
        outputs: &VIEW_OUTPUT_ANGLES,
    },
    BuiltinSignatureDescriptor {
        label: "angles = view([az el] | preset)",
        inputs: &VIEW_INPUTS_VECTOR,
        outputs: &VIEW_OUTPUT_ANGLES,
    },
    BuiltinSignatureDescriptor {
        label: "angles = view(az, el)",
        inputs: &VIEW_INPUTS_AZ_EL,
        outputs: &VIEW_OUTPUT_ANGLES,
    },
    BuiltinSignatureDescriptor {
        label: "angles = view(ax, [az el] | preset)",
        inputs: &VIEW_INPUTS_AX_VECTOR,
        outputs: &VIEW_OUTPUT_ANGLES,
    },
    BuiltinSignatureDescriptor {
        label: "angles = view(ax, az, el)",
        inputs: &VIEW_INPUTS_AX_AZ_EL,
        outputs: &VIEW_OUTPUT_ANGLES,
    },
];

const VIEW_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.VIEW.INVALID_ARGUMENT",
    identifier: Some("RunMat:view:InvalidArgument"),
    when: "Arguments are malformed, wrong arity, wrong shape, or unsupported preset.",
    message: "view: invalid argument",
};

const VIEW_ERROR_INVALID_AXES: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.VIEW.INVALID_AXES",
    identifier: Some("RunMat:view:InvalidAxes"),
    when: "Axes target is invalid or no longer exists.",
    message: "view: invalid axes target",
};

const VIEW_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.VIEW.INTERNAL",
    identifier: Some("RunMat:view:Internal"),
    when: "Internal plotting state operation fails.",
    message: "view: internal operation failed",
};

const VIEW_ERRORS: [BuiltinErrorDescriptor; 3] = [
    VIEW_ERROR_INVALID_ARGUMENT,
    VIEW_ERROR_INVALID_AXES,
    VIEW_ERROR_INTERNAL,
];

pub const VIEW_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &VIEW_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &VIEW_ERRORS,
};

fn view_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
    view_error_with_message(error.message, error)
}

fn view_error_with_message(
    message: impl Into<String>,
    error: &'static BuiltinErrorDescriptor,
) -> RuntimeError {
    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
    if let Some(identifier) = error.identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

fn map_view_figure_error(err: FigureError) -> RuntimeError {
    match err {
        FigureError::InvalidSubplotIndex { .. }
        | FigureError::InvalidAxesHandle
        | FigureError::InvalidHandle(_) => view_error(&VIEW_ERROR_INVALID_AXES),
        other => view_error_with_message(
            format!("{}: {}", VIEW_ERROR_INTERNAL.message, other),
            &VIEW_ERROR_INTERNAL,
        ),
    }
}

fn parse_view_target(
    args: &[Value],
) -> crate::BuiltinResult<(
    (crate::builtins::plotting::state::FigureHandle, usize),
    &[Value],
)> {
    if let Some(first) = args.first() {
        if let Ok(crate::builtins::plotting::properties::PlotHandle::Axes(fig, axes)) =
            crate::builtins::plotting::properties::resolve_plot_handle(first, "view")
        {
            return Ok(((fig, axes), &args[1..]));
        }
    }
    Ok((current_axes_target(), args))
}

fn parse_view_angles(args: &[Value]) -> crate::BuiltinResult<(f32, f32)> {
    match args.len() {
        1 => {
            let tensor = scalar_or_tensor(&args[0])?;
            if tensor.data.len() == 1 {
                match tensor.data[0] as i32 {
                    2 => Ok((0.0, 90.0)),
                    3 => Ok((-37.5, 30.0)),
                    _ => Err(view_error(&VIEW_ERROR_INVALID_ARGUMENT)),
                }
            } else if tensor.data.len() == 2 {
                Ok((tensor.data[0] as f32, tensor.data[1] as f32))
            } else {
                Err(view_error(&VIEW_ERROR_INVALID_ARGUMENT))
            }
        }
        2 => {
            let az = scalar_or_tensor(&args[0])?;
            let el = scalar_or_tensor(&args[1])?;
            if az.data.len() != 1 || el.data.len() != 1 {
                return Err(view_error(&VIEW_ERROR_INVALID_ARGUMENT));
            }
            Ok((az.data[0] as f32, el.data[0] as f32))
        }
        _ => Err(view_error(&VIEW_ERROR_INVALID_ARGUMENT)),
    }
}

fn scalar_or_tensor(value: &Value) -> crate::BuiltinResult<Tensor> {
    match value {
        Value::Num(v) => Ok(Tensor {
            rows: 1,
            cols: 1,
            shape: vec![1, 1],
            data: vec![*v],
            dtype: runmat_builtins::NumericDType::F64,
        }),
        Value::Int(i) => Ok(Tensor {
            rows: 1,
            cols: 1,
            shape: vec![1, 1],
            data: vec![i.to_f64()],
            dtype: runmat_builtins::NumericDType::F64,
        }),
        other => Tensor::try_from(other).map_err(|_| view_error(&VIEW_ERROR_INVALID_ARGUMENT)),
    }
}

#[runtime_builtin(
    name = "view",
    category = "plotting",
    summary = "Set or query azimuth/elevation camera view angles.",
    keywords = "view,plotting,3d,camera",
    suppress_auto_output = true,
    type_resolver(get_type),
    descriptor(crate::builtins::plotting::view::VIEW_DESCRIPTOR),
    builtin_path = "crate::builtins::plotting::view"
)]
pub fn view_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    let (target, rest) = parse_view_target(&args)?;
    if rest.is_empty() {
        let meta = crate::builtins::plotting::state::axes_metadata_snapshot(target.0, target.1)
            .map_err(map_view_figure_error)?;
        let az = meta.view_azimuth_deg.unwrap_or(-37.5) as f64;
        let el = meta.view_elevation_deg.unwrap_or(30.0) as f64;
        return Ok(Value::Tensor(Tensor {
            rows: 1,
            cols: 2,
            shape: vec![1, 2],
            data: vec![az, el],
            dtype: runmat_builtins::NumericDType::F64,
        }));
    }
    let (az, el) = parse_view_angles(rest)?;
    set_view_for_axes(target.0, target.1, az, el).map_err(map_view_figure_error)?;
    Ok(Value::Tensor(Tensor {
        rows: 1,
        cols: 2,
        shape: vec![1, 2],
        data: vec![az as f64, el as f64],
        dtype: runmat_builtins::NumericDType::F64,
    }))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::builtins::plotting::get::get_builtin;
    use crate::builtins::plotting::set::set_builtin;
    use crate::builtins::plotting::tests::{ensure_plot_test_env, lock_plot_registry};
    use crate::builtins::plotting::{
        clear_figure, clone_figure, configure_subplot, current_figure_handle,
        reset_hold_state_for_run,
    };

    #[test]
    fn view_sets_axes_local_angles() {
        let _guard = lock_plot_registry();
        ensure_plot_test_env();
        reset_hold_state_for_run();
        let _ = clear_figure(None);
        let value = view_builtin(vec![Value::Num(45.0), Value::Num(20.0)]).unwrap();
        let t = Tensor::try_from(&value).unwrap();
        assert_eq!(t.data, vec![45.0, 20.0]);
        let fig = clone_figure(current_figure_handle()).unwrap();
        let meta = fig.axes_metadata(0).unwrap();
        assert_eq!(meta.view_azimuth_deg, Some(45.0));
        assert_eq!(meta.view_elevation_deg, Some(20.0));
    }

    #[test]
    fn view_is_subplot_local() {
        let _guard = lock_plot_registry();
        ensure_plot_test_env();
        reset_hold_state_for_run();
        let _ = clear_figure(None);
        configure_subplot(1, 2, 1).unwrap();
        let _ = view_builtin(vec![Value::Num(10.0), Value::Num(15.0)]).unwrap();
        let fig = clone_figure(current_figure_handle()).unwrap();
        assert_eq!(fig.axes_metadata(0).unwrap().view_azimuth_deg, None);
        assert_eq!(fig.axes_metadata(1).unwrap().view_azimuth_deg, Some(10.0));
    }

    #[test]
    fn view_supports_query_forms_and_presets() {
        let _guard = lock_plot_registry();
        ensure_plot_test_env();
        reset_hold_state_for_run();
        let _ = clear_figure(None);

        let ax = Value::Num(crate::builtins::plotting::state::encode_axes_handle(
            current_figure_handle(),
            0,
        ));
        let _ = view_builtin(vec![ax.clone(), Value::Num(2.0)]).unwrap();
        let v = view_builtin(vec![ax.clone()]).unwrap();
        let t = Tensor::try_from(&v).unwrap();
        assert_eq!(t.data, vec![0.0, 90.0]);

        let _ = view_builtin(vec![ax.clone(), Value::Num(3.0)]).unwrap();
        let v = view_builtin(vec![ax]).unwrap();
        let t = Tensor::try_from(&v).unwrap();
        assert_eq!(t.data, vec![-37.5, 30.0]);
    }

    #[test]
    fn view_handles_large_angles_and_property_roundtrip() {
        let _guard = lock_plot_registry();
        ensure_plot_test_env();
        reset_hold_state_for_run();
        let _ = clear_figure(None);

        let ax = Value::Num(crate::builtins::plotting::state::encode_axes_handle(
            current_figure_handle(),
            0,
        ));
        let _ = view_builtin(vec![ax.clone(), Value::Num(405.0), Value::Num(89.0)]).unwrap();
        let queried = get_builtin(vec![ax.clone(), Value::String("View".into())]).unwrap();
        let t = Tensor::try_from(&queried).unwrap();
        assert_eq!(t.data, vec![405.0, 89.0]);

        set_builtin(vec![
            ax,
            Value::String("View".into()),
            Value::Tensor(Tensor {
                rows: 1,
                cols: 2,
                shape: vec![1, 2],
                data: vec![180.0, -30.0],
                dtype: runmat_builtins::NumericDType::F64,
            }),
        ])
        .unwrap();
        let fig = clone_figure(current_figure_handle()).unwrap();
        assert_eq!(fig.axes_metadata(0).unwrap().view_azimuth_deg, Some(180.0));
        assert_eq!(
            fig.axes_metadata(0).unwrap().view_elevation_deg,
            Some(-30.0)
        );
    }

    #[test]
    fn view_descriptor_signatures_cover_query_and_set_forms() {
        let labels: Vec<&str> = VIEW_DESCRIPTOR
            .signatures
            .iter()
            .map(|sig| sig.label)
            .collect();
        assert!(labels.contains(&"angles = view()"));
        assert!(labels.contains(&"angles = view(ax)"));
        assert!(labels.contains(&"angles = view([az el] | preset)"));
        assert!(labels.contains(&"angles = view(az, el)"));
        assert!(labels.contains(&"angles = view(ax, [az el] | preset)"));
        assert!(labels.contains(&"angles = view(ax, az, el)"));
    }
}
