use std::fs::File;
use std::io::prelude::*;
use termion::color;

use super::ast::*;
use super::error::*;
use super::location::*;
use super::syntax;
use super::visitor::*;

pub enum Item {
  Decl(Decl),
  Fact(Fact),
  Disjunction(Disjunction),
  Rule(Rule),
}

fn row_col(src: &str, byte_offset: usize) -> (usize, usize) {
  let mut curr_line_num = 1;
  let mut curr_line_start = 0;
  for i in 0..src.len() {
    if src.chars().nth(i).unwrap() == '\n' {
      curr_line_num += 1;
      curr_line_start = i + 1;
    }
    if i == byte_offset {
      return (curr_line_num, i - curr_line_start + 1);
    }
  }
  panic!("Byte offset not hit");
}

pub fn parse_rule(s: &str) -> Result<Rule, CompileError> {
  let parser = syntax::RuleParser::new();
  let s_with_eof = format!("{}\n", s);
  let result = parser.parse(&s_with_eof).map_err(|e| {
    match &e {
      lalrpop_util::ParseError::UnrecognizedToken { token, .. } => {
        let offset = token.0;
        let (row, col) = row_col(s, offset);
        println!(
          "{}[Error]{} Syntax error at row {} col {}:",
          color::Fg(color::Red),
          color::Fg(color::Reset),
          row,
          col,
        );
        let snippet = s[(offset - 10).max(10)..(offset + 10).min(s.len())].to_string();
        println!("{}", snippet);
        println!("{}", e);
      }
      _ => {
        println!("{}", e);
      }
    }
    CompileError::SyntaxError
  });
  result
}

fn parse_items(s: &str) -> Result<Vec<Item>, CompileError> {
  let parser = syntax::ItemsParser::new();
  let s_with_eof = format!("{}\n", s);
  parser.parse(&s_with_eof).map_err(|e| {
    match &e {
      lalrpop_util::ParseError::UnrecognizedToken { token, .. } => {
        let offset = token.0;
        let (row, col) = row_col(s, offset);
        println!(
          "{}[Error]{} Syntax error at row {} col {}:",
          color::Fg(color::Red),
          color::Fg(color::Reset),
          row,
          col,
        );
        let snippet = s[(offset - 10).max(10)..(offset + 10).min(s.len())].to_string();
        println!("{}", snippet);
        println!("{}", e);
      }
      _ => {
        println!("{}", e);
      }
    }
    CompileError::SyntaxError
  })
}

struct LocationAssigner {
  source: String,
  id_counter: usize,
}

impl LocationAssigner {
  fn new(source: &str) -> Self {
    Self {
      source: source.to_string(),
      id_counter: 0,
    }
  }

  fn assign_and_increment_counter(&mut self, l: &mut Location) {
    let (row, col) = row_col(&self.source, l.byte_offset);
    l.id = self.id_counter;
    l.row = row;
    l.col = col;
    self.id_counter += 1;
  }
}

impl NodeVisitorMut for LocationAssigner {
  fn visit_location(&mut self, l: &mut Location) -> Result<(), CompileError> {
    self.assign_and_increment_counter(l);
    Ok(())
  }
}

fn assign_node_locations(src: &str, ast: &mut Program) {
  let mut assigner = LocationAssigner::new(src);

  // Use unwrap because this cannot fail
  visit_program_mut(&mut assigner, ast).unwrap();
}

pub fn parse_str(s: &str) -> Result<Program, CompileError> {
  let items = parse_items(s)?;
  let mut decls = vec![];
  let mut facts = vec![];
  let mut disjunctions = vec![];
  let mut rules = vec![];
  for item in items {
    match item {
      Item::Decl(d) => decls.push(d),
      Item::Fact(f) => facts.push(f),
      Item::Disjunction(d) => disjunctions.push(d),
      Item::Rule(r) => rules.push(r),
    }
  }
  let mut ast = Program {
    decls,
    facts,
    disjunctions,
    rules,
  };
  assign_node_locations(s, &mut ast);
  Ok(ast)
}

pub fn parse_file(filename: &str) -> Result<Program, CompileError> {
  // Initialize a string containing file content
  let mut contents = String::new();

  // Open the file with the given file path
  let mut file = File::open(filename).map_err(|_| CompileError::CannotOpenFile)?;

  // Read the file content into the string
  file
    .read_to_string(&mut contents)
    .map_err(|_| CompileError::CannotReadFile)?;

  // Parse the string
  parse_str(&contents)
}
