use std::{cell::RefCell, cmp, collections::HashMap, fs::{self, File}, io::{self, Read, Seek, Write}, os::fd::IntoRawFd, path::PathBuf, process::{self, Stdio}, rc::Rc, sync::{Arc, LazyLock, Mutex}, usize};
use syntect::parsing::{SyntaxSet, SyntaxReference};
use tracing::{info, instrument, trace, warn};
use tracing_subscriber::field::debug;
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
use unicode_width::UnicodeWidthStr;
use which::which;

use crate::{logging::LogError, parse::{CachedParseState, ParseCacheTrait, perform_str_replacements}, undo::UndoState};

pub static PRIVESC_CMD: LazyLock<&'static str> = LazyLock::new(|| {
    let cmds = vec!["run0", "sudo", "doas"];
    for cmd in cmds {
        if which(cmd).is_ok() {
            return cmd;
        }
    }
    return "sudo";
});

pub struct Buffer {
    pub name: String,
    pub content: String,
    pub filename: Option<String>,
    /// cursors byte index into the buffer
    pub position: usize,
    /// visual (grapheme) cursor position
    pub cursor: Cursor,
    /// the indexes of all the beginnings of lines
    pub linestarts: Vec<usize>,
    /// This buffer shall not be edited
    pub readonly: bool,
    /// How far the buffer is scrolled
    pub top: usize,
    /// horizontal scroll
    pub left: usize,
    /// Which column the cursor wants to be in (that's vague I know).
    /// If you move the cursor up and down it will jump to this column if the line is long enough.
    pub prefered_col: Option<usize>,
    /// The cached parse states for this buffer
    pub parse_cache: Rc<RefCell<HashMap<usize, CachedParseState>>>,
    pub syntax: Option<SyntaxReference>,
    /// start and end positions of highlgihts
    pub highlights: Vec<(usize, usize)>,
    pub dirty: bool,
    pub undo: UndoState,
    pub ondisk_hash: Option<u64>,
}

fn generate_linestarts(content: &str) -> Vec<usize> {
    let mut ns: Vec<usize> = vec![0];
    ns.extend(content.bytes().enumerate().filter_map(|(i, b)| if b == b'\n' {Some(i+1)} else {None}));
    //if content.chars().last().is_some_and(|c| c != '\n') { ns.push(content.len()) }
    ns.push(content.len());
    ns
}

// pub fn generate_linestarts_textwrap(content: &str, width: usize) -> Vec<usize> {
//     let mut ns: Vec<usize> = vec![0];
//     ns.extend(content.bytes().enumerate().filter_map(|(i, b)| if b == b'\n' {Some(i+1)} else {None}));
//     //if content.chars().last().is_some_and(|c| c != '\n') { ns.push(content.len()) }
//     ns.push(content.len());
//     ns
// }

//* The column and line of the cursor, starting at (0,0) */
#[derive(Debug, Clone, Copy)]
pub struct Cursor {
    pub x: usize,
    pub y: usize,
}

/// how much columns to use for this grapheme cluster
/// TODO should I really consider newlines not to take a column?
/// considering they can be rendered with a column
// pub fn grapheme_width(gr: &str) -> usize {
//     if gr.chars().any(|c| c == '\t') {
//         crate::parse::whitespace::TABSIZE
//     } else {
//         gr.width()
//     }
// }

/// the amount of columsn a str will take,
/// so grapheme clusters plus tab slots
pub fn str_column_length(s: &str) -> usize {
    perform_str_replacements(s, false).width()
}

/// like [str_column_length] but it strips the newline at the end
pub fn str_column_length_no_lb(s: &str) -> usize {
    str_column_length(s.trim_end_matches(|c| c == '\r'|| c == '\n'))
}

impl Buffer {
    pub fn new(name: String, filename: Option<String>, content: String) -> Self {
        let linestarts = generate_linestarts(&content);
        Self {
            filename,
            name: name,
            content,
            position: 0,
            cursor: Cursor { x: 0, y: 0 },
            linestarts,
            readonly: false,
            top: 0,
            left: 0,
            prefered_col: None,
            parse_cache: Rc::new(RefCell::new(HashMap::new())),
            syntax: None,
            highlights: vec![],
            dirty: false,
            undo: UndoState::new(),
            ondisk_hash: None,
        }
    }

    pub fn calculate_ondisk_hash(&mut self) {
        if let Some(filename) = &self.filename {
            match fs::File::options().read(true).open(filename) {
                Ok(mut f) => {
                    let mut buf = vec![]; 
                    let _ = f.read_to_end(&mut buf);
                    self.ondisk_hash = Some(xxhash_rust::xxh3::xxh3_64(&buf));
                },
                Err(e) => tracing::error!("failure calculating ondisk hash for {filename} {e:?}")
            }
        }
    }
    
