use unicode_width::UnicodeWidthChar;

use crate::cell::{Cell, Occupancy};
use crate::pen::Pen;
use std::ops::{Index, Range, RangeFull};

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum CharWidth {
    Single,
    Double,
}

impl CharWidth {
    fn as_usize(self) -> usize {
        match self {
            CharWidth::Single => 1,
            CharWidth::Double => 2,
        }
    }
}

#[derive(Clone, PartialEq)]
pub struct Line {
    pub(crate) cells: Vec<Cell>,
    pub(crate) wrapped: bool,
}

impl Line {
    pub(crate) fn blank(cols: usize, pen: Pen) -> Self {
        Line {
            cells: vec![Cell::blank(pen); cols],
            wrapped: false,
        }
    }

    pub(crate) fn reset(&mut self, cols: usize, pen: Pen) {
        self.cells.clear();
        self.cells.resize(cols, Cell::blank(pen));
        self.wrapped = false;
    }

    pub(crate) fn clear(&mut self, range: Range<usize>, pen: &Pen) {
        if range.start == self.len() {
            return;
        }

        let start_col = range.start;
        let end_col = range.end;

        if self.cells[start_col].occupancy() == Occupancy::WideTail {
            self.cells[start_col - 1].set(' ', Occupancy::Single, *pen);
        }

        self.cells[range].fill(Cell::blank(*pen));

        if let Some(next_cell) = self.cells.get_mut(end_col) {
            if next_cell.occupancy() == Occupancy::WideTail {
                next_cell.set(' ', Occupancy::Single, *pen);
            }
        }
    }

    pub(crate) fn print(&mut self, col: usize, ch: char, pen: Pen) -> Option<usize> {
        let cell_occupancy = self.cells[col].occupancy();
        let char_width = self.char_display_width(ch);
        let remaining_cols = self.len() as isize - 1 - col as isize;

        match (cell_occupancy, char_width, remaining_cols) {
            (Occupancy::Single, CharWidth::Single, _) => {
                self.cells[col].set(ch, Occupancy::Single, pen);
            }

            (Occupancy::Single, CharWidth::Double, 0) => {
                return None;
            }

            (Occupancy::Single, CharWidth::Double, 1) => {
                debug_assert_eq!(self.cells[col + 1].occupancy(), Occupancy::Single);

                self.cells[col].set(ch, Occupancy::WideHead, pen);
                self.cells[col + 1].set(' ', Occupancy::WideTail, pen);
            }

            (Occupancy::Single, CharWidth::Double, _right) => {
                self.cells[col].set(ch, Occupancy::WideHead, pen);

                if self.cells[col + 1].occupancy() == Occupancy::WideHead {
                    self.cells[col + 2].set(' ', Occupancy::Single, pen);
                }

                self.cells[col + 1].set(' ', Occupancy::WideTail, pen);
            }

            (Occupancy::WideHead, CharWidth::Single, right) => {
                debug_assert!(right >= 1);
                debug_assert_eq!(self.cells[col + 1].occupancy(), Occupancy::WideTail);

                self.cells[col].set(ch, Occupancy::Single, pen);
                self.cells[col + 1].set(' ', Occupancy::Single, pen);
            }

            (Occupancy::WideHead, CharWidth::Double, right) => {
                debug_assert!(right >= 1);
                debug_assert_eq!(self.cells[col + 1].occupancy(), Occupancy::WideTail);

                self.cells[col].set(ch, Occupancy::WideHead, pen);
                self.cells[col + 1].set(' ', Occupancy::WideTail, pen);
            }

            (Occupancy::WideTail, CharWidth::Single, _right) => {
                debug_assert!(col > 0);
                debug_assert_eq!(self.cells[col - 1].occupancy(), Occupancy::WideHead);

                self.cells[col - 1].set(' ', Occupancy::Single, pen);
                self.cells[col].set(ch, Occupancy::Single, pen);
            }

            (Occupancy::WideTail, CharWidth::Double, 0) => {
                debug_assert!(col > 0);
                debug_assert_eq!(self.cells[col - 1].occupancy(), Occupancy::WideHead);

                return None;
            }

            (Occupancy::WideTail, CharWidth::Double, 1) => {
                debug_assert!(col > 0);
                debug_assert_eq!(self.cells[col - 1].occupancy(), Occupancy::WideHead);

                self.cells[col - 1].set(' ', Occupancy::Single, pen);
                self.cells[col].set(ch, Occupancy::WideHead, pen);
                self.cells[col + 1].set(' ', Occupancy::WideTail, pen);
            }

            (Occupancy::WideTail, CharWidth::Double, _right) => {
                debug_assert!(col > 0);
                debug_assert_eq!(self.cells[col - 1].occupancy(), Occupancy::WideHead);

                self.cells[col - 1].set(' ', Occupancy::Single, pen);
                self.cells[col].set(ch, Occupancy::WideHead, pen);

                if self.cells[col + 1].occupancy() == Occupancy::WideHead {
                    self.cells[col + 2].set(' ', Occupancy::Single, pen);
                }

                self.cells[col + 1].set(' ', Occupancy::WideTail, pen);
            }
        }

        Some(char_width.as_usize())
    }

