// This file is part of hnefatafl-copenhagen.
//
// hnefatafl-copenhagen is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// hnefatafl-copenhagen is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::{
    fmt,
    hash::{Hash, Hasher},
    str::FromStr,
};

use anyhow::Context;
use serde::{Deserialize, Serialize};

use crate::{
    board::BoardSize,
    role::Role,
    time::{TimeLeft, TimeSettings},
};

pub const BOARD_LETTERS: &str = "ABCDEFGHIJKLM";

pub const EXIT_SQUARES_11X11: [Vertex; 4] = [
    Vertex {
        size: BoardSize::_11,
        x: 0,
        y: 0,
    },
    Vertex {
        size: BoardSize::_11,
        x: 10,
        y: 0,
    },
    Vertex {
        size: BoardSize::_11,
        x: 0,
        y: 10,
    },
    Vertex {
        size: BoardSize::_11,
        x: 10,
        y: 10,
    },
];

const THRONE_11X11: Vertex = Vertex {
    size: BoardSize::_11,
    x: 5,
    y: 5,
};

const RESTRICTED_SQUARES_11X11: [Vertex; 5] = [
    Vertex {
        size: BoardSize::_11,
        x: 0,
        y: 0,
    },
    Vertex {
        size: BoardSize::_11,
        x: 10,
        y: 0,
    },
    Vertex {
        size: BoardSize::_11,
        x: 0,
        y: 10,
    },
    Vertex {
        size: BoardSize::_11,
        x: 10,
        y: 10,
    },
    THRONE_11X11,
];

pub const EXIT_SQUARES_13X13: [Vertex; 4] = [
    Vertex {
        size: BoardSize::_13,
        x: 0,
        y: 0,
    },
    Vertex {
        size: BoardSize::_13,
        x: 12,
        y: 0,
    },
    Vertex {
        size: BoardSize::_13,
        x: 0,
        y: 12,
    },
    Vertex {
        size: BoardSize::_13,
        x: 12,
        y: 12,
    },
];

const THRONE_13X13: Vertex = Vertex {
    size: BoardSize::_13,
    x: 6,
    y: 6,
};

const RESTRICTED_SQUARES_13X13: [Vertex; 5] = [
    Vertex {
        size: BoardSize::_13,
        x: 0,
        y: 0,
    },
    Vertex {
        size: BoardSize::_13,
        x: 12,
        y: 0,
    },
    Vertex {
        size: BoardSize::_13,
        x: 0,
        y: 12,
    },
    Vertex {
        size: BoardSize::_13,
        x: 12,
        y: 12,
    },
    THRONE_13X13,
];

#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialOrd, Serialize)]
pub struct PlayRecordTimed {
    pub play: Option<Plae>,
    pub attacker_time: TimeLeft,
    pub defender_time: TimeLeft,
}

impl Hash for PlayRecordTimed {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.play.hash(state);
    }
}

impl PartialEq for PlayRecordTimed {
    fn eq(&self, other: &Self) -> bool {
        self.play == other.play
    }
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Plae {
    Play(Play),
    AttackerResigns,
    DefenderResigns,
}

impl fmt::Display for Plae {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Play(play) => write!(f, "play {} {} {}", play.role, play.from, play.to),
            Self::AttackerResigns => write!(f, "play attacker resigns _"),
            Self::DefenderResigns => write!(f, "play defender resigns _"),
        }
    }
}

impl Plae {
    /// # Errors
    ///
    /// If you try to convert an illegal character or you don't get vertex-vertex.
    pub fn from_str_(play: &str, role: &Role) -> anyhow::Result<Self> {
        let Some((from, to)) = play.split_once('-') else {
            return Err(anyhow::Error::msg("expected: vertex-vertex"));
        };

        Ok(Self::Play(Play {
            role: *role,
            from: Vertex::from_str(from)?,
            to: Vertex::from_str(to)?,
        }))
    }
}

impl TryFrom<Vec<&str>> for Plae {
    type Error = anyhow::Error;

    fn try_from(args: Vec<&str>) -> Result<Self, Self::Error> {
        let error_str = "expected: 'play ROLE FROM TO' or 'play ROLE resign'";

        if args.len() < 3 {
            return Err(anyhow::Error::msg(error_str));
        }

        let role = Role::from_str(args[1])?;
        if args[2] == "resigns" {
            if role == Role::Defender {
                return Ok(Self::DefenderResigns);
            }

            return Ok(Self::AttackerResigns);
        }

        if args.len() < 4 {
            return Err(anyhow::Error::msg(error_str));
        }

        Ok(Self::Play(Play {
            role: Role::from_str(args[1])?,
            from: Vertex::from_str(args[2])?,
            to: Vertex::from_str(args[3])?,
        }))
    }
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Plays {
    PlayRecordsTimed(Vec<PlayRecordTimed>),
    PlayRecords(Vec<Option<Plae>>),
}

impl Plays {
    #[must_use]
    pub fn is_empty(&self) -> bool {
        match self {
            Plays::PlayRecordsTimed(plays) => plays.is_empty(),
            Plays::PlayRecords(plays) => plays.is_empty(),
        }
    }

    #[must_use]
    pub fn len(&self) -> usize {
        match self {
            Plays::PlayRecordsTimed(plays) => plays.len(),
            Plays::PlayRecords(plays) => plays.len(),
        }
    }