    /// for testing only, use new() instead
    fn empty() -> Self {
        Self::new(String::new(), None, String::new())
    }

    pub fn increase_all_linestarts(&mut self, from: usize, n: usize) {
        self.linestarts.iter_mut().for_each(|ls| if from >= *ls { *ls = ls.saturating_add(n) });
    }

    /// awful bug fix for a dumb design flaw.
    /// gets the amount of excess bytes preceding
    /// the position due to multi-byte graphemes
    pub fn magic_unicode_offset_bug_fix(&self) -> usize {
         self.content.grapheme_indices(true)
            .filter(|(i, s)| i < &self.position && s.len() > 1)
            .fold(0, |a, (_i, s)| a + s.len() - 1);
        return 0
    }

	/// number of excess bytes between two points caused
	/// by multi-byte graphemes
    pub fn excess_bytes(&self, start: usize, end: usize) -> usize {
        let chunk = &self.content[start..end];
        return chunk.len() - chunk.graphemes(true).count();
    }

    /// update byte position based on the cursor
    /// the cursor must be within bounds, but if it sits on a tab (or similar)
    /// it will move to the right
    pub fn update_position(&mut self) {
        let line_graphemes: Vec<&str> = self.current_line_str().graphemes(true).collect();
        let mut pos = 0;
        let mut col = 0;
        for gr in line_graphemes {
            if col >= self.cursor.x {
                self.cursor.x = col;
                break;
            }
            col += str_column_length(gr);
            pos += gr.len();
        }
        self.position = self.linestarts[self.cursor.y] + pos;
    }

    /// update cursor based on the byte position
    pub fn update_cursor(&mut self) {
        for (i, win) in self.linestarts.windows(2).enumerate() {
            if win[1] > self.position {
                self.cursor.y = i;
                self.cursor.x = str_column_length(self.current_line_str_before_cursor());
                return;
            }
        }
        // especial last line handling
        self.cursor.y = self.linestarts.len() - 2;
        self.cursor.x = str_column_length(self.current_line_str_before_cursor());
    }


    /// current line start and end using only self.cursor.y
    pub fn current_line(&self) -> (usize, usize) {
        return (self.linestarts[self.cursor.y], self.linestarts[self.cursor.y+1]);
    }

    /// length of current line in bytes
    pub fn current_line_length(&self) -> usize {
        let (start, end) = self.current_line();
        end - start
    }

    /// length of current line in grapheme clusters excluding linebreak
    // pub fn current_line_grapheme_length_no_lb(&self) -> usize {
    //     let str = self.current_line_str();
    //     if str.graph
    // }

    pub fn current_line_str(&self) -> &str {
        &self.content[self.linestarts[self.cursor.y]..self.linestarts[self.cursor.y+1]]
    }

    /// this one use self.position, so do not use it to calculate the position (please)
    pub fn current_line_str_before_cursor(&self) -> &str {
        &self.content[self.linestarts[self.cursor.y]..self.position]
    }

    pub fn is_first_line(&self) -> bool {
        self.cursor.y == 0
    }

    pub fn is_last_line(&self) -> bool {
        self.cursor.y + 2 == self.linestarts.len()
    }

    /// set position and do related work (like running [Self::update_cursor])
    pub fn set_position(&mut self, pos: usize) {
        self.prefered_col = None;
        self.position = pos;
        self.update_cursor();
    }

    pub fn set_readonly(&mut self, ro: bool) {
        self.readonly = ro;
    }

    /// Set the position into the buffer based on a location on the viewport
    pub fn set_viewport_cursor_pos(&mut self, x: u16, y: u16) {
        self.cursor.y = cmp::min(self.top + y as usize, self.linestarts.len() - 2);
        self.prefered_col = Some(self.left + x as usize);
        self.place_cursor_x(self.left + x as usize);
        
        self.update_position();
    }

    /// Get position as column and row (of the total buffer not the viewport)
    // pub fn cursor_pos(&self) -> (u16, u16) {
    //     let mut row = 0;
    //     let mut col = 0;
    //     for (index, chr) in self.content.chars().enumerate() {
    //         if index >= self.position {
    //             break;
    //         }
    //         if chr == '\t' {
    //             col += crate::parse::whitespace::TABSIZE as u16;
    //         } else {
    //             col += 1;
    //         }
    //         if chr == '\n' {
    //             row += 1;
    //             col = 0;
    //         }
    //     }
    //     return (col, row)
    // }

