use crate::{Color, Date, Error, GameResult, GameType, Win};
use chrono::Datelike;
use nom::{
    branch::alt,
    bytes::complete::{escaped_transform, tag, take_until, take_until1},
    character::complete::{alpha1, digit1, multispace0, multispace1, none_of},
    combinator::{opt, value},
    error::ParseError,
    multi::{many0, many1, separated_list1},
    IResult, Parser,
};
use serde::{Deserialize, Serialize};
use std::{fmt::Write, num::ParseIntError, time::Duration};

impl From<ParseSizeError> for Error {
    fn from(_: ParseSizeError) -> Self {
        Self::InvalidBoardSize
    }
}

#[derive(Debug)]
pub enum ParseSizeError {
    ParseIntError(ParseIntError),
    InsufficientArguments,
}

impl From<ParseIntError> for ParseSizeError {
    fn from(e: ParseIntError) -> Self {
        Self::ParseIntError(e)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum Annotation {
    BadMove,
    DoubtfulMove,
    InterestingMove,
    Tesuji,
}

#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum Evaluation {
    Even,
    GoodForBlack,
    GoodForWhite,
    Unclear,
}

impl From<&str> for GameType {
    fn from(s: &str) -> Self {
        match s {
            "1" => Self::Go,
            "2" => Self::Othello,
            "3" => Self::Chess,
            "4" => Self::GomokuRenju,
            "5" => Self::NineMensMorris,
            "6" => Self::Backgammon,
            "7" => Self::ChineseChess,
            "8" => Self::Shogi,
            "9" => Self::LinesOfAction,
            "10" => Self::Ataxx,
            "11" => Self::Hex,
            "12" => Self::Jungle,
            "13" => Self::Neutron,
            "14" => Self::PhilosophersFootball,
            "15" => Self::Quadrature,
            "16" => Self::Trax,
            "17" => Self::Tantrix,
            "18" => Self::Amazons,
            "19" => Self::Octi,
            "20" => Self::Gess,
            "21" => Self::Twixt,
            "22" => Self::Zertz,
            "23" => Self::Plateau,
            "24" => Self::Yinsh,
            "25" => Self::Punct,
            "26" => Self::Gobblet,
            "27" => Self::Hive,
            "28" => Self::Exxit,
            "29" => Self::Hnefatal,
            "30" => Self::Kuba,
            "31" => Self::Tripples,
            "32" => Self::Chase,
            "33" => Self::TumblingDown,
            "34" => Self::Sahara,
            "35" => Self::Byte,
            "36" => Self::Focus,
            "37" => Self::Dvonn,
            "38" => Self::Tamsk,
            "39" => Self::Gipf,
            "40" => Self::Kropki,
            _ => Self::Other(s.to_owned()),
        }
    }
}

impl From<&GameType> for String {
    fn from(g: &GameType) -> String {
        match g {
            GameType::Go => "1".to_owned(),
            GameType::Othello => "2".to_owned(),
            GameType::Chess => "3".to_owned(),
            GameType::GomokuRenju => "4".to_owned(),
            GameType::NineMensMorris => "5".to_owned(),
            GameType::Backgammon => "6".to_owned(),
            GameType::ChineseChess => "7".to_owned(),
            GameType::Shogi => "8".to_owned(),
            GameType::LinesOfAction => "9".to_owned(),
            GameType::Ataxx => "10".to_owned(),
            GameType::Hex => "11".to_owned(),
            GameType::Jungle => "12".to_owned(),
            GameType::Neutron => "13".to_owned(),
            GameType::PhilosophersFootball => "14".to_owned(),
            GameType::Quadrature => "15".to_owned(),
            GameType::Trax => "16".to_owned(),
            GameType::Tantrix => "17".to_owned(),
            GameType::Amazons => "18".to_owned(),
            GameType::Octi => "19".to_owned(),
            GameType::Gess => "20".to_owned(),
            GameType::Twixt => "21".to_owned(),
            GameType::Zertz => "22".to_owned(),
            GameType::Plateau => "23".to_owned(),
            GameType::Yinsh => "24".to_owned(),
            GameType::Punct => "25".to_owned(),
            GameType::Gobblet => "26".to_owned(),
            GameType::Hive => "27".to_owned(),
            GameType::Exxit => "28".to_owned(),
            GameType::Hnefatal => "29".to_owned(),
            GameType::Kuba => "30".to_owned(),
            GameType::Tripples => "31".to_owned(),
            GameType::Chase => "32".to_owned(),
            GameType::TumblingDown => "33".to_owned(),
            GameType::Sahara => "34".to_owned(),
            GameType::Byte => "35".to_owned(),
            GameType::Focus => "36".to_owned(),
            GameType::Dvonn => "37".to_owned(),
            GameType::Tamsk => "38".to_owned(),
            GameType::Gipf => "39".to_owned(),
            GameType::Kropki => "40".to_owned(),
            GameType::Other(v) => v.clone(),
        }
    }
}

impl ToString for GameType {
    fn to_string(&self) -> String {
        String::from(self)
    }
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Size {
    pub width: i32,
    pub height: i32,
}

impl TryFrom<&str> for Size {
    type Error = ParseSizeError;
    fn try_from(s: &str) -> Result<Self, Self::Error> {
        let parts = s
            .split(':')
            .map(|v| v.parse::<i32>())
            .collect::<Result<Vec<i32>, ParseIntError>>()?;
        match parts[..] {
            [width, height, ..] => Ok(Size { width, height }),
            [dim] => Ok(Size {
                width: dim,
                height: dim,
            }),
            [] => Err(ParseSizeError::InsufficientArguments),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Position(String);

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PositionList(pub Vec<String>);

impl PositionList {
    pub fn compressed_list(&self) -> String {
        self.0.to_vec().join(":")
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Tree {
    pub root: Node,
}

impl ToString for Tree {
    fn to_string(&self) -> String {
        format!("({})", self.root.to_string())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Node {
    pub properties: Vec<Property>,
    pub next: Vec<Node>,
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum SetupInstr {
    Piece((Color, String)),
    Clear(String),
}

impl Node {
    pub fn mv(&self) -> Option<(Color, Move)> {
        self.find_by(|prop| match prop {
            Property::Move(val) => Some(val.clone()),
            _ => None,
        })
    }

    pub fn setup(&self) -> Option<Vec<SetupInstr>> {
        let mut setup = Vec::new();
        for prop in self.properties.iter() {
            match prop {
                Property::SetupBlackStones(positions) => {
                    setup.append(
                        &mut positions
                            .0
                            .iter()
                            .map(|pos| SetupInstr::Piece((Color::Black, pos.clone())))
                            .collect::<Vec<SetupInstr>>(),
                    );
                }
                Property::SetupWhiteStones(positions) => {
                    setup.append(
                        &mut positions
                            .0
                            .iter()
                            .map(|pos| SetupInstr::Piece((Color::White, pos.clone())))
                            .collect::<Vec<SetupInstr>>(),
                    );
                }
                Property::ClearStones(positions) => {
                    setup.append(
                        &mut positions
                            .0
                            .iter()
                            .map(|pos| SetupInstr::Clear(pos.clone()))
                            .collect::<Vec<SetupInstr>>(),
                    );
                }
                _ => return None,
            }
        }
        if !setup.is_empty() {
            Some(setup)
        } else {
            None
        }
    }

    fn find_by<F, R>(&self, f: F) -> Option<R>
    where
        F: FnMut(&Property) -> Option<R>,
    {
        self.properties.iter().filter_map(f).next()
    }
}

impl ToString for Node {
    fn to_string(&self) -> String {
        let props = self
            .properties
            .iter()
            .map(|prop| prop.to_string())
            .collect::<String>();

        let next = if self.next.len() == 1 {
            self.next
                .iter()
                .map(|node| node.to_string())
                .collect::<Vec<String>>()
                .join("")
        } else {
            self.next
                .iter()
                .map(|node| format!("({})", node.to_string()))
                .collect::<Vec<String>>()
                .join("")
        };
        format!(";{}{}", props, next)
    }
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum Move {
    Move(String),
    Pass,
}

impl Move {
    pub fn coordinate(&self) -> Option<(u8, u8)> {
        match self {
            Move::Pass => None,
            Move::Move(s) => {
                if s.len() == 2 {
                    let mut parts = s.chars();
                    let column_char = parts.next().unwrap();
                    let column = column_char as u8 - b'a';
                    let row_char = parts.next().unwrap();
                    let row = row_char as u8 - b'a';
                    Some((row, column))
                } else {
                    unimplemented!("moves must contain exactly two characters");
                }
            }
        }
    }
}

//  KO
//  MN
//  N
//  AR
//  CR
//  DD
//  LB
//  LN
//  MA
//  SL
//  SQ
//  TR
//  OB
//  OW
//  FG
//  PM
//  VW
#[derive(Clone, Debug, PartialEq)]
pub enum Property {
    //  B, W
    Move((Color, Move)),

    // C
    Comment(String),

    //  BM, DO, IT, TE
    Annotation(Annotation),

    //  AP
    Application(String),

    //  CA
    Charset(String),

    //  FF
    FileFormat(i32),

    //  GM
    GameType(GameType),

    //  ST
    VariationDisplay,

    //  SZ
    BoardSize(Size),

    //  AB
    SetupBlackStones(PositionList),

    //  AE
    ClearStones(PositionList),

    //  AW
    SetupWhiteStones(PositionList),

    //  PL
    NextPlayer(Color),

    //  DM, GB, GW, UC
    Evaluation(Evaluation),

    //  HO
    Hotspot,

    //  V
    Value(f32),

    //  AN
    Annotator(String),

    //  BR
    BlackRank(String),

    //  BT
    BlackTeam(String),

    //  CP
    Copyright(String),

    //  DT
    EventDates(Vec<Date>),

    //  EV
    EventName(String),

    //  GN
    GameName(String),

    //  GC
    ExtraGameInformation(String),

    //  ON
    GameOpening(String),

    //  OT
    Overtime(String),

    //  PB
    BlackPlayer(String),

    //  PC
    GameLocation(String),

    //  PW
    WhitePlayer(String),

    //  RE
    Result(GameResult),

    //  RO
    Round(String),

    //  RU
    Ruleset(String),

    //  SO
    Source(String),

    //  TM
    TimeLimit(Duration),

    //  US
    User(String),

    //  WR
    WhiteRank(String),

    //  WT
    WhiteTeam(String),

    //  BL, WL
    TimeLeft((Color, Duration)),

    // TW, TB
    Territory(Color, Vec<Position>),

    Unknown(UnknownProperty),
}

#[derive(Clone, Debug, PartialEq)]
pub struct UnknownProperty {
    pub ident: String,
    pub value: String,
}

impl ToString for Property {
    fn to_string(&self) -> String {
        match self {
            Property::Move((color, Move::Move(mv))) => {
                format!("{}[{}]", color.abbreviation(), mv)
            }
            Property::Move((color, Move::Pass)) => {
                format!("{}[]", color.abbreviation())
            }
            Property::TimeLeft((color, time)) => {
                format!("{}[{}]", color.abbreviation(), time.as_secs())
            }
            Property::Comment(value) => format!("C[{}]", value),
            Property::Annotation(Annotation::BadMove) => "BM[]".to_owned(),
            Property::Annotation(Annotation::DoubtfulMove) => "DO[]".to_owned(),
            Property::Annotation(Annotation::InterestingMove) => "IT[]".to_owned(),
            Property::Annotation(Annotation::Tesuji) => "TE[]".to_owned(),
            Property::Application(app) => format!("AP[{}]", app),
            Property::Charset(set) => format!("CA[{}]", set),
            Property::FileFormat(ff) => format!("FF[{}]", ff),
            Property::GameType(gt) => format!("GM[{}]", gt.to_string()),
            Property::VariationDisplay => unimplemented!(),
            Property::BoardSize(Size { width, height }) => {
                if width == height {
                    format!("SZ[{}]", width)
                } else {
                    format!("SZ[{}:{}]", width, height)
                }
            }
            Property::SetupBlackStones(positions) => {
                format!("AB[{}]", positions.compressed_list(),)
            }
            Property::ClearStones(positions) => {
                format!("AE[{}]", positions.compressed_list(),)
            }
            Property::SetupWhiteStones(positions) => {
                format!("AW[{}]", positions.compressed_list(),)
            }
            Property::NextPlayer(color) => format!("PL[{}]", color.abbreviation()),
            Property::Evaluation(Evaluation::Even) => "DM[]".to_owned(),
            Property::Evaluation(Evaluation::GoodForBlack) => "GB[]".to_owned(),
            Property::Evaluation(Evaluation::GoodForWhite) => "GW[]".to_owned(),
            Property::Evaluation(Evaluation::Unclear) => "UC[]".to_owned(),
            Property::Hotspot => "HO[]".to_owned(),
            Property::Value(value) => format!("V[{}]", value),
            Property::Annotator(value) => format!("AN[{}]", value),
            Property::BlackRank(value) => format!("BR[{}]", value),
            Property::BlackTeam(value) => format!("BT[{}]", value),
            Property::Copyright(value) => format!("CP[{}]", value),
            Property::EventDates(_) => unimplemented!(),
            Property::EventName(value) => format!("EV[{}]", value),
            Property::GameName(value) => format!("GN[{}]", value),
            Property::ExtraGameInformation(value) => format!("GC[{}]", value),
            Property::GameOpening(value) => format!("ON[{}]", value),
            Property::Overtime(value) => format!("OT[{}]", value),
            Property::BlackPlayer(value) => format!("PB[{}]", value),
            Property::GameLocation(value) => format!("PC[{}]", value),
            Property::WhitePlayer(value) => format!("PW[{}]", value),
            Property::Result(_) => unimplemented!(),
            Property::Round(value) => format!("RO[{}]", value),
            Property::Ruleset(value) => format!("RU[{}]", value),
            Property::Source(value) => format!("SO[{}]", value),
            Property::TimeLimit(value) => format!("TM[{}]", value.as_secs()),
            Property::User(value) => format!("US[{}]", value),
            Property::WhiteRank(value) => format!("WR[{}]", value),
            Property::WhiteTeam(value) => format!("WT[{}]", value),
            Property::Territory(Color::White, positions) => {
                positions
                    .iter()
                    .fold("TW".to_owned(), |mut output, Position(p)| {
                        let _ = write!(output, "{}", p);
                        output
                    })
            }
            Property::Territory(Color::Black, positions) => {
                positions
                    .iter()
                    .fold("TB".to_owned(), |mut output, Position(p)| {
                        let _ = write!(output, "{}", p);
                        output
                    })
            }
            Property::Unknown(UnknownProperty { ident, value }) => {
                format!("{}[{}]", ident, value)
            }
        }
    }
}

pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
    input: &'a str,
) -> IResult<&'a str, Vec<Tree>, E> {
    let (input, roots) = separated_list1(multispace1, parse_tree)(input)?;
    let trees = roots
        .into_iter()
        .map(|root| Tree { root })
        .collect::<Vec<Tree>>();

    Ok((input, trees))
}

// note: must preserve unknown properties
// note: must fix or preserve illegally formatted game-info properties
// note: must correct or delete illegally foramtted properties, but display a warning
fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
    let (input, _) = multispace0(input)?;
    let (input, _) = tag("(")(input)?;
    let (input, node) = parse_node(input)?;
    let (input, _) = multispace0(input)?;
    let (input, _) = tag(")")(input)?;

    Ok((input, node))
}

fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
    let (input, _) = multispace0(input)?;
    let (input, _) = opt(tag(";"))(input)?;
    let (input, properties) = many1(parse_property)(input)?;

    let (input, next) = opt(parse_node)(input)?;
    let (input, mut next_seq) = many0(parse_tree)(input)?;

    let mut next = next.map(|n| vec![n]).unwrap_or(vec![]);
    next.append(&mut next_seq);

    Ok((input, Node { properties, next }))
}

fn parse_property<'a, E: nom::error::ParseError<&'a str>>(
    input: &'a str,
) -> IResult<&'a str, Property, E> {
    let (input, _) = multispace0(input)?;
    let (input, ident) = alpha1(input)?;

    let (input, prop) = match ident {
        "W" => parse_propval(parse_move(Color::White))(input)?,
        "B" => parse_propval(parse_move(Color::Black))(input)?,
        "C" => parse_propval(parse_comment())(input)?,
        "WL" => parse_propval(parse_time_left(Color::White))(input)?,
        "BL" => parse_propval(parse_time_left(Color::Black))(input)?,
        "BM" => discard_propval()
            .map(|_| Property::Annotation(Annotation::BadMove))
            .parse(input)?,
        "DO" => discard_propval()
            .map(|_| Property::Annotation(Annotation::DoubtfulMove))
            .parse(input)?,
        "IT" => discard_propval()
            .map(|_| Property::Annotation(Annotation::InterestingMove))
            .parse(input)?,
        "TE" => discard_propval()
            .map(|_| Property::Annotation(Annotation::Tesuji))
            .parse(input)?,
        "AP" => parse_propval(parse_simple_text().map(Property::Application))(input)?,
        "CA" => parse_propval(parse_simple_text().map(Property::Charset))(input)?,
        "FF" => parse_propval(parse_number().map(Property::FileFormat))(input)?,
        "GM" => parse_propval(parse_gametype().map(Property::GameType))(input)?,
        "ST" => discard_propval()
            .map(|_| Property::VariationDisplay)
            .parse(input)?,
        "SZ" => parse_propval(parse_size().map(Property::BoardSize))(input)?,
        "DM" => discard_propval()
            .map(|_| Property::Evaluation(Evaluation::Even))
            .parse(input)?,
        "GB" => discard_propval()
            .map(|_| Property::Evaluation(Evaluation::GoodForBlack))
            .parse(input)?,
        "GW" => discard_propval()
            .map(|_| Property::Evaluation(Evaluation::GoodForWhite))
            .parse(input)?,
        "UC" => discard_propval()
            .map(|_| Property::Evaluation(Evaluation::Unclear))
            .parse(input)?,
        "V" => unimplemented!(),
        "AN" => parse_propval(parse_simple_text().map(Property::Annotator))(input)?,
        "BR" => parse_propval(parse_simple_text().map(Property::BlackRank))(input)?,
        "BT" => parse_propval(parse_simple_text().map(Property::BlackTeam))(input)?,
        "CP" => parse_propval(parse_simple_text().map(Property::Copyright))(input)?,
        "DT" => parse_propval(parse_date_field().map(Property::EventDates))(input)?,
        "EV" => parse_propval(parse_simple_text().map(Property::EventName))(input)?,
        "GN" => parse_propval(parse_simple_text().map(Property::GameName))(input)?,
        "GC" => parse_propval(parse_simple_text().map(Property::ExtraGameInformation))(input)?,
        "ON" => parse_propval(parse_simple_text().map(Property::GameOpening))(input)?,
        "OT" => parse_propval(parse_simple_text().map(Property::Overtime))(input)?,
        "PB" => parse_propval(parse_simple_text().map(Property::BlackPlayer))(input)?,
        "PC" => parse_propval(parse_simple_text().map(Property::GameLocation))(input)?,
        "PW" => parse_propval(parse_simple_text().map(Property::WhitePlayer))(input)?,
        "RE" => parse_propval(parse_game_result().map(Property::Result))(input)?,
        "RO" => parse_propval(parse_simple_text().map(Property::Round))(input)?,
        "RU" => parse_propval(parse_simple_text().map(Property::Ruleset))(input)?,
        "SO" => parse_propval(parse_simple_text().map(Property::Source))(input)?,
        "TM" => parse_propval(
            parse_real().map(|seconds| Property::TimeLimit(Duration::from_secs(seconds as u64))),
        )(input)?,
        "US" => parse_propval(parse_simple_text().map(Property::User))(input)?,
        "WR" => parse_propval(parse_simple_text().map(Property::WhiteRank))(input)?,
        "WT" => parse_propval(parse_simple_text().map(Property::WhiteTeam))(input)?,
        "TW" => parse_territory()
            .map(|p| Property::Territory(Color::White, p))
            .parse(input)?,
        "TB" => parse_territory()
            .map(|p| Property::Territory(Color::Black, p))
            .parse(input)?,
        _ => parse_propval(parse_simple_text().map(|value| {
            Property::Unknown(UnknownProperty {
                ident: ident.to_owned(),
                value,
            })
        }))(input)?,
    };

    Ok((input, prop))
}

fn parse_comment<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Property, E> {
    parse_text().map(Property::Comment)
}

fn parse_move<'a, E: nom::error::ParseError<&'a str>>(
    color: Color,
) -> impl Parser<&'a str, Property, E> {
    {
        let color = color.clone();
        move |input: &'a str| {
            take_until("]")
                .map(|text: &'a str| {
                    if text.is_empty() {
                        Property::Move((color.clone(), Move::Pass))
                    } else {
                        Property::Move((color.clone(), Move::Move(text.to_owned())))
                    }
                })
                .parse(input)
        }
    }
}

fn parse_gametype<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameType, E> {
    |input: &'a str| take_until1("]").map(GameType::from).parse(input)
}

fn parse_time_left<'a, E: ParseError<&'a str>>(
    color: Color,
) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> {
    {
        let color = color.clone();
        move |input: &'a str| {
            let (input, value) = parse_real().parse(input)?;
            Ok((
                input,
                Property::TimeLeft((color.clone(), Duration::from_secs(value as u64))),
            ))
        }
    }
}

fn parse_size<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Size, E> {
    |input: &'a str| {
        let (input, dimensions) = separated_list1(tag(":"), digit1)(input)?;
        let (width, height) = match dimensions.as_slice() {
            [width] => (width.parse::<i32>().unwrap(), width.parse::<i32>().unwrap()),
            [width, height] => (
                width.parse::<i32>().unwrap(),
                height.parse::<i32>().unwrap(),
            ),
            _ => (19, 19),
        };
        Ok((input, Size { width, height }))
    }
}

fn parse_game_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E>
{
    alt((parse_draw_result(), parse_void_result(), parse_win_result()))
}

fn parse_territory<'a, E: nom::error::ParseError<&'a str>>(
) -> impl Parser<&'a str, Vec<Position>, E> {
    many1(|input| {
        let (input, _) = tag("[")(input)?;
        let (input, position) = parse_simple_text().map(Position).parse(input)?;
        let (input, _) = tag("]")(input)?;
        Ok((input, position))
    })
}

