mod edn;
mod tag;

use std::cmp::Ordering::*;
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use std::sync::Arc;
use std::vec;

use cirru_parser::{Cirru, CirruWriterOptions};

pub use edn::{
  is_simple_char, DynEq, Edn, EdnAnyRef, EdnListView, EdnMapView, EdnRecordView, EdnSetView, EdnTupleView,
};
pub use tag::EdnTag;

/// parse Cirru code into data
pub fn parse(s: &str) -> Result<Edn, String> {
  let xs = cirru_parser::parse(s)?;
  if xs.len() == 1 {
    match &xs[0] {
      Cirru::Leaf(s) => Err(format!("expected expr for data, got leaf: {}", s)),
      Cirru::List(_) => extract_cirru_edn(&xs[0]),
    }
  } else {
    Err(format!("Expected 1 expr for edn, got length {}: {:?} ", xs.len(), xs))
  }
}

fn extract_cirru_edn(node: &Cirru) -> Result<Edn, String> {
  match node {
    Cirru::Leaf(s) => match &**s {
      "nil" => Ok(Edn::Nil),
      "true" => Ok(Edn::Bool(true)),
      "false" => Ok(Edn::Bool(false)),
      "" => Err(String::from("empty string is invalid for edn")),
      s1 => match s1.chars().next().unwrap() {
        '\'' => Ok(Edn::Symbol(s1[1..].into())),
        ':' => Ok(Edn::tag(&s1[1..])),
        '"' | '|' => Ok(Edn::Str(s1[1..].into())),
        _ => {
          if let Ok(f) = s1.trim().parse::<f64>() {
            Ok(Edn::Number(f))
          } else {
            Err(format!("unknown token for edn value: {:?}", s1))
          }
        }
      },
    },
    Cirru::List(xs) => {
      if xs.is_empty() {
        Err(String::from("empty expr is invalid for edn"))
      } else {
        match &xs[0] {
          Cirru::Leaf(s) => match &**s {
            "quote" => {
              if xs.len() == 2 {
                Ok(Edn::Quote(xs[1].to_owned()))
              } else {
                Err(String::from("missing edn quote value"))
              }
            }
            "do" => {
              let mut ret: Option<Edn> = None;

              for x in xs.iter().skip(1) {
                if is_comment(x) {
                  continue;
                }
                if ret.is_some() {
                  return Err(String::from("multiple values in do"));
                }
                ret = Some(extract_cirru_edn(x)?);
              }
              if ret.is_none() {
                return Err(String::from("missing edn do value"));
              }
              ret.ok_or_else(|| String::from("missing edn do value"))
            }
            "::" => {
              let mut tag: Option<Edn> = None;
              let mut extra: Vec<Edn> = vec![];
              for x in xs.iter().skip(1) {
                if is_comment(x) {
                  continue;
                }
                if tag.is_some() {
                  extra.push(extract_cirru_edn(x)?);
                  continue;
                } else {
                  tag = Some(extract_cirru_edn(x)?);
                }
              }
              if let Some(x0) = tag {
                Ok(Edn::Tuple(EdnTupleView {
                  tag: Arc::new(x0),
                  extra,
                }))
              } else {
                Err(String::from("missing edn :: fst value"))
              }
            }
            "[]" => {
              let mut ys: Vec<Edn> = Vec::with_capacity(xs.len() - 1);
              for x in xs.iter().skip(1) {
                if is_comment(x) {
                  continue;
                }
                match extract_cirru_edn(x) {
                  Ok(v) => ys.push(v),
                  Err(v) => return Err(v),
                }
              }
              Ok(Edn::List(EdnListView(ys)))
            }
            "#{}" => {
              #[allow(clippy::mutable_key_type)]
              let mut ys: HashSet<Edn> = HashSet::new();
              for x in xs.iter().skip(1) {
                if is_comment(x) {
                  continue;
                }
                match extract_cirru_edn(x) {
                  Ok(v) => {
                    ys.insert(v);
                  }
                  Err(v) => return Err(v),
                }
              }
              Ok(Edn::Set(EdnSetView(ys)))
            }
            "{}" => {
              #[allow(clippy::mutable_key_type)]
              let mut zs: HashMap<Edn, Edn> = HashMap::new();
              for x in xs.iter().skip(1) {
                if is_comment(x) {
                  continue;
                }
                match x {
                  Cirru::Leaf(s) => return Err(format!("expected a pair, invalid map entry: {}", s)),
                  Cirru::List(ys) => {
                    if ys.len() == 2 {
                      match (extract_cirru_edn(&ys[0]), extract_cirru_edn(&ys[1])) {
                        (Ok(k), Ok(v)) => {
                          zs.insert(k, v);
                        }
                        (Err(e), _) => return Err(format!("invalid map entry `{}` from `{}`", e, &ys[0])),
                        (Ok(k), Err(e)) => return Err(format!("invalid map entry for `{}`, got {}", k, e)),
                      }
                    }
                  }
                }
              }
              Ok(Edn::Map(EdnMapView(zs)))
            }
            "%{}" => {
              if xs.len() >= 3 {
                let name = match &xs[1] {
                  Cirru::Leaf(s) => EdnTag::new(s.strip_prefix(':').unwrap_or(s)),
                  Cirru::List(e) => return Err(format!("expected record name in string: {:?}", e)),
                };
                let mut entries: Vec<(EdnTag, Edn)> = Vec::with_capacity(xs.len() - 1);

                for x in xs.iter().skip(2) {
                  if is_comment(x) {
                    continue;
                  }
                  match x {
                    Cirru::Leaf(s) => return Err(format!("expected record, invalid record entry: {}", s)),
                    Cirru::List(ys) => {
                      if ys.len() == 2 {
                        match (&ys[0], extract_cirru_edn(&ys[1])) {
                          (Cirru::Leaf(s), Ok(v)) => {
                            entries.push((EdnTag::new(s.strip_prefix(':').unwrap_or(s)), v));
                          }
                          (Cirru::Leaf(s), Err(e)) => {
                            return Err(format!("invalid record value for `{}`, got: {}", s, e))
                          }
                          (Cirru::List(zs), _) => return Err(format!("invalid list as record key: {:?}", zs)),
                        }
                      } else {
                        return Err(format!("expected pair of 2: {:?}", ys));
                      }
                    }
                  }
                }
                if entries.is_empty() {
                  return Err(String::from("empty record is invalid"));
                }
                Ok(Edn::Record(EdnRecordView {
                  tag: name,
                  pairs: entries,
                }))
              } else {
                Err(String::from("insufficient items for edn record"))
              }
            }
            "buf" => {
              let mut ys: Vec<u8> = Vec::with_capacity(xs.len() - 1);
              for x in xs.iter().skip(1) {
                if is_comment(x) {
                  continue;
                }
                match x {
                  Cirru::Leaf(y) => {
                    if y.len() == 2 {
                      match hex::decode(&(**y)) {
                        Ok(b) => {
                          if b.len() == 1 {
                            ys.push(b[0])
                          } else {
                            return Err(format!("hex for buffer might be too large, got: {:?}", b));
                          }
                        }
                        Err(e) => return Err(format!("expected length 2 hex string in buffer, got: {} {}", y, e)),
                      }
                    } else {
                      return Err(format!("expected length 2 hex string in buffer, got: {}", y));
                    }
                  }
                  _ => return Err(format!("expected hex string in buffer, got: {}", x)),
                }
              }
              Ok(Edn::Buffer(ys))
            }
            "atom" => {
              if xs.len() == 2 {
                Ok(Edn::Atom(Box::new(extract_cirru_edn(&xs[1])?)))
              } else {
                Err(String::from("missing edn atom value"))
              }
            }
            a => Err(format!("invalid operator for edn: {}", a)),
          },
          Cirru::List(a) => Err(format!("invalid nodes for edn: {:?}", a)),
        }
      }
    }
  }
}