    /// return the previous grapheme string and its left boundary
    pub fn prev_grapheme(&self) -> Option<(&str, usize)> {
        let mut gcursor = GraphemeCursor::new(self.position, self.content.len(), true);
        match gcursor.prev_boundary(&self.content, 0).log() {
            Ok(Some(pb)) => {
                Some((&self.content[pb..self.position], pb))
            },
            Ok(None) | Err(_) => None,
        }
    }

    /// return the previous grapheme string and its right boundary
    pub fn cur_grapheme(&self) -> Option<(&str, usize)> {
        let mut gcursor = GraphemeCursor::new(self.position, self.content.len(), true);
        match gcursor.next_boundary(&self.content, 0).log() {
            Ok(Some(pb)) => {
                Some((&self.content[self.position..pb], pb))
            },
            Ok(None) | Err(_) => None,
        }
    }
    /// move left to previous grapheme cluster
    pub fn move_left(&mut self) {
        if let Some((_s, i)) = self.prev_grapheme() {
            self.position = i;
            self.prefered_col = None;
            self.update_cursor();
        }
    }

    /// move to next grapheme cluster
    pub fn move_right(&mut self) {
        if let Some((_s, b)) = self.cur_grapheme() {
            self.position = b;
            self.prefered_col = None;
            self.update_cursor();
        }
    }

    // OLD cursor based move_right behaviour
    // /// move a column to the right
    // pub fn move_right(&mut self) {
    //     if !self.is_end_of_line() {
    //         self.prefered_col = None;
    //         self.cursor.x += 1;
    //         self.update_position();
    //     } else if !self.is_last_line() {
    //         self.prefered_col = Some(0);
    //         self.move_down();
    //     }
    // }


    /// place the x cursor anywhere on the line,
    /// assuming cursor.y is set this will move it to position or eol
    /// and handle the preferred_col
    pub fn place_cursor_x(&mut self, x: usize) {
        let line_length = str_column_length_no_lb(self.current_line_str());
        self.prefered_col = Some(self.prefered_col.unwrap_or(x));
        self.cursor.x = cmp::min(self.prefered_col.unwrap(), line_length);
    }

    /// move up a row
    pub fn move_up(&mut self) {
        if self.is_first_line() { self.goto_start_of_line(); return }
        self.cursor.y = self.cursor.y.saturating_sub(1);
        self.place_cursor_x(self.cursor.x);
        self.update_position();
    }

    /// move down a row
    pub fn move_down(&mut self) {
        if self.is_last_line() { self.goto_end_of_line(); return }
        self.cursor.y += 1;
        self.place_cursor_x(self.cursor.x);
        self.update_position();
    }

    pub fn page_up(&mut self, height: usize) {
        self.top = self.top.saturating_sub(height);
        self.cursor.y = self.cursor.y.saturating_sub(height);
        self.place_cursor_x(self.cursor.x);
        self.update_position();
    }

    pub fn page_down(&mut self, height: usize) {
        self.top = cmp::min(self.top + height, self.linestarts.len().saturating_sub(height));
        self.cursor.y = cmp::min(self.cursor.y + height, self.linestarts.len() - 2);
        self.place_cursor_x(self.cursor.x);
        self.update_position();
    }

    pub fn to_top(&mut self) {
        self.position = 0;
        self.update_cursor();
    }

    pub fn to_bottom(&mut self) {
        self.position = self.content.len()-1;
        self.update_cursor();
    }

    fn start_of_line(&self) -> usize {
        self.linestarts[self.cursor.y]
    }

    fn end_of_line(&self) -> usize {
        self.linestarts[self.cursor.y + 1]
    }

    pub fn move_word_left(&mut self) {
        let Some((s, _)) = self.prev_grapheme() else { return };
        let mut prev = s.chars().next().unwrap();
        let cond: fn(char) -> bool = if prev.is_whitespace() {
            |c: char| c.is_whitespace()
        } else if prev.is_alphanumeric() {
            |c: char| c.is_alphanumeric() || c == '_'
        } else {
            |c: char| !c.is_alphanumeric() && !c.is_whitespace()
        };
        let end_of_prev_line = self.linestarts[self.cursor.y].saturating_sub(1);
        while cond(prev) && self.position >= end_of_prev_line {
            self.move_left();
            let Some((s, _)) = self.prev_grapheme() else { break };
            prev = s.chars().next().unwrap();
        }
        self.prefered_col = None;
        self.update_cursor();
    }

