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

use cow_utils::CowUtils;
use proc_macro2::TokenStream;

use crate::{log, log_result};

#[cfg(feature = "generate-js")]
pub mod javascript;
mod rust;
mod yaml;
#[cfg(feature = "generate-js")]
use javascript::print_javascript;
use rust::{print_rust, rust_fmt};
use yaml::print_yaml;

/// Get path for an output.
pub fn output_path(krate: &str, path: &str) -> String {
    format!("{krate}/src/generated/{path}")
}

/// Add a generated file warning to top of file.
fn add_header(code: &str, generator_path: &str, comment_start: &str) -> String {
    format!(
        "{comment_start} Auto-generated code, DO NOT EDIT DIRECTLY!\n\
        {comment_start} To edit this generated file you have to edit `{generator_path}`.\n\n\
        {code}"
    )
}

/// An output from codegen.
///
/// Can be Rust, Javascript, or other formats.
#[expect(dead_code)]
pub enum Output {
    Rust {
        path: String,
        tokens: TokenStream,
    },
    RustString {
        path: String,
        code: String,
    },
    #[cfg(feature = "generate-js")]
    Javascript {
        path: String,
        code: String,
    },
    Yaml {
        path: String,
        code: String,
    },
    Raw {
        path: String,
        code: String,
    },
}

impl Output {
    /// Convert [`Output`] to [`RawOutput`].
    ///
    /// This involves printing and formatting the output.
    pub fn into_raw(self, generator_path: &str) -> RawOutput {
        let generator_path = generator_path.cow_replace('\\', "/");

        let (path, code) = match self {
            Self::Rust { path, tokens } => {
                let code = print_rust(&tokens, &generator_path);
                (path, code)
            }
            Self::RustString { path, code } => {
                let code = rust_fmt(&code);
                (path, code)
            }
            #[cfg(feature = "generate-js")]
            Self::Javascript { path, code } => {
                let code = print_javascript(&code, &generator_path);
                (path, code)
            }
            Self::Yaml { path, code } => {
                let code = print_yaml(&code, &generator_path);
                (path, code)
            }
            Self::Raw { path, code } => (path, code),
        };
        RawOutput { path, content: code.into_bytes() }
    }
}

/// A raw output from codegen.
///
/// Content is formatted, and in byte array form, ready to write to file.
#[derive(Debug)]
pub struct RawOutput {
    pub path: String,
    pub content: Vec<u8>,
}

impl RawOutput {
    /// Write [`RawOutput`] to file
    pub fn write_to_file(&self, root_path: &Path) -> io::Result<()> {
        log!("Write {}... ", &self.path);
        let result = write_to_file_impl(&self.content, &self.path, root_path);
        log_result!(result);
        result
    }
}

fn write_to_file_impl(data: &[u8], path: &str, root_path: &Path) -> io::Result<()> {
    let path = root_path.join(path);

    // If contents hasn't changed, don't touch the file
    if let Ok(existing_data) = fs::read(&path)
        && existing_data == data
    {
        return Ok(());
    }

    if let Some(parent_path) = path.parent() {
        fs::create_dir_all(parent_path)?;
    }
    fs::write(path, data)
}