fn parse_propval<'a, E: nom::error::ParseError<&'a str>>(
    mut parser: impl Parser<&'a str, Property, E>,
) -> impl FnMut(&'a str) -> IResult<&'a str, Property, E> {
    move |input| {
        let (input, _) = multispace0(input)?;
        let (input, _) = tag("[")(input)?;
        let (input, value) = parser.parse(input)?;
        let (input, _) = tag("]")(input)?;

        Ok((input, value))
    }
}

fn discard_propval<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, (), E> {
    |input| {
        let (input, _) = multispace0(input)?;
        let (input, _) = tag("[")(input)?;
        let (input, _) = parse_text().parse(input)?;
        let (input, _) = tag("]")(input)?;
        Ok((input, ()))
    }
}

fn parse_number<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, i32, E> {
    |input| {
        let (input, sign) = opt(alt((tag("+"), tag("-"))))(input)?;
        let (input, value) = digit1(input)?;
        let mult = if sign == Some("-") { -1 } else { 1 };
        Ok((input, value.parse::<i32>().unwrap() * mult))
    }
}

fn parse_real<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, f32, E> {
    |input| {
        let (input, sign) = opt(alt((tag("+"), tag("-"))))(input)?;
        let (input, whole_value) = digit1(input)?;
        let (input, fractional_value) = opt(|input| {
            let (input, _) = tag(".")(input)?;
            let (input, fractional_value) = digit1(input)?;
            Ok((input, format!(".{}", fractional_value)))
        })(input)?;
        let value = format!(
            "{}{}{}",
            sign.unwrap_or("+"),
            whole_value,
            fractional_value.unwrap_or("".to_owned())
        )
        .parse::<f32>()
        .unwrap();
        Ok((input, value))
    }
}

fn parse_simple_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> {
    |input| {
        let (input, value) = opt(escaped_transform(
            none_of("\\]"),
            '\\',
            alt((
                value("]", tag("]")),
                value("\\", tag("\\")),
                value("", tag("\n")),
            )),
        ))(input)?;
        Ok((input, value.unwrap_or("".to_owned())))
    }
}

fn parse_text<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, String, E> {
    |input| {
        let (input, value) = opt(escaped_transform(
            none_of("\\]"),
            '\\',
            alt((
                value("]", tag("]")),
                value("\\", tag("\\")),
                value("", tag("\n")),
            )),
        ))(input)?;
        Ok((input, value.unwrap_or("".to_owned())))
    }
}

enum DateSegment {
    One(i32),
    Two(i32, i32),
    Three(i32, i32, i32),
}

fn parse_date_field<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, Vec<Date>, E> {
    |input| {
        let (input, first_date) = parse_date().parse(input)?;
        let (input, mut more_dates) = many0(parse_date_segment())(input)?;

        let mut date_segments = vec![first_date];
        date_segments.append(&mut more_dates);

        let mut dates = vec![];
        let mut most_recent = None;

        for date_segment in date_segments {
            let new_date = match date_segment {
                DateSegment::One(v) => match most_recent {
                    Some(Date::Year(_)) => Date::Year(v),
                    Some(Date::YearMonth(y, _)) => Date::YearMonth(y, v as u32),
                    Some(Date::Date(d)) => Date::Date(d.clone().with_day(v as u32).unwrap()),
                    None => Date::Year(v),
                },
                DateSegment::Two(y, m) => Date::YearMonth(y, m as u32),
                DateSegment::Three(y, m, d) => {
                    Date::Date(chrono::NaiveDate::from_ymd_opt(y, m as u32, d as u32).unwrap())
                }
            };
            dates.push(new_date.clone());
            most_recent = Some(new_date);
        }

        Ok((input, dates))
    }
}

fn parse_date_segment<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> {
    |input| {
        let (input, _) = tag(",")(input)?;
        let (input, element) = parse_date().parse(input)?;

        Ok((input, element))
    }
}

fn parse_date<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> {
    alt((parse_three(), parse_two(), parse_one()))
}

fn parse_one<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> {
    parse_number().map(DateSegment::One)
}

fn parse_two<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> {
    |input| {
        let (input, year) = parse_number().parse(input)?;
        let (input, _) = tag("-")(input)?;
        let (input, month) = parse_number().parse(input)?;
        Ok((input, DateSegment::Two(year, month)))
    }
}

fn parse_three<'a, E: ParseError<&'a str>>() -> impl Parser<&'a str, DateSegment, E> {
    |input| {
        let (input, year) = parse_number().parse(input)?;
        let (input, _) = tag("-")(input)?;
        let (input, month) = parse_number().parse(input)?;
        let (input, _) = tag("-")(input)?;
        let (input, day) = parse_number().parse(input)?;
        Ok((input, DateSegment::Three(year, month, day)))
    }
}