    pub fn move_word_right(&mut self) {
        let Some((s, _)) = self.cur_grapheme() else { return };
        let mut next = s.chars().next().unwrap();
        let cond = if next.is_whitespace() {
            |c: char| c.is_whitespace()
        } else if next.is_alphanumeric() {
            |c: char| c.is_alphanumeric() || c == '_'
        } else {
            |c: char| !c.is_alphanumeric() && !c.is_whitespace()
        };
        while cond(next) {
            self.move_right();
            let Some((s, _)) = self.cur_grapheme() else { break };
            next = s.chars().next().unwrap();
        }
        self.prefered_col = None;
        self.update_cursor();
    }

    pub fn goto_start_of_line(&mut self) {
        self.cursor.x = 0;
        self.prefered_col = None;
        self.update_position();
    }

    pub fn goto_end_of_line(&mut self) {
        self.cursor.x = str_column_length_no_lb(self.current_line_str());
        self.prefered_col = None;
        self.update_position();
    }

    fn current_char(&self) -> Option<char> {
        return self.content.chars().nth(self.position);
    }

    /// find a query through the buffer, set highlights field and return count
    pub fn highlight(&mut self, query: String) -> usize {
        let matches: Vec<_> = self.content.match_indices(&query).map(|(start, match_)| {
            (start, start + match_.len())
        }).collect();

        self.highlights = matches;
        return self.highlights.len()
    }

    /// Jump the cursor to the end of next highlight
    pub fn jump_next_highlight(&mut self) {
        if self.highlights.is_empty() {
            return
        }
        let first = self.highlights.first().unwrap();
        let last = self.highlights.last().unwrap();
        // jump somewhere forward
        if last.1 > self.position {
            if let Some((_start, end)) = self.highlights.iter().find(|(_start, end)| self.position < *end) {
                self.position = *end;
                self.prefered_col = None;
            }
        }
        // loop over
        else {
            self.position = first.1;
        }
        self.update_cursor();
    }

    /// Jump the cursor to the end of previous highlight
    pub fn jump_previous_highlight(&mut self) {
        if self.highlights.is_empty() {
            return
        }
        let first = self.highlights.first().unwrap();
        let last = self.highlights.last().unwrap();
        // jump somewhere backwards
        if first.1 < self.position {
            if let Some((_start, end)) = self.highlights.iter().rev().find(|(_start, end)| self.position > *end) {
                self.position = *end;
                self.prefered_col = None;
            }
        }
        // loop over
        else {
            self.position = first.1;
        }
        self.update_cursor();
    }

