//! JSON-based project file persistence with versioned format.

use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::project::{Bank, PatternSpeed, Project};

const VERSION: u8 = 1;
const EXTENSION: &str = "cagire";

fn ensure_extension(path: &Path) -> PathBuf {
    if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
        path.to_path_buf()
    } else {
        path.with_extension(EXTENSION)
    }
}

#[derive(Serialize, Deserialize)]
struct ProjectFile {
    version: u8,
    banks: Vec<Bank>,
    #[serde(default)]
    sample_paths: Vec<PathBuf>,
    #[serde(default = "default_tempo")]
    tempo: f64,
    #[serde(default)]
    playing_patterns: Vec<(usize, usize)>,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    prelude: String,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    script: String,
    #[serde(default, skip_serializing_if = "is_default_speed")]
    script_speed: PatternSpeed,
    #[serde(default = "default_script_length", skip_serializing_if = "is_default_script_length")]
    script_length: usize,
}

fn is_default_speed(s: &PatternSpeed) -> bool {
    *s == PatternSpeed::default()
}

fn default_script_length() -> usize {
    16
}

fn is_default_script_length(n: &usize) -> bool {
    *n == default_script_length()
}

fn default_tempo() -> f64 {
    120.0
}

impl From<&Project> for ProjectFile {
    fn from(project: &Project) -> Self {
        Self {
            version: VERSION,
            banks: project.banks.clone(),
            sample_paths: project.sample_paths.clone(),
            tempo: project.tempo,
            playing_patterns: project.playing_patterns.clone(),
            prelude: project.prelude.clone(),
            script: project.script.clone(),
            script_speed: project.script_speed,
            script_length: project.script_length,
        }
    }
}

impl From<ProjectFile> for Project {
    fn from(file: ProjectFile) -> Self {
        let mut project = Self {
            banks: file.banks,
            sample_paths: file.sample_paths,
            tempo: file.tempo,
            playing_patterns: file.playing_patterns,
            prelude: file.prelude,
            script: file.script,
            script_speed: file.script_speed,
            script_length: file.script_length,
        };
        project.normalize();
        project
    }
}

/// Error returned by project save/load operations.
#[derive(Debug)]
pub enum FileError {
    Io(io::Error),
    Json(serde_json::Error),
    Version(u8),
}

impl std::fmt::Display for FileError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            FileError::Io(e) => write!(f, "IO error: {e}"),
            FileError::Json(e) => write!(f, "JSON error: {e}"),
            FileError::Version(v) => write!(f, "Unsupported version: {v}"),
        }
    }
}

impl From<io::Error> for FileError {
    fn from(e: io::Error) -> Self {
        FileError::Io(e)
    }
}

impl From<serde_json::Error> for FileError {
    fn from(e: serde_json::Error) -> Self {
        FileError::Json(e)
    }
}

/// Write a project to disk as pretty-printed JSON, returning the final path.
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
    let path = ensure_extension(path);
    let file = ProjectFile::from(project);
    let json = serde_json::to_string_pretty(&file)?;
    fs::write(&path, json)?;
    Ok(path)
}

/// Read a project from a `.cagire` file on disk.
pub fn load(path: &Path) -> Result<Project, FileError> {
    let json = fs::read_to_string(path)?;
    load_str(&json)
}

/// Parse a project from a JSON string.
pub fn load_str(json: &str) -> Result<Project, FileError> {
    let file: ProjectFile = serde_json::from_str(json)?;
    if file.version > VERSION {
        return Err(FileError::Version(file.version));
    }
    Ok(Project::from(file))
}