fn parse_draw_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E>
{
    tag("0").map(|_| GameResult::Draw)
}

fn parse_void_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E>
{
    tag("Void").map(|_| GameResult::Void)
}

fn parse_win_result<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, GameResult, E>
{
    |input: &'a str| {
        let (input, color) = alt((
            tag("B").map(|_| Color::Black),
            tag("b").map(|_| Color::Black),
            tag("W").map(|_| Color::White),
            tag("w").map(|_| Color::White),
        ))(input)?;
        let (input, _) = tag("+")(input)?;
        let (input, score) = parse_win_score().parse(input)?;

        Ok((
            input,
            match color {
                Color::Black => GameResult::Black(score),
                Color::White => GameResult::White(score),
            },
        ))
    }
}

enum WinType {
    St(String),
    Num(f32),
}

fn parse_win_score<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a str, Win, E> {
    |input: &'a str| {
        let (input, win) = alt((
            parse_real().map(WinType::Num),
            parse_simple_text().map(WinType::St),
        ))(input)?;
        let w = match win {
            WinType::St(s) => match s.to_ascii_lowercase().as_str() {
                "r" | "resign" => Win::Resignation,
                "t" | "time" => Win::Time,
                "f" | "forfeit" => Win::Forfeit,
                _ => Win::Unknown,
            },
            WinType::Num(n) => Win::Score(n),
        };
        Ok((input, w))
    }
}

