// SPDX-FileCopyrightText: 2025 Andrey Kutejko <andy128k@gmail.com>
// SPDX-FileCopyrightText: 2026 Wladimir Palant https://palant.info/
//
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::intviewer::input_modes::InputMode;

pub struct DataPresentation {
    mode: DataPresentationMode,
    wrap_limit: u32,
    tab_size: u32,
    fixed_count: u32,
}

impl DataPresentation {
    pub fn new() -> Self {
        Self {
            mode: DataPresentationMode::NoWrap,
            wrap_limit: 80,
            tab_size: 8,
            fixed_count: 16,
        }
    }

    #[inline]
    pub fn mode(&self) -> DataPresentationMode {
        self.mode
    }

    #[inline]
    pub fn set_mode(&mut self, mode: DataPresentationMode) {
        self.mode = mode;
    }

    pub fn align_offset_to_line_start(&self, input_mode: &InputMode, offset: u64) -> u64 {
        match self.mode() {
            DataPresentationMode::NoWrap => self.nowrap_align_offset(input_mode, offset),
            DataPresentationMode::Wrap => self.wrap_align_offset(input_mode, offset),
            DataPresentationMode::Fixed => self.fixed_align_offset(input_mode, offset),
            DataPresentationMode::Binary => self.bin_align_offset(input_mode, offset),
        }
    }

    pub fn scroll_lines(&self, input_mode: &InputMode, current_offset: u64, delta: i32) -> u64 {
        match self.mode() {
            DataPresentationMode::NoWrap => {
                self.nowrap_scroll_lines(input_mode, current_offset, delta)
            }
            DataPresentationMode::Wrap => self.wrap_scroll_lines(input_mode, current_offset, delta),
            DataPresentationMode::Fixed => {
                self.fixed_scroll_lines(input_mode, current_offset, delta, false)
            }
            DataPresentationMode::Binary => {
                self.bin_scroll_lines(input_mode, current_offset, delta)
            }
        }
    }

    pub fn end_of_line_offset(&self, input_mode: &InputMode, start_of_line: u64) -> u64 {
        match self.mode() {
            DataPresentationMode::NoWrap => self.nowrap_get_eol(input_mode, start_of_line),
            DataPresentationMode::Wrap => self.wrap_get_eol(input_mode, start_of_line),
            DataPresentationMode::Fixed => self.fixed_get_eol(input_mode, start_of_line),
            DataPresentationMode::Binary => self.bin_get_eol(input_mode, start_of_line),
        }
    }

    pub fn set_tab_size(&mut self, tab_size: u32) {
        self.tab_size = tab_size;
    }

    pub fn set_fixed_count(&mut self, chars_per_line: u32) {
        self.fixed_count = chars_per_line;
    }

    pub fn set_wrap_limit(&mut self, chars_per_line: u32) {
        self.wrap_limit = chars_per_line;
    }

    /// Scans the file from offset "start" backwards until a CR/LF/CRLF is found.
    /// Returns the offset of the previous CR/LF/CRLF, or None if start of the file is reached.
    fn find_previous_crlf(&self, input_mode: &InputMode, start: u64) -> Option<u64> {
        for offset in (0..start).rev() {
            match input_mode.raw_byte(offset) {
                None => return None,
                Some(b'\r') => return Some(offset),
                Some(b'\n') => {
                    if offset > 0 && input_mode.raw_byte(offset - 1) == Some(b'\r') {
                        // Treat CRLF combination as a single EOL
                        return Some(offset - 1);
                    } else {
                        return Some(offset);
                    }
                }
                _ => {}
            }
        }
        None
    }

    /// Scans the file backwards starting from offset to find the start of the line.
    fn nowrap_align_offset(&self, input_mode: &InputMode, offset: u64) -> u64 {
        for offset in (0..offset).rev() {
            return match input_mode.raw_byte(offset) {
                None => 0,
                Some(b'\n') => offset + 1,
                Some(b'\r') => {
                    if input_mode.raw_byte(offset + 1) == Some(b'\n') {
                        // Treat CRLF combination as a single EOL
                        continue;
                    } else {
                        offset + 1
                    }
                }
                _ => continue,
            };
        }
        0
    }