    // Tries to find and set a syntax
    pub fn find_syntax<'a>(&mut self, syntax_set: &'a SyntaxSet) -> Option<&'a SyntaxReference> {
        let name = self.name.clone();
        let extension = name.split('.').last().unwrap_or("");
        let syntax = match syntax_set.find_syntax_by_extension(extension) {
            Some(syntax) => Some(syntax),
            None => {
                match self.content.lines().next() {
                    Some(first_line) => syntax_set.find_syntax_by_first_line(&first_line),
                    None => None,
                }
            },
        };
        if let Some(syntax) = syntax {
            self.syntax = Some(syntax.clone());
        }
        syntax
    }

    /// save to disk
    pub fn save(&mut self) -> io::Result<()> {
        if self.filename.is_none() {
            return Err(io::Error::other("no filename"))
        }
        if self.readonly {
            return Err(io::Error::other("buffer is readonly"))
        }
        let filename = self.filename.clone().unwrap();
        let mut file = File::options().create(true).read(true).write(true).open(&filename)?;
        file.rewind()?;
        file.write_all(self.content.as_bytes())?;
        file.set_len(self.content.len() as u64)?;

        info!("Wrote {} bytes to {}", self.content.as_bytes().len(), filename);

        Ok(())
    }

    #[tracing::instrument(skip(self), level="debug", fields(cmd = *PRIVESC_CMD))]
    pub fn save_as_root(&mut self) -> io::Result<()> {
        if self.filename.is_none() {
            return Err(io::Error::other("no filename"))
        }
        let filename = self.filename.clone().unwrap();
        let (reader, mut writer) = io::pipe()?;
        let mut dd = process::Command::new(*PRIVESC_CMD)
            .args(vec!["dd", "bs=4k", &format!("of={}", filename)])
            .stdin(reader)
            .stdout(Stdio::null())
            .stderr(Stdio::piped())
            .spawn()?;
        writer.write_all(self.content.as_bytes())?;
        writer.flush()?;
        nix::unistd::close(writer.into_raw_fd())?;
        let status = dd.wait()?;
        match status.success() {
            true => {
                info!("Wrote {} bytes to {} using {} dd", self.content.as_bytes().len(), filename, *PRIVESC_CMD);
                Ok(())
            },
            false => {
                let mut stderr = String::new();
                dd.stderr.unwrap().read_to_string(&mut stderr)?;
                Err(io::Error::other(stderr))
            },
        }
    }

    #[tracing::instrument(skip(self), level="trace")]
    pub fn dirty(&self) -> bool {
        match &self.filename {
            Some(_) => {
                match self.ondisk_hash {
                    Some(hash) => hash != xxhash_rust::xxh3::xxh3_64(self.content.as_bytes()),
                    None => true,
                }
            },
            None => {
                !self.content.is_empty()
            },
        }
    }

    pub fn insert(&mut self, chr: char) {
        self.content.insert(self.position, chr);
        self.position += 1;
        self.update_linestarts();
        self.update_cursor();
        // TODO can I invalidate from the current line instead?
        self.parse_cache.borrow_mut().invalidate_from(self.top);
        self.prefered_col = None;
    }

    /// insert a string without moving the cursor
    pub fn insert_str(&mut self, content: &str) {
        self.content.insert_str(self.position, content);
        self.parse_cache.borrow_mut().invalidate_from(self.top);
        self.update_linestarts();
    }

    /// insert a string at some position
    pub fn insert_str_at(&mut self, position: usize, content: &str) {
        self.content.insert_str(position, content);
        self.update_linestarts();
    }

    /// like [insert_str] but moves the cursor
    pub fn paste(&mut self, content: &str) {
        self.prefered_col = None;
        self.insert_str(content);
        self.position += content.len();
        self.update_cursor();
    }

    /// Delete previous grapheme, returns removed grapheme
    pub fn backspace(&mut self) -> String {
        if let Some((_s, b)) = self.prev_grapheme() {
            let removed = self.content.drain(b..self.position).collect();
            self.position = b;
            self.prefered_col = None;
            self.update_linestarts();
            self.update_cursor();
            removed
        } else {
            String::new()
        }
    }

    /// Delete grapheme at cursor, returns removed grapheme
    pub fn delete(&mut self) -> String {
        if let Some((_s, b)) = self.cur_grapheme() {
            let removed = self.content.drain(self.position..b).collect();
            self.update_linestarts();
            self.update_cursor();
            removed
        } else {
            String::new()
        }
    }

    /// the action you'd expect from ctrl-backspace
    pub fn delete_word_left(&mut self) -> String {
        let old = self.position;
        self.move_word_left();
        self.drain(self.position..old)
    }

    /// Drain a range from the buffer, return removed
    pub fn drain<R>(&mut self, range: R) -> String where R:  std::ops::RangeBounds<usize> {
        let removed = self.content.drain(range).collect();
        self.update_linestarts();
        removed
    }

    pub fn update_linestarts(&mut self) {
        self.linestarts = generate_linestarts(&self.content);
    }

}

#[test]
fn snowman() {
    let mut buf = Buffer::empty();
    buf.paste("here is ☃ snowman");
    buf.position = 0;
    for _ in 0..12 {
        buf.move_right();
    }
    assert!(buf.position == 12 + String::from("☃").len() - 1);
}

#[test]
fn step_over_y() {
    let mut buf = Buffer::empty();
    let y = "y̆";
    buf.paste(y);
    buf.position = 0;
    buf.move_right();
    assert!(buf.position == 3);
}

#[test]
fn step_over_flags() {
    let mut buf = Buffer::empty();
    let flags: &str = "🇷🇺🇸🇹";
    buf.paste(flags);
    buf.position = 0;
    buf.move_right();
    assert!(buf.position == 8);
    assert!(buf.cursor.x == 2);
}

#[test]
fn step_over_ghosts() {
    let mut buf = Buffer::empty();
    let ghosts: &str = "👻👻👻";
    buf.paste(ghosts);
    buf.position = 0;
    buf.move_right();
    assert!(buf.position == 4);
    assert!(buf.cursor.x == 2);
}

#[test]
fn linestarts() {
    let mut buf = Buffer::empty();
    buf.paste(
"123
123
");
    assert!(buf.linestarts == vec![0,4,8,8]);
}

#[test]
fn linestarts_snowman() {
    let mut buf = Buffer::empty();
    buf.paste(
"1☃3
123
");
    assert!(buf.linestarts == vec![0,6,10,10]);
}

#[test]
fn linestarts_no_lb() {
    let mut buf = Buffer::empty();
    buf.paste(
"123");
    assert!(buf.linestarts == vec![0,3]);
}

#[test]
fn linestarts_empty() {
    let buf = Buffer::empty();
    assert!(buf.linestarts == vec![0,0]);
}