#[cfg(test)]
mod test {
    use super::*;

    const EXAMPLE: &str = "(;FF[4]C[root](;C[a];C[b](;C[c])
(;C[d];C[e]))
(;C[f](;C[g];C[h];C[i])
(;C[j])))";

    #[test]
    fn it_can_parse_properties() {
        let (_, prop) = parse_property::<nom::error::VerboseError<&str>>("C[a]").unwrap();
        assert_eq!(prop, Property::Comment("a".to_owned()));
    }

    #[test]
    fn it_can_parse_a_standalone_node() {
        let (_, node) = parse_node::<nom::error::VerboseError<&str>>(";B[ab]").unwrap();

        assert_eq!(
            node,
            Node {
                properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))],
                next: vec![]
            }
        );

        let (_, node) =
            parse_node::<nom::error::VerboseError<&str>>(";B[ab];W[dp];B[pq]C[some comments]")
                .unwrap();

        assert_eq!(
            node,
            Node {
                properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))],
                next: vec![Node {
                    properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))],
                    next: vec![Node {
                        properties: vec![
                            Property::Move((Color::Black, Move::Move("pq".to_owned()))),
                            Property::Comment("some comments".to_owned())
                        ],
                        next: vec![],
                    }]
                }]
            }
        );
    }

    #[test]
    fn it_can_parse_a_simple_sequence() {
        let (_, sequence) =
            parse_tree::<nom::error::VerboseError<&str>>("(;B[ab];W[dp];B[pq]C[some comments])")
                .unwrap();

        assert_eq!(
            sequence,
            Node {
                properties: vec![Property::Move((Color::Black, Move::Move("ab".to_owned())))],
                next: vec![Node {
                    properties: vec![Property::Move((Color::White, Move::Move("dp".to_owned())))],
                    next: vec![Node {
                        properties: vec![
                            Property::Move((Color::Black, Move::Move("pq".to_owned()))),
                            Property::Comment("some comments".to_owned())
                        ],
                        next: vec![],
                    }]
                }],
            },
        );
    }

    #[test]
    fn it_can_parse_a_branching_sequence() {
        let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))";
        let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap();

        let expected = Node {
            properties: vec![Property::Comment("a".to_owned())],
            next: vec![Node {
                properties: vec![Property::Comment("b".to_owned())],
                next: vec![
                    Node {
                        properties: vec![Property::Comment("c".to_owned())],
                        next: vec![],
                    },
                    Node {
                        properties: vec![Property::Comment("d".to_owned())],
                        next: vec![Node {
                            properties: vec![Property::Comment("e".to_owned())],
                            next: vec![],
                        }],
                    },
                ],
            }],
        };

        assert_eq!(tree, expected);
    }

    #[test]
    fn it_can_parse_example_1() {
        let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();

        let j = Node {
            properties: vec![Property::Comment("j".to_owned())],
            next: vec![],
        };
        let i = Node {
            properties: vec![Property::Comment("i".to_owned())],
            next: vec![],
        };
        let h = Node {
            properties: vec![Property::Comment("h".to_owned())],
            next: vec![i],
        };
        let g = Node {
            properties: vec![Property::Comment("g".to_owned())],
            next: vec![h],
        };
        let f = Node {
            properties: vec![Property::Comment("f".to_owned())],
            next: vec![g, j],
        };
        let e = Node {
            properties: vec![Property::Comment("e".to_owned())],
            next: vec![],
        };
        let d = Node {
            properties: vec![Property::Comment("d".to_owned())],
            next: vec![e],
        };
        let c = Node {
            properties: vec![Property::Comment("c".to_owned())],
            next: vec![],
        };
        let b = Node {
            properties: vec![Property::Comment("b".to_owned())],
            next: vec![c, d],
        };
        let a = Node {
            properties: vec![Property::Comment("a".to_owned())],
            next: vec![b],
        };
        let expected = Node {
            properties: vec![
                Property::FileFormat(4),
                Property::Comment("root".to_owned()),
            ],
            next: vec![a, f],
        };

        assert_eq!(tree, expected);
    }

    #[test]
    fn it_can_regenerate_the_tree() {
        let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
        let tree1 = Tree { root: tree1 };
        assert_eq!(
            tree1.to_string(),
            "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]))(;C[f](;C[g];C[h];C[i])(;C[j])))"
        );
        let (_, tree2) = parse_tree::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
        assert_eq!(tree1, Tree { root: tree2 });
    }

    #[test]
    fn it_parses_propvals() {
        let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
            .parse("[]")
            .unwrap();
        assert_eq!(propval, Property::Comment("".to_owned()));

        let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
            .parse("[normal propval]")
            .unwrap();
        assert_eq!(propval, Property::Comment("normal propval".to_owned()));

        let (_, propval) = parse_propval::<nom::error::VerboseError<&str>>(parse_comment())
            .parse(r"[need an [escape\] in the propval]")
            .unwrap();
        assert_eq!(
            propval,
            Property::Comment("need an [escape] in the propval".to_owned())
        );
    }

    #[test]
    fn it_parses_propvals_with_hard_linebreaks() {
        let (_, propval) = parse_text::<nom::error::VerboseError<&str>>()
            .parse(
                "There are hard linebreaks & soft linebreaks.
Soft linebreaks...",
            )
            .unwrap();
        assert_eq!(
            propval,
            "There are hard linebreaks & soft linebreaks.
Soft linebreaks..."
        );
    }

    #[test]
    fn it_parses_propvals_with_escaped_closing_brackets() {
        let (_, propval) = parse_text::<nom::error::VerboseError<&str>>()
            .parse(r"escaped closing \] bracket")
            .unwrap();
        assert_eq!(propval, r"escaped closing ] bracket".to_owned());
    }

    #[test]
    fn it_parses_propvals_with_soft_linebreaks() {
        let (_, propval) = parse_text::<nom::error::VerboseError<&str>>()
            .parse(
                r"Soft linebreaks are linebreaks preceeded by '\\' like this one >o\
k<. Hard line breaks are all other linebreaks.",
            )
            .unwrap();
        assert_eq!(
            propval,
            "Soft linebreaks are linebreaks preceeded by '\\' like this one >ok<. Hard line breaks are all other linebreaks."
        );
    }

    #[test]
    fn it_parses_sgf_with_newline_in_sequence() {
        let data = String::from(
            "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]
))(;C[f](;C[g];C[h];C[i])(;C[j])))",
        );
        parse_tree::<nom::error::VerboseError<&str>>(&data).unwrap();
    }

    #[test]
    fn it_parses_sgf_with_newline_between_two_sequence_closings() {
        let data = String::from(
            "(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e])
)(;C[f](;C[g];C[h];C[i])(;C[j])))",
        );
        parse_tree::<nom::error::VerboseError<&str>>(&data).unwrap();
    }

    #[test]
    fn it_can_convert_moves_to_coordinates() {
        assert_eq!(Move::Pass.coordinate(), None);
        assert_eq!(Move::Move("dd".to_owned()).coordinate(), Some((3, 3)));
        assert_eq!(Move::Move("jj".to_owned()).coordinate(), Some((9, 9)));
        assert_eq!(Move::Move("pp".to_owned()).coordinate(), Some((15, 15)));
    }
}

