use crate::Session;
use crate::parse::Variable;
use hmath::ConversionError;

#[derive(Clone, Debug)]
pub enum SJFLError {
    UnexpectedEof(Span),
    UnexpectedChar(u8, Span),
    NotDefinedVar(Variable),

    // expected a variable name, but found a keyword
    InvalidVarName(String, Span),

    MalformedString(u32),  // when the index is not in the table,

    FileError(String),
    CorruptedBinarySJFL(String)
}

impl SJFLError {

    pub(crate) fn to_string(&self, session: &mut Session, buffer: &[u8]) -> String {
        match self {
            SJFLError::UnexpectedEof(s) => if s.is_binary {
                String::from("Unexpected EOF while parsing a binary SJFL file!")
            } else {
                format!("Unexpected EOF while parsing an SJFL file!\n\n{}", s.get_preview(buffer))
            },
            SJFLError::UnexpectedChar(c, s) => {
                let char_preview = if 32 <= *c && *c <= 126 {
                    format!("{:?}", *c as char)
                } else if *c >= 240 {
                    format!("{}", std::str::from_utf8(&buffer[s.index..(s.index + 4)]).unwrap_or("?"))
                } else if *c >= 224 {
                    format!("{}", std::str::from_utf8(&buffer[s.index..(s.index + 3)]).unwrap_or("?"))
                } else if *c >= 192 {
                    format!("{}", std::str::from_utf8(&buffer[s.index..(s.index + 2)]).unwrap_or("?"))
                } else {
                    format!("\\{c:03}")
                };

                format!("Unexpected char `{char_preview}` is found while parsing an SJFL file!\n\n{}", s.get_preview(buffer))
            }
            SJFLError::NotDefinedVar(v) => {
                let var_name = session.get_string_from_index(v.index).unwrap_or(vec![b'?'; 4]);
                let var_name = String::from_utf8_lossy(&var_name);
                let preview = if v.span.is_binary {
                    String::new()
                } else {
                    format!("\n\n{}", v.span.get_preview(buffer))
                };

                format!("Undefined Variable `{var_name}` is found!{preview}")
            },
            SJFLError::InvalidVarName(v, s) => {
                let preview = if s.is_binary {
                    String::new()
                } else {
                    format!("\n\n{}", s.get_preview(buffer))
                };

                format!("Expected a variable name, but found `{v}`, which is a keyword!{preview}")
            }
            SJFLError::FileError(e) | SJFLError::CorruptedBinarySJFL(e) => e.to_string(),
            SJFLError::MalformedString(s) => format!(
                "Interned String {s} is not in the cache. This is very likely due to a bug in SJFL itself or the application using SJFL, not the user input. Please contact the providers of the application, or open an issue in the SJFL repo."
            )
        }
    }

}

#[derive(Clone, Debug)]
pub struct Span {
    index: usize,
    pub is_binary: bool,  // binary strings don't generate spans
}

impl Span {

    pub fn binary() -> Self {
        Span {
            index: usize::MAX, is_binary: true
        }
    }

    pub fn from_index(index: usize) -> Self {
        Span { index, is_binary: false }
    }

    pub fn get_preview(&self, buffer: &[u8]) -> String {

        if self.is_binary {
            return String::from("Span is not available for binary files!");
        }

        let mut row = 0;
        let mut lines = vec![];
        let mut tmp_lines = vec![];
    
        for (i, c) in buffer.iter().enumerate() {
    
            if *c == b'\n' {
                lines.push(tmp_lines);
                tmp_lines = vec![];
            }
    
            else {
                tmp_lines.push(*c);
            }
    
            if self.index == i {
                row = lines.len();
            }
    
        }
    
        lines.push(tmp_lines);
    
        let preview = lines.into_iter().enumerate().map(
            |(index, line)| {
                let marker = if index == row {
                    vec![b'>'; 3]
                } else {
                    vec![b' '; 3]
                };
    
                let line_no = format!(
                    "{index:05} | "
                ).as_bytes().to_vec();
    
                let content = if line.len() > 86 {
                    vec![
                        cut_char(&line, 80).to_vec(),
                        vec![b'.'; 3]
                    ].concat()
                } else {
                    line
                };
    
                let line = vec![
                    marker, line_no, content
                ].concat();
    
                String::from_utf8_lossy(&line).to_string()
            }
        ).collect::<Vec<String>>();

        let preview_start_index = row.max(3) - 3;
        let preview_end_index = (preview_start_index + 7).min(preview.len());

        let preview = preview[preview_start_index..preview_end_index].to_vec();
    
        preview.join("\n")
    }

}

fn cut_char(line: &[u8], length: usize) -> Vec<u8> {
    let mut result = Vec::with_capacity(length + 3);

    for (ind, c) in line.iter().enumerate() {
        result.push(*c);

        if ind > length {

            if *c < 128 {
                break;
            }

            else if *c >= 192 {
                result.pop().unwrap();
                break;
            }

        }

    }

    result
}