    fn nowrap_scroll_lines(
        &self,
        input_mode: &InputMode,
        mut current_offset: u64,
        mut delta: i32,
    ) -> u64 {
        if delta == 0 {
            return current_offset;
        }

        let mut forward = true;
        if delta < 0 {
            delta = -delta;
            forward = false;
        }

        for _ in 0..delta {
            let temp = if forward {
                self.nowrap_get_eol(input_mode, current_offset)
            } else {
                let Some(temp) = self
                    .find_previous_crlf(input_mode, current_offset)
                    .map(|t| self.nowrap_align_offset(input_mode, t))
                else {
                    break;
                };
                temp
            };

            // Offset didn't changed ? we've reached eof
            if temp == current_offset {
                break;
            }

            current_offset = temp;
        }
        current_offset
    }

    fn nowrap_get_eol(&self, input_mode: &InputMode, start_of_line: u64) -> u64 {
        let mut offset = start_of_line;
        while let Some(value) = input_mode.raw_byte(offset) {
            offset += 1;

            // break upon end of line
            if value == b'\n' {
                break;
            } else if value == b'\r' {
                // Treat CRLF combination as a single EOL
                if let Some(value) = input_mode.raw_byte(offset)
                    && value == b'\n'
                {
                    offset += 1;
                }
                break;
            }
        }
        offset
    }

    /// returns the start offset of the previous line,
    /// with special handling for wrap mode.
    fn find_previous_wrapped_text_line(&self, input_mode: &InputMode, start: u64) -> u64 {
        // step 1: find start offset of previous text line
        let Some(mut offset) = self
            .find_previous_crlf(input_mode, start)
            .map(|t| self.nowrap_align_offset(input_mode, t))
        else {
            return start;
        };

        // Step 2
        loop {
            let next_line_offset = self.wrap_get_eol(input_mode, offset);

            // this is the line we want: When the next line's offset is the current
            // offset ('start' parameter), 'offset' will point to the previous
            // displayable line

            if next_line_offset >= start {
                return offset;
            }

            offset = next_line_offset;
        }
    }

    pub fn wrap_align_offset(&self, input_mode: &InputMode, offset: u64) -> u64 {
        let mut line_start = self.nowrap_align_offset(input_mode, offset);
        let mut temp = line_start;
        while temp <= offset {
            line_start = temp;
            temp = self.wrap_scroll_lines(input_mode, temp, 1);
            if temp == line_start {
                break;
            }
        }
        line_start
    }

    fn wrap_scroll_lines(
        &self,
        input_mode: &InputMode,
        mut current_offset: u64,
        mut delta: i32,
    ) -> u64 {
        if delta == 0 {
            return current_offset;
        }

        let mut forward = true;
        if delta < 0 {
            delta = -delta;
            forward = false;
        }

        for _ in 0..delta {
            let temp = if forward {
                self.wrap_get_eol(input_mode, current_offset)
            } else {
                self.find_previous_wrapped_text_line(input_mode, current_offset)
            };

            // Offset didn't change? we've reached eof
            if temp == current_offset {
                break;
            }

            current_offset = temp;
        }
        current_offset
    }

    fn wrap_get_eol(&self, input_mode: &InputMode, start_of_line: u64) -> u64 {
        /* A Single TAB character in the file,
        Translates to several displayable characters on the screen.
        We need to take that into account when calculating number of
        characters before wraping the line */
        let mut char_count = 0;

        let mut offset = start_of_line;

        loop {
            let Some(value) = input_mode.raw_byte(offset) else {
                break;
            };

            offset = input_mode.next_char_offset(offset);

            // break upon end of line
            if value == b'\n' {
                break;
            } else if value == b'\r' {
                // Treat \r\n combination as a single EOL
                if let Some(value) = input_mode.raw_byte(offset)
                    && value == b'\n'
                {
                    offset = input_mode.next_char_offset(offset);
                }
                break;
            } else if value == b'\t' {
                char_count = next_tab_position(char_count, self.tab_size);
            } else {
                char_count += 1;
            }

            if char_count >= self.wrap_limit {
                break;
            }
        }
        offset
    }

    fn fixed_align_offset(&self, _input_mode: &InputMode, offset: u64) -> u64 {
        // We cannot align the offset short of scanning the entire file, just take the current
        // offset as line start.
        offset
    }

