use indexmap::IndexMap;
use nom::{
  Parser,
  branch::alt,
  bytes::complete::{is_not, tag, take_while1},
  character::complete::multispace0,
  combinator::{cut, map, opt, value},
  multi::{many0, separated_list0, separated_list1},
  sequence::{delimited, terminated},
};

use crate::{common::*, gqls::prelude::*};

#[derive(Debug)]
pub(crate) enum ParseTreeElement
{
  Property
  {
    label: String,
    properties: IndexMap<String, Property>,
    parent: Option<String>,
  },
  Node
  {
    label: String
  },
  Edge
  {
    source: String,
    label: String,
    destination: String,
  },
}

#[inline]
fn is_label(chr: char) -> bool
{
  chr.is_alphanumeric() || chr == '_'
}

fn parse_label(input: &str) -> VResult<&str, &str>
{
  take_while1(is_label).parse(input)
}

#[inline]
fn is_identifier(chr: char) -> bool
{
  chr.is_alphanumeric() || chr == '_'
}

fn parse_identifier(input: &str) -> VResult<&str, &str>
{
  take_while1(is_identifier).parse(input)
}

fn parse_property(input: &str) -> VResult<&str, Property>
{
  map(
    (
      alt((
        map(tag("STRING"), |_| {
          Property::Literal(LiteralBaseType::String.into())
        }),
        map(tag("FLOAT"), |_| {
          Property::Literal(LiteralBaseType::Float.into())
        }),
        map(tag("INTEGER"), |_| {
          Property::Literal(LiteralBaseType::Integer.into())
        }),
        map(tag("BOOLEAN"), |_| {
          Property::Literal(LiteralBaseType::Boolean.into())
        }),
        map(tag("TIMESTAMP"), |_| {
          Property::Literal(LiteralBaseType::TimeStamp.into())
        }),
        map(parse_property_map, |property_map| {
          Property::Map(property_map)
        }),
        map((tag("["), parse_property, tag("]")), |(_, property, _)| {
          Property::Array(Box::new(property))
        }),
      )),
      opt(ws0(tag("?"))),
    ),
    |(p, q)| match q
    {
      Some(_) => Property::Optional(Box::new(p)),
      None => p,
    },
  )
  .parse(input)
}

fn parse_property_map(input: &str) -> VResult<&str, IndexMap<String, Property>>
{
  map(
    (
      tag("{"),
      opt((
        separated_list1(
          ws0(tag(",")),
          map(
            (ws0(parse_identifier), ws0(tag(":")), ws0(parse_property)),
            |(name, _, property)| (name.to_string(), property),
          ),
        ),
        opt(ws0(tag(","))),
      )),
      opt(ws0(tag(","))),
      ws0(tag("}")),
    ),
    |(_, properties, _, _)| {
      properties
        .map(|(x, _)| x.into_iter().collect())
        .unwrap_or_default()
    },
  )
  .parse(input)
}

/// parse a property definition
fn parse_property_definition(input: &str) -> VResult<&str, ParseTreeElement>
{
  alt((
    map(
      (
        parse_label,
        ws0(tag("<:")),
        ws0(parse_label),
        ws0(parse_property_map),
      ),
      |(label, _, parent, properties)| ParseTreeElement::Property {
        label: label.to_string(),
        properties,
        parent: Some(parent.to_string()),
      },
    ),
    map(
      (parse_label, ws0(parse_property_map)),
      |(label, properties)| ParseTreeElement::Property {
        label: label.to_string(),
        properties,
        parent: None,
      },
    ),
  ))
  .parse(input)
}

fn parse_node_definition(input: &str) -> VResult<&str, ParseTreeElement>
{
  map(
    (tag("("), ws0(parse_label), ws0(tag(")"))),
    |(_, label, _)| ParseTreeElement::Node {
      label: label.to_string(),
    },
  )
  .parse(input)
}
fn parse_edge_definition(input: &str) -> VResult<&str, ParseTreeElement>
{
  map(
    (
      tag("("),
      ws0(parse_label),
      ws0(tag(")-[")),
      ws0(parse_label),
      ws0(tag("]->(")),
      ws0(parse_label),
      ws0(tag(")")),
    ),
    |(_, source, _, label, _, destination, _)| ParseTreeElement::Edge {
      source: source.to_string(),
      label: label.to_string(),
      destination: destination.to_string(),
    },
  )
  .parse(input)
}

fn single_line_comment(input: &str) -> VResult<&str, ()>
{
  value((), (multispace0, tag("//"), is_not("\n\r"), multispace0)).parse(input)
}

/// parse the schema
pub(crate) fn parse_schema(input: &str) -> VResult<&str, Vec<ParseTreeElement>>
{
  terminated(
    separated_list0(
      tag(","),
      cut(ws0(delimited(
        many0(single_line_comment),
        alt((
          parse_property_definition,
          parse_edge_definition,
          parse_node_definition,
        )),
        many0(single_line_comment),
      ))),
    ),
    multispace0,
  )
  .parse(input)
}

#[cfg(test)]
mod tests
{
  #[test]
  fn test_single_line_comment()
  {
    assert!(super::single_line_comment("// hello").is_ok());
    assert!(super::single_line_comment("// hello\n").is_ok());
    assert!(super::single_line_comment("/ hello").is_err());
    assert!(super::single_line_comment(" // hello").is_ok());
    assert!(super::single_line_comment("\n // hello").is_ok());
  }
}