    #[must_use]
    pub fn new(time_settings: &TimeSettings) -> Self {
        match time_settings {
            TimeSettings::Timed(_) => Plays::PlayRecordsTimed(Vec::new()),
            TimeSettings::UnTimed => Plays::PlayRecords(Vec::new()),
        }
    }

    #[must_use]
    pub fn time_left(&self, role: Role, index: usize) -> String {
        match self {
            Plays::PlayRecordsTimed(plays) => match role {
                Role::Attacker => {
                    if let Some(play) = plays.get(index) {
                        play.attacker_time.to_string()
                    } else {
                        "-".to_string()
                    }
                }
                Role::Defender => {
                    if let Some(play) = plays.get(index) {
                        play.defender_time.to_string()
                    } else {
                        "-".to_string()
                    }
                }
                Role::Roleless => unreachable!(),
            },
            Plays::PlayRecords(_) => "-".to_string(),
        }
    }
}

impl Default for Plays {
    fn default() -> Self {
        Plays::PlayRecordsTimed(Vec::new())
    }
}

impl fmt::Display for Plays {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Plays::PlayRecordsTimed(plays) => {
                for play in plays {
                    if let Some(play) = &play.play {
                        write!(f, "{play}, ")?;
                    }
                }
            }
            Plays::PlayRecords(plays) => {
                for play in plays {
                    if let Some(play) = &play {
                        write!(f, "{play}, ")?;
                    }
                }
            }
        }

        Ok(())
    }
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Play {
    pub role: Role,
    pub from: Vertex,
    pub to: Vertex,
}

impl fmt::Display for Play {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?} from {} to {}", self.role, self.from, self.to)
    }
}

#[derive(Debug, Default)]
pub struct Captures(pub Vec<Vertex>);

impl fmt::Display for Captures {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for vertex in &self.0 {
            write!(f, "{vertex} ")?;
        }

        Ok(())
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Vertex {
    pub size: BoardSize,
    pub x: usize,
    pub y: usize,
}

impl fmt::Display for Vertex {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let letters = match self.size {
            BoardSize::_11 => &BOARD_LETTERS.to_lowercase(),
            BoardSize::_13 => BOARD_LETTERS,
        };

        let board_size: usize = self.size.into();

        write!(
            f,
            "{}{}",
            letters.chars().collect::<Vec<_>>()[self.x],
            board_size - self.y
        )
    }
}

impl FromStr for Vertex {
    type Err = anyhow::Error;

    fn from_str(vertex: &str) -> anyhow::Result<Self> {
        let mut chars = vertex.chars();

        if let Some(mut ch) = chars.next() {
            let size = if ch.is_lowercase() { 11 } else { 13 };

            ch = ch.to_ascii_uppercase();
            let x = BOARD_LETTERS[..size]
                .find(ch)
                .context("play: the first letter is not a legal char")?;

            let mut y = chars.as_str().parse()?;
            if y > 0 && y <= size {
                y = size - y;
                return Ok(Self {
                    size: size.try_into()?,
                    x,
                    y,
                });
            }
        }

        Err(anyhow::Error::msg("play: invalid coordinate"))
    }
}

impl Vertex {
    #[must_use]
    pub fn up(&self) -> Option<Vertex> {
        if self.y > 0 {
            Some(Vertex {
                size: self.size,
                x: self.x,
                y: self.y - 1,
            })
        } else {
            None
        }
    }

    #[must_use]
    pub fn left(&self) -> Option<Vertex> {
        if self.x > 0 {
            Some(Vertex {
                size: self.size,
                x: self.x - 1,
                y: self.y,
            })
        } else {
            None
        }
    }

    #[must_use]
    pub fn down(&self) -> Option<Vertex> {
        let board_size: usize = self.size.into();

        if self.y < board_size - 1 {
            Some(Vertex {
                size: self.size,
                x: self.x,
                y: self.y + 1,
            })
        } else {
            None
        }
    }

    #[inline]
    #[must_use]
    pub fn on_exit_square(&self) -> bool {
        match self.size {
            BoardSize::_11 => EXIT_SQUARES_11X11.contains(self),
            BoardSize::_13 => EXIT_SQUARES_13X13.contains(self),
        }
    }

    #[inline]
    #[must_use]
    pub fn on_throne(&self) -> bool {
        match self.size {
            BoardSize::_11 => THRONE_11X11 == *self,
            BoardSize::_13 => THRONE_13X13 == *self,
        }
    }

    #[must_use]
    pub fn on_restricted_square(&self) -> bool {
        match &self.size {
            BoardSize::_11 => RESTRICTED_SQUARES_11X11.contains(self),
            BoardSize::_13 => RESTRICTED_SQUARES_13X13.contains(self),
        }
    }

    #[must_use]
    pub fn right(&self) -> Option<Vertex> {
        let board_size: usize = self.size.into();

        if self.x < board_size - 1 {
            Some(Vertex {
                size: self.size,
                x: self.x + 1,
                y: self.y,
            })
        } else {
            None
        }
    }

    #[must_use]
    pub fn touches_wall(&self) -> bool {
        let board_size: usize = self.size.into();

        self.x == 0 || self.x == board_size - 1 || self.y == 0 || self.y == board_size - 1
    }
}

impl From<&Vertex> for usize {
    #[inline]
    fn from(vertex: &Vertex) -> Self {
        let board_size: usize = vertex.size.into();
        vertex.y * board_size + vertex.x
    }
}