    pub(crate) fn shift_right(&mut self, col: usize, n: usize, pen: Pen) {
        let col = col.min(self.len() - 1);
        let cur_cell = &mut self.cells[col];

        if cur_cell.occupancy() == Occupancy::WideTail {
            cur_cell.set(' ', Occupancy::Single, pen);
            self.cells[col - 1].set(' ', Occupancy::Single, pen);
        }

        self.cells[col..].rotate_right(n);

        let cur_cell = &mut self.cells[col];

        if cur_cell.occupancy() == Occupancy::WideTail {
            cur_cell.set(' ', Occupancy::Single, pen);
            self.cells
                .last_mut()
                .unwrap()
                .set(' ', Occupancy::Single, pen);
        }
    }

    pub(crate) fn delete(&mut self, col: usize, n: usize, pen: &Pen) {
        if self.cells[col].occupancy() == Occupancy::WideTail {
            self.cells[col - 1].set(' ', Occupancy::Single, *pen);
        }

        self.cells[col..].rotate_left(n);

        let cur_cell = &mut self.cells[col];

        if cur_cell.occupancy() == Occupancy::WideTail {
            cur_cell.set(' ', Occupancy::Single, *pen);
        }

        let fill_start = self.cells.len() - n;
        self.cells[fill_start..].fill(Cell::blank(*pen));
    }

    pub(crate) fn extend(&mut self, mut other: Line, len: usize) -> (bool, Option<Line>) {
        let mut needed = len - self.len();

        if needed == 0 {
            return (true, Some(other));
        }

        if !self.wrapped {
            self.expand(len, &Pen::default());

            return (true, Some(other));
        }

        if !other.wrapped {
            other.trim();
        }

        if needed < other.len() {
            if other[needed].occupancy() == Occupancy::WideTail {
                needed -= 1;
                let pen = self.cells[self.len() - 1].pen();
                self.cells.push(Cell::new(' ', Occupancy::Single, *pen));
            }

            self.cells.extend(&other[0..needed]);
            let mut cells = other.cells;
            cells.rotate_left(needed);
            cells.truncate(cells.len() - needed);

            return (
                true,
                Some(Line {
                    cells,
                    wrapped: other.wrapped,
                }),
            );
        }

        self.cells.extend(&other[..]);

        if !other.wrapped {
            self.wrapped = false;

            if self.len() < len {
                self.expand(len, &Pen::default());
            }

            (true, None)
        } else {
            (false, None)
        }
    }

    pub(crate) fn expand(&mut self, len: usize, pen: &Pen) {
        let tpl = Cell::blank(*pen);
        let filler = std::iter::repeat_n(tpl, len - self.len());
        self.cells.extend(filler);
    }

    pub(crate) fn contract(&mut self, mut len: usize) -> Option<Line> {
        if !self.wrapped {
            let trimmed_len = self.len() - self.trailers();
            self.cells.truncate(len.max(trimmed_len));
        }

        if self.len() > len {
            let wide_char_boundary = self.cells[len].occupancy() == Occupancy::WideTail;

            if wide_char_boundary {
                len -= 1;
            }

            let mut rest = Line {
                cells: self.cells.split_off(len),
                wrapped: self.wrapped,
            };

            if wide_char_boundary {
                let pen = self.cells[self.cells.len() - 1].pen();
                self.cells.push(Cell::new(' ', Occupancy::Single, *pen));
            }

            if !self.wrapped {
                rest.trim();
            }

            if rest.cells.is_empty() {
                None
            } else {
                self.wrapped = true;

                Some(rest)
            }
        } else {
            None
        }
    }

    pub fn len(&self) -> usize {
        self.cells.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    pub fn cells(&self) -> &[Cell] {
        &self.cells
    }

    pub fn chunks<'a>(
        &'a self,
        predicate: impl Fn(&Cell, &Cell) -> bool + 'a,
    ) -> impl Iterator<Item = Vec<Cell>> + 'a {
        let i = self
            .cells
            .iter()
            .filter(|c| c.occupancy() != Occupancy::WideTail);

