use {
    crate::{
        area::Area,
        crossterm::{
            cursor::MoveTo,
            event::{
                KeyCode,
                KeyEvent,
                KeyModifiers,
            },
            queue,
            style::Print,
            QueueableCommand,
        },
        displayable_line::DisplayableLine,
        errors::Result,
        text::FmtText,
        SPACE_FILLING,
    },
    crokey::{
        KeyCombination,
        OneToThree,
    },
    std::io::{
        stdout,
        Write,
    },
};

/// A scrollable text, in a specific area.
///
/// The text is assumed to have been computed for the given area.
///
/// For example:
///
/// ```
/// use termimad::*;
///
/// // You typically borrow those 3 vars from elsewhere
/// let markdown = "#title\n* item 1\n* item 2";
/// let area = Area::new(0, 0, 10, 12);
/// let skin = MadSkin::default();
///
/// // displaying
/// let text = skin.area_text(markdown, &area);
/// let view = TextView::from(&area, &text);
/// view.write().unwrap();
/// ```
///
/// This struct is just a very thin wrapper and may
/// be created dynamically for renderings or event
/// handling.
///
/// If the text and skin are constant, you might prefer to
/// use a MadView instead of a TextView: the MadView owns
/// the markdown string and ensures the formatted text
/// is computed accordingly to the area.
pub struct TextView<'a, 't> {
    area: &'a Area,
    text: &'t FmtText<'t, 't>,
    pub scroll: usize, // number of lines hidden at start
    pub show_scrollbar: bool,
}

impl<'a, 't> TextView<'a, 't> {
    /// make a displayed text, that is a text in an area
    pub const fn from(area: &'a Area, text: &'t FmtText<'_, '_>) -> TextView<'a, 't> {
        TextView {
            area,
            text,
            scroll: 0,
            show_scrollbar: true,
        }
    }

    pub fn content_height(&self) -> usize {
        self.text.lines.len()
    }

    /// return an option which when filled contains
    ///  a tupple with the top and bottom of the vertical
    ///  scrollbar. Return none when the content fits
    ///  the available space (or if show_scrollbar is false).
    pub fn scrollbar(&self) -> Option<(u16, u16)> {
        if self.show_scrollbar {
            self.area
                .scrollbar(self.scroll as u16, self.content_height() as u16)
        } else {
            None
        }
    }

    /// display the text in the area, taking the scroll into account.
    pub fn write(&self) -> Result<()> {
        let mut stdout = stdout();
        self.write_on(&mut stdout)?;
        stdout.flush()?;
        Ok(())
    }

    /// display the text in the area, taking the scroll into account.
    pub fn write_on<W: Write>(&self, w: &mut W) -> Result<()> {
        let scrollbar = self.scrollbar();
        let mut lines = self.text.lines.iter().skip(self.scroll);
        let mut width = self.area.width as usize;
        if scrollbar.is_some() {
            width -= 1;
        }
        for j in 0..self.area.height {
            let y = self.area.top + j;
            w.queue(MoveTo(self.area.left, y))?;
            if let Some(line) = lines.next() {
                let dl = DisplayableLine::new(self.text.skin, line, Some(width));
                queue!(w, Print(&dl))?;
            } else {
                SPACE_FILLING.queue_styled(w, &self.text.skin.paragraph.compound_style, width)?;
            }
            if let Some((sctop, scbottom)) = scrollbar {
                if sctop <= y && y <= scbottom {
                    self.text.skin.scrollbar.thumb.queue(w)?;
                } else {
                    self.text.skin.scrollbar.track.queue(w)?;
                }
            }
        }
        Ok(())
    }

    /// set the scroll position but makes it fit into allowed positions.
    /// Return the actual scroll.
    pub fn set_scroll(&mut self, scroll: usize) -> usize {
        let area_height = self.area.height as usize;
        self.scroll = if self.content_height() > area_height {
            scroll.min(self.content_height() - area_height)
        } else {
            0
        };
        self.scroll
    }

    /// Change the scroll position.
    ///
    /// lines_count can be negative
    pub fn try_scroll_lines(&mut self, lines_count: i32) {
        if lines_count < 0 {
            let lines_count = -lines_count as usize;
            self.scroll = self.scroll.saturating_sub(lines_count);
        } else {
            self.set_scroll(self.scroll + lines_count as usize);
        }
    }

    /// change the scroll position
    /// pages_count can be negative
    pub fn try_scroll_pages<C: Into<f64>>(&mut self, pages_count: C) {
        let pages_count: f64 = pages_count.into();
        let lines: f64 = pages_count * f64::from(self.area.height);
        let lines = if lines < 0.0 {
            lines.floor()
        } else {
            lines.ceil()
        };
        self.try_scroll_lines(lines as i32);
    }

    pub fn line_up(&mut self) -> bool {
        if self.scroll > 0 {
            self.scroll -= 1;
            true
        } else {
            false
        }
    }

    pub fn line_down(&mut self) -> bool {
        let content_height = self.content_height();
        let page_height = self.area.height as usize;
        if self.scroll + page_height < content_height {
            self.scroll += 1;
            true
        } else {
            false
        }
    }

    pub fn page_up(&mut self) -> bool {
        let page_height = self.area.height as usize;
        if self.scroll > page_height {
            self.scroll -= page_height;
            true
        } else if self.scroll > 0 {
            self.scroll = 0;
            true
        } else {
            false
        }
    }

    pub fn page_down(&mut self) -> bool {
        let content_height = self.content_height();
        let page_height = self.area.height as usize;
        if self.scroll + 2 * page_height < content_height {
            self.scroll += page_height;
            true
        } else if self.scroll + page_height < content_height {
            self.scroll = content_height - page_height;
            true
        } else {
            false
        }
    }

    /// Apply an event being a key: page_up, page_down, up and down.
    ///
    /// Return true when the event led to a change, false when it
    /// was discarded.
    pub fn apply_key_event(&mut self, key: KeyEvent) -> bool {
        self.apply_key_combination(key)
    }

    /// Apply an event being a key: page_up, page_down, up and down.
    ///
    /// Return true when the event led to a change, false when it
    /// was discarded.
    pub fn apply_key_combination<K: Into<KeyCombination>>(&mut self, key: K) -> bool {
        let key = key.into();
        if key.modifiers != KeyModifiers::NONE {
            return false;
        }
        match key.codes {
            OneToThree::One(KeyCode::Up) => self.line_up(),
            OneToThree::One(KeyCode::Down) => self.line_down(),
            OneToThree::One(KeyCode::PageUp) => self.page_up(),
            OneToThree::One(KeyCode::PageDown) => self.page_down(),
            _ => false,
        }
    }
}