#[cfg(test)]
mod date_test {
    use super::*;
    use chrono::NaiveDate;
    use cool_asserts::assert_matches;

    #[test]
    fn it_parses_a_year() {
        assert_matches!(parse_date_field::<nom::error::VerboseError<&str>>().parse("1996"), Ok((_, date)) => {
            assert_eq!(date, vec![Date::Year(1996)]);
        });
    }

    #[test]
    fn it_parses_a_month() {
        assert_matches!(
            parse_date_field::<nom::error::VerboseError<&str>>().parse("1996-12"),
            Ok((_, date)) => assert_eq!(date, vec![Date::YearMonth(1996, 12)])
        );
    }

    #[test]
    fn it_parses_a_date() {
        assert_matches!(
            parse_date_field::<nom::error::VerboseError<&str>>().parse("1996-12-27"),
            Ok((_, date)) => assert_eq!(date, vec![Date::Date(
                NaiveDate::from_ymd_opt(1996, 12, 27).unwrap()
            )])
        );
    }

    #[test]
    fn it_parses_date_continuation() {
        assert_matches!(
            parse_date_field::<nom::error::VerboseError<&str>>().parse("1996-12-27,28"),
            Ok((_, date)) => assert_eq!(date, vec![
                Date::Date(NaiveDate::from_ymd_opt(1996, 12, 27).unwrap()),
                Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap())
            ])
        );
    }

    #[test]
    fn it_parses_date_crossing_year_boundary() {
        assert_matches!(
            parse_date_field::<nom::error::VerboseError<&str>>().parse("1996-12-27,28,1997-01-03,04"),
            Ok((_, date)) => assert_eq!(date, vec![
                Date::Date(NaiveDate::from_ymd_opt(1996, 12, 27).unwrap()),
                Date::Date(NaiveDate::from_ymd_opt(1996, 12, 28).unwrap()),
                Date::Date(NaiveDate::from_ymd_opt(1997, 1, 3).unwrap()),
                Date::Date(NaiveDate::from_ymd_opt(1997, 1, 4).unwrap()),
            ])
        );
    }
}

#[cfg(test)]
mod property_tests {
    use super::*;

    #[test]
    fn it_can_parse_time_left() {
        let (_, val) =
            parse_time_left::<nom::error::VerboseError<&str>>(Color::Black)("170]").unwrap();
        assert_eq!(
            val,
            Property::TimeLeft((Color::Black, Duration::from_secs(170)))
        );

        let (_, val) =
            parse_time_left::<nom::error::VerboseError<&str>>(Color::Black)("170.6]").unwrap();
        assert_eq!(
            val,
            Property::TimeLeft((Color::Black, Duration::from_secs(170)))
        );

        let (_, prop) = parse_property::<nom::error::VerboseError<&str>>("BL[170]").unwrap();
        assert_eq!(
            prop,
            Property::TimeLeft((Color::Black, Duration::from_secs(170)))
        );

        let (_, prop) = parse_property::<nom::error::VerboseError<&str>>("BL[170.5]").unwrap();
        assert_eq!(
            prop,
            Property::TimeLeft((Color::Black, Duration::from_secs(170)))
        );
    }
}
