use core::fmt;

use binrw::{BinRead, BinWrite};
use substring::Substring;

use crate::{Error, Result};


const MAX_SYMBOL_ID: u32 = 20;
const SYMBOL_PARTS: &str = include_str!("symbol_parts.txt");
const SYMBOL_PART_WIDTH: usize = 6;
const SYMBOL_PART_HEIGTH: usize = 3;


#[derive(Debug, Clone, Copy, BinRead, BinWrite)]
pub struct Symbol {
    #[br(assert(id < MAX_SYMBOL_ID))]
    id: u32,
}

impl Symbol {
    pub fn set_by_id(&mut self, id: u32) -> Result<()> {
        if id > MAX_SYMBOL_ID {
            return Err(Error::SymbolIdOutOfRange);
        }

        self.id = id;

        Ok(())
    }

    pub fn wrapping_next(&self) -> Self {
        let id = self.id + 1;
        if id > MAX_SYMBOL_ID {
            Self { id: 0 }
        } else {
            Self { id }
        }
    }

    pub fn wrapping_previous(&self) -> Self {
        match self.id.checked_sub(1) {
            Some(id) => Self { id },
            None => Self { id: MAX_SYMBOL_ID },
        }
    }
}

impl AsRef<u32> for Symbol {
    fn as_ref(&self) -> &u32 {
        &self.id
    }
}

impl fmt::Display for Symbol {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", get_symbol(self.id as usize).unwrap())
    }
}

fn get_symbol(id: usize) -> Option<String> {
    let (top_left, top_right, btm_left, btm_right) = symbol_part_ids(id)?;
    get_symbol_with_parts(top_left, top_right, btm_left, btm_right)
}

fn symbol_part_ids(id: usize) -> Option<(usize, usize, usize, usize)> {
    let ids = match id {
        0 => (0, 1, 3, 2),
        1 => (4, 4, 7, 7),
        2 => (9, 9, 13, 2),
        3 => (15, 16, 9, 9),
        4 => (4, 9, 4, 9),
        5 => (15, 12, 3, 9),
        6 => (5, 5, 9, 12),
        7 => (12, 9, 15, 15),
        8 => (7, 9, 12, 8),
        9 => (12, 12, 9, 9),
        10 => (14, 7, 14, 7),
        11 => (8, 8, 13, 13),
        12 => (2, 3, 2, 3),
        13 => (10, 7, 7, 12),
        14 => (7, 7, 10, 12),
        15 => (15, 15, 15, 15),
        16 => (4, 4, 4, 4),
        17 => (11, 10, 11, 10),
        18 => (12, 8, 12, 8),
        19 => (6, 6, 11, 10),
        20 => (12, 9, 11, 10),
        _ => return None,
    };

    Some(ids)
}

fn get_symbol_with_parts(
    top_left: usize,
    top_right: usize,
    btm_left: usize,
    btm_right: usize,
) -> Option<String> {
    if top_left > 16 || top_right > 16 || btm_left > 16 || btm_right > 16 {
        return None;
    }

    let top_left = get_symbol_part(top_left);
    let top_right = get_symbol_part(top_right);
    let btm_left = get_symbol_part(btm_left);
    let btm_right = get_symbol_part(btm_right);

    let top = top_left
        .lines()
        .zip(top_right.lines())
        .map(|(left, right)| format!("{}  {}", left, right));

    let bottom = btm_left
        .lines()
        .zip(btm_right.lines())
        .map(|(left, right)| format!("{}  {}", left, right));

    const EMPTY_LINE: &str = "              ";

    let symbol = top
        .chain([EMPTY_LINE.to_string()].into_iter())
        .chain(bottom)
        .collect::<Vec<_>>()
        .join("\n");

    Some(symbol)
}

fn get_symbol_part(idx: usize) -> &'static str {
    const SYMBOL_PART_SIZE: usize = SYMBOL_PART_WIDTH * SYMBOL_PART_HEIGTH + 2;
    let start = (SYMBOL_PART_SIZE + 2) * idx;
    let end = start + SYMBOL_PART_SIZE;
    SYMBOL_PARTS.substring(start, end)
}