use crate::schala::{SourceReference, Stage};
use crate::source_map::Location;
use crate::tokenizing::{Token, TokenKind};
use crate::parsing::ParseError;
use crate::typechecking::TypeError;

pub struct SchalaError {
  errors: Vec<Error>,
  //TODO unify these sometime
  formatted_parse_error: Option<String>,
}

impl SchalaError {

  pub(crate) fn display(&self) -> String {
    if let Some(ref err) = self.formatted_parse_error {
      err.clone()
    } else {
      self.errors[0].text.as_ref().cloned().unwrap_or_default()
    }
  }

  pub(crate) fn from_type_error(err: TypeError) -> Self {
    Self {
      formatted_parse_error: None,
      errors: vec![
        Error { location: None, text: Some(err.msg), stage: Stage::Typechecking }
      ]
    }
  }

  pub(crate) fn from_string(text: String, stage: Stage) -> Self {
    Self {
      formatted_parse_error: None,
      errors: vec![
        Error { location: None, text: Some(text), stage }
      ]
    }
  }

  pub(crate) fn from_parse_error(parse_error: ParseError, source_reference: &SourceReference) -> Self {
    Self {
      formatted_parse_error: Some(format_parse_error(parse_error, source_reference)),
      errors: vec![],
    }
  }

  pub(crate) fn from_tokens(tokens: &[Token]) -> Option<SchalaError> {
    let token_errors: Vec<Error> = tokens.iter()
      .filter_map(|tok| match tok.kind {
        TokenKind::Error(ref err) => Some(Error {
          location: Some(tok.location),
          text: Some(err.clone()),
          stage: Stage::Tokenizing,
        }),
        _ => None
      }).collect();

    if token_errors.is_empty() {
      None
    } else {
      Some(SchalaError {
        errors: token_errors,
        formatted_parse_error: None,
      })
    }
  }
}

#[allow(dead_code)]
struct Error {
  location: Option<Location>,
  text: Option<String>,
  stage: Stage,
}


fn format_parse_error(error: ParseError, source_reference: &SourceReference) -> String {
  let line_num = error.token.location.line_num;
  let ch = error.token.location.char_num;
  let line_from_program = source_reference.get_line(line_num);
  let location_pointer = format!("{}^", " ".repeat(ch));

  let line_num_digits = format!("{}", line_num).chars().count();
  let space_padding = " ".repeat(line_num_digits);

  let production = match error.production_name {
    Some(n) => format!("\n(from production \"{}\")", n),
    None => "".to_string()
  };

  format!(r#"
{error_msg}{production}
{space_padding} |
{line_num} | {}
{space_padding} | {}
"#, line_from_program, location_pointer, error_msg=error.msg, space_padding=space_padding, line_num=line_num, production=production
)
}