        Chunks::new(i, predicate)
    }

    pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
        self.cells.iter().filter_map(|c| {
            if c.occupancy() != Occupancy::WideTail {
                Some(c.char())
            } else {
                None
            }
        })
    }

    pub fn text(&self) -> String {
        self.chars().collect()
    }

    fn trim(&mut self) {
        let trailers = self.trailers();

        if trailers > 0 {
            self.cells.truncate(self.len() - trailers);
        }
    }

    fn trailers(&self) -> usize {
        self.cells
            .iter()
            .rev()
            .take_while(|cell| cell.is_default())
            .count()
    }

    pub(crate) fn is_blank(&self) -> bool {
        self.cells.iter().all(|c| c.is_default())
    }

    fn char_display_width(&self, ch: char) -> CharWidth {
        if ch <= '\u{7e}' || ch.width().unwrap_or(1) != 2 {
            CharWidth::Single
        } else {
            CharWidth::Double
        }
    }
}

impl std::fmt::Debug for Line {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut s = self.text();

        if self.wrapped {
            s.push('⏎');
        }

        write!(f, "{:?}", s)
    }
}

struct Chunks<'a, I, F>
where
    I: Iterator<Item = &'a Cell>,
    F: Fn(&Cell, &Cell) -> bool,
{
    iter: I,
    predicate: F,
    cells: Vec<Cell>,
}

impl<'a, I: Iterator<Item = &'a Cell>, F: Fn(&Cell, &Cell) -> bool> Chunks<'a, I, F> {
    fn new(iter: I, predicate: F) -> Self {
        Self {
            iter,
            predicate,
            cells: Vec::new(),
        }
    }
}

impl<'a, I: Iterator<Item = &'a Cell>, F: Fn(&Cell, &Cell) -> bool> Iterator for Chunks<'a, I, F> {
    type Item = Vec<Cell>;

    fn next(&mut self) -> Option<Self::Item> {
        for cell in self.iter.by_ref() {
            if self.cells.is_empty() {
                self.cells.push(*cell);
                continue;
            }

            if (self.predicate)(self.cells.last().unwrap(), cell) {
                let cells = std::mem::take(&mut self.cells);
                self.cells.push(*cell);
                return Some(cells);
            } else {
                self.cells.push(*cell);
            }
        }

        if self.cells.is_empty() {
            None
        } else {
            Some(std::mem::take(&mut self.cells))
        }
    }
}

impl Index<usize> for Line {
    type Output = Cell;

    fn index(&self, index: usize) -> &Self::Output {
        &self.cells[index]
    }
}

impl Index<Range<usize>> for Line {
    type Output = [Cell];

    fn index(&self, range: Range<usize>) -> &Self::Output {
        &self.cells[range]
    }
}

impl Index<RangeFull> for Line {
    type Output = [Cell];

    fn index(&self, range: RangeFull) -> &Self::Output {
        &self.cells[range]
    }
}

#[cfg(test)]
mod tests {
    use super::{Cell, Chunks, Occupancy, Pen};

    fn chars(cells: &[Cell]) -> Vec<char> {
        cells.iter().map(|c| c.char()).collect()
    }

    #[test]
    fn chunks() {
        let pen = Pen::default();

        let cells = [
            Cell::new('0', Occupancy::Single, pen),
            Cell::new('a', Occupancy::Single, pen),
            Cell::new('b', Occupancy::Single, pen),
            Cell::new('C', Occupancy::Single, pen),
            Cell::new('D', Occupancy::Single, pen),
            Cell::new('E', Occupancy::Single, pen),
            Cell::new('1', Occupancy::Single, pen),
            Cell::new('F', Occupancy::Single, pen),
            Cell::new('g', Occupancy::Single, pen),
        ];

        let chunks: Vec<Vec<Cell>> = Chunks::new(cells.iter(), |c1, c2| {
            c1.char().is_ascii_digit()
                || c2.char().is_ascii_digit()
                || (c1.char().is_lowercase() && c2.char().is_uppercase())
                || (c1.char().is_uppercase() && c2.char().is_lowercase())
        })
        .collect();

        assert_eq!(&chars(&chunks[0]), &['0']);
        assert_eq!(&chars(&chunks[1]), &['a', 'b']);
        assert_eq!(&chars(&chunks[2]), &['C', 'D', 'E']);
        assert_eq!(&chars(&chunks[3]), &['1']);
        assert_eq!(&chars(&chunks[4]), &['F']);
        assert_eq!(&chars(&chunks[5]), &['g']);
    }
}