    fn fixed_scroll_lines(
        &self,
        input_mode: &InputMode,
        current_offset: u64,
        delta: i32,
        past_last_line: bool,
    ) -> u64 {
        let fixed_count = self.fixed_count.max(1);
        if delta < 0 {
            let mut offset = current_offset;
            for _ in 0..(-delta) as u32 * fixed_count {
                offset = input_mode.previous_char_offset(offset);
            }
            offset
        } else {
            // Slightly more complicated logic when scrolling forward because we usually don't
            // want to "scroll" past the start of the last line: it will cause the content to
            // shift around.
            let max_offset = input_mode.max_offset();
            let mut line_offset = current_offset;
            for _ in 0..delta {
                let mut offset = line_offset;
                for _ in 0..fixed_count {
                    offset = input_mode.next_char_offset(offset);
                    if !past_last_line && offset >= max_offset {
                        return line_offset;
                    }
                }
                line_offset = offset;
            }
            line_offset.clamp(0, max_offset)
        }
    }

    fn fixed_get_eol(&self, input_mode: &InputMode, start_of_line: u64) -> u64 {
        self.fixed_scroll_lines(input_mode, start_of_line, 1, true)
    }

    fn bin_align_offset(&self, input_mode: &InputMode, offset: u64) -> u64 {
        let fixed_count = self.fixed_count.max(1) as u64;
        offset.clamp(0, input_mode.max_offset()) / fixed_count * fixed_count
    }

    fn bin_scroll_lines(&self, input_mode: &InputMode, current_offset: u64, delta: i32) -> u64 {
        let fixed_count = self.fixed_count.max(1);
        (self
            .bin_align_offset(input_mode, current_offset)
            .saturating_add_signed(fixed_count as i64 * delta as i64))
        .clamp(0, input_mode.max_offset())
    }

    fn bin_get_eol(&self, input_mode: &InputMode, start_of_line: u64) -> u64 {
        self.bin_scroll_lines(input_mode, start_of_line, 1)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DataPresentationMode {
    #[default]
    NoWrap,
    Wrap,
    /// fixed number of characters per line.
    Fixed,
    /// fixed number of binary characters per line.
    /// e.g. CHAR=BYTE, no UTF-8 or other translations
    Binary,
}

pub fn next_tab_position(column: u32, tab_size: u32) -> u32 {
    (column + tab_size) / tab_size * tab_size
}

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

    #[test]
    fn set_data_presentation_mode() {
        let mut dp = DataPresentation::new();
        for mode in [
            DataPresentationMode::Wrap,
            DataPresentationMode::NoWrap,
            DataPresentationMode::Fixed,
            DataPresentationMode::Binary,
        ] {
            dp.set_mode(mode);
            assert_eq!(dp.mode(), mode);
        }
    }

    const ABRACADABRA: &[u8] = b"\na\r\nab\nabra\r\nabrac\nabraca\r\nabracad\nabracada\r\nabracadab\nabracadabr\r\nabracadabra\n";

    #[test]
    fn nowrap() {
        let mut imd = InputMode::new(ABRACADABRA);
        imd.set_mode("CP437");
        let mut dp = DataPresentation::new();
        dp.set_mode(DataPresentationMode::NoWrap);

        assert_eq!(dp.nowrap_align_offset(&imd, 0), 0);
        assert_eq!(dp.nowrap_align_offset(&imd, 1), 1);
        assert_eq!(dp.nowrap_align_offset(&imd, 2), 1);
        assert_eq!(dp.nowrap_align_offset(&imd, 3), 1);
        assert_eq!(dp.nowrap_align_offset(&imd, 4), 4);
        assert_eq!(dp.nowrap_align_offset(&imd, 6), 4);
        assert_eq!(dp.nowrap_align_offset(&imd, 6), 4);
        assert_eq!(dp.nowrap_align_offset(&imd, 7), 7);

        assert_eq!(dp.scroll_lines(&imd, 0, 1), 1);
        assert_eq!(dp.scroll_lines(&imd, 1, 1), 4);
        assert_eq!(dp.scroll_lines(&imd, 4, 1), 7);

        assert_eq!(dp.scroll_lines(&imd, 7, -1), 4);
        assert_eq!(dp.scroll_lines(&imd, 4, -1), 1);
        assert_eq!(dp.scroll_lines(&imd, 1, -1), 0);
    }

    #[test]
    fn test_next_tab_position() {
        assert_eq!(next_tab_position(0, 8), 8);
        assert_eq!(next_tab_position(1, 8), 8);
        assert_eq!(next_tab_position(7, 8), 8);
        assert_eq!(next_tab_position(8, 8), 16);
        assert_eq!(next_tab_position(9, 8), 16);
    }
}