fn is_comment(node: &Cirru) -> bool {
  match node {
    Cirru::Leaf(_) => false,
    Cirru::List(xs) => xs.first() == Some(&Cirru::Leaf(";".into())),
  }
}

fn assemble_cirru_node(data: &Edn) -> Cirru {
  match data {
    Edn::Nil => "nil".into(),
    Edn::Bool(v) => v.to_string().as_str().into(),
    Edn::Number(n) => n.to_string().as_str().into(),
    Edn::Symbol(s) => format!("'{}", s).as_str().into(),
    Edn::Tag(s) => format!(":{}", s).as_str().into(),
    Edn::Str(s) => format!("|{}", s).as_str().into(),
    Edn::Quote(v) => Cirru::List(vec!["quote".into(), (*v).to_owned()]),
    Edn::List(xs) => {
      let mut ys: Vec<Cirru> = Vec::with_capacity(xs.len() + 1);
      ys.push("[]".into());
      for x in xs {
        ys.push(assemble_cirru_node(x));
      }
      Cirru::List(ys)
    }
    Edn::Set(xs) => {
      let mut ys: Vec<Cirru> = Vec::with_capacity(xs.len() + 1);
      ys.push("#{}".into());
      let mut items = xs.0.iter().collect::<Vec<_>>();
      items.sort();
      for x in items {
        ys.push(assemble_cirru_node(x));
      }
      Cirru::List(ys)
    }
    Edn::Map(xs) => {
      let mut ys: Vec<Cirru> = Vec::with_capacity(xs.len() + 1);
      ys.push("{}".into());
      let mut items = Vec::from_iter(xs.0.iter());
      items.sort_by(|(a1, a2): &(&Edn, &Edn), (b1, b2): &(&Edn, &Edn)| {
        match (a1.is_literal(), b1.is_literal(), a2.is_literal(), b2.is_literal()) {
          (true, true, true, false) => Less,
          (true, true, false, true) => Greater,
          (true, false, ..) => Less,
          (false, true, ..) => Greater,
          _ => a1.cmp(b1),
        }
      });
      for (k, v) in items {
        ys.push(Cirru::List(vec![assemble_cirru_node(k), assemble_cirru_node(v)]))
      }
      Cirru::List(ys)
    }
    Edn::Record(EdnRecordView {
      tag: name,
      pairs: entries,
    }) => {
      let mut ys: Vec<Cirru> = Vec::with_capacity(entries.len() + 2);
      ys.push("%{}".into());
      ys.push(format!(":{}", name).as_str().into());
      let mut ordered_entries = entries.to_owned();
      ordered_entries.sort_by(|(_a1, a2), (_b1, b2)| match (a2.is_literal(), b2.is_literal()) {
        (true, false) => Less,
        (false, true) => Greater,
        _ => Equal,
      });
      for entry in ordered_entries {
        let v = &entry.1;
        ys.push(Cirru::List(vec![
          format!(":{}", entry.0).as_str().into(),
          assemble_cirru_node(v),
        ]));
      }

      Cirru::List(ys)
    }
    Edn::Tuple(EdnTupleView { tag, extra }) => {
      let mut ys: Vec<Cirru> = vec!["::".into(), assemble_cirru_node(tag)];
      for item in extra {
        ys.push(assemble_cirru_node(item))
      }
      Cirru::List(ys)
    }
    Edn::Buffer(buf) => {
      let mut ys: Vec<Cirru> = Vec::with_capacity(buf.len() + 1);
      ys.push("buf".into());
      for b in buf {
        ys.push(hex::encode(vec![b.to_owned()]).as_str().into());
      }
      Cirru::List(ys)
    }
    Edn::AnyRef(..) => unreachable!("AnyRef is not serializable"),
    Edn::Atom(v) => {
      let ys = vec!["atom".into(), assemble_cirru_node(v)];
      Cirru::List(ys)
    }
  }
}

/// generate string from Edn
pub fn format(data: &Edn, use_inline: bool) -> Result<String, String> {
  let options = CirruWriterOptions { use_inline };
  match assemble_cirru_node(data) {
    Cirru::Leaf(s) => cirru_parser::format(&[vec!["do", &*s].into()], options),
    Cirru::List(xs) => cirru_parser::format(&[(Cirru::List(xs))], options),
  }
}
