mod bsjfl;
mod err;
mod file;
mod parse;
mod session;
mod statement;
mod value;

#[cfg(test)]
mod tests;

pub use value::Value;
pub use err::SJFLError;
pub use session::Session;

use statement::Statement;
use parse::{ Parse, skip_whitespaces_and_comments };
use bsjfl::decode_timestamp;

/// `s` shall not contain any statement. But it can use variables in the `session`.
///
/// The second field of the error variant is an error message.
pub fn evaluate(s: &str, session: &mut Session) -> Result<Value, (SJFLError, String)> {
    let mut ind = 0;
    let s = s.as_bytes();

    ind = skip_whitespaces_and_comments(s, ind).map_err(|e| (e.clone(), e.to_string(session, s)))?;

    let (mut v, _) = Value::parse(s, ind, session).map_err(|e| (e.clone(), e.to_string(session, s)))?;
    v.substitute_vars(session).map_err(|e| (e.clone(), e.to_string(session, s)))?;

    Ok(v)
}

/// `file_name` should not contain a file extension.
/// If `file_name` is `foo`, it looks for `foo.sjfl` and `foo.bsjfl` in the current directory.
/// It tries to use the binary one if available. You cannot access files in another directory.
///
/// The file shall not contain any statement. Variables that are defined in `session` are ignored. (TODO: It's not ignored in current implementation)
/// That means the content of the file cannot use any variable, at all.
///
/// It would be useful if you want to parse a json file.
///
/// The second field of the error variant is an error message.
pub fn evaluate_file(file_name: &str, session: &mut Session) -> Result<Value, (SJFLError, String)> {
    let mut candidates = (None, None, None);  // (sjfl, json, bsjfl)

    for file in session.files.iter() {

        if file.get_file_name() == file_name {

            if file.is_json {
                candidates.1 = Some(file.clone());
            }

            else if file.is_binary {
                candidates.2 = Some(file.clone());
            }

            else {
                candidates.0 = Some(file.clone());
            }

        }

    }

    // (sjfl, json, bsjfl)
    match candidates {
        (None, None, None) => Err(
            SJFLError::FileError(format!("failed to evaluate {file_name}! make sure that it does not contain extension (eg: `foo`, not `foo.sjfl`), and the session is properly initialized (call `session.init_dir` or `Session::new_dir`)"))
        ).map_err(|e| (e.clone(), e.to_string(session, &[]))),
        (Some(f), None, None) | (None, Some(f), None) | (Some(f), Some(_), None) => {

            match f.get_content() {
                Ok(b) => {
                    let result = evaluate(&String::from_utf8_lossy(&b), session)?;
                    let new_binary_file = f.save_binary(result.clone(), session, f.timestamp);
                    session.files.push(new_binary_file);

                    Ok(result)
                },
                Err(e) => Err(SJFLError::FileError(format!("{e:?}"))).map_err(|e| (e.clone(), e.to_string(session, &[])))
            }

        }
        (None, None, Some(f)) => {
            let mut cursor = 0;

            match f.get_content() {
                Ok(b) => Value::from_bin(&b, &mut cursor, session).map_err(|e| (e.clone(), e.to_string(session, &[]))),
                Err(e) => Err(SJFLError::FileError(format!("{e:?}"))).map_err(|e| (e.clone(), e.to_string(session, &[])))
            }

        }
        (Some(f), None, Some(bf)) | (None, Some(f), Some(bf)) | (Some(f), Some(_), Some(bf)) => {

            let (bf_content, has_to_read_raw_text) = if let Ok(bf_content) = bf.get_content() {

                if let Ok(timestamp) = decode_timestamp(&bf_content, session) {

                    if timestamp == f.timestamp {
                        (bf_content, false)
                    }

                    else {
                        (vec![], true)
                    }

                }

                else {
                    (vec![], true)
                }

            }

            else {
                (vec![], true)
            };

            if has_to_read_raw_text {

                match f.get_content() {
                    Ok(f_content) => {
                        let result = evaluate(&String::from_utf8_lossy(&f_content), session)?;
                        let new_binary_file = f.save_binary(result.clone(), session, f.timestamp);
                        session.files.push(new_binary_file);
    
                        Ok(result)
                    }
                    Err(e) => Err(SJFLError::FileError(
                        format!("Binary file is outdated, and raw-text file is broken: {e:?}"))
                    ).map_err(|e| (e.clone(), e.to_string(session, &[])))
                }

            }

            else {
                let mut cursor = 0;

                Value::from_bin(&bf_content, &mut cursor, session).map_err(|e| (e.clone(), e.to_string(session, &[])))
            }

        }
    }

}

/// Instead of returning the result, it mutates the `session`. It only returns `Err` when it fails.
///
/// The second field of the error variant is an error message.
pub fn execute(s: &str, session: &mut Session) -> Result<(), (SJFLError, String)> {
    let mut ind = 0;
    let s = s.as_bytes();

    loop {
        ind = match skip_whitespaces_and_comments(s, ind) {
            Err(SJFLError::UnexpectedEof(_)) => { return Ok(()); },  // it's not unexpected!
            Err(e) => { return Err((e.clone(), e.to_string(session, s))); }
            Ok(n) => n
        };

        let (st, new_ind) = Statement::parse(s, ind, session).map_err(|e| (e.clone(), e.to_string(session, s)))?;
        ind = new_ind;

        st.execute(session).map_err(|e| (e.clone(), e.to_string(session, s)))?;
    }

}

/// The second field of the error variant is an error message.
pub fn execute_file(file_name: &str, session: &mut Session) -> Result<(), (SJFLError, String)> {
    todo!()
}