use std::{
    io::{self, Write},
    iter,
    sync::mpsc,
    thread,
};

use crossterm::{
    cursor, event, handle_command,
    style::{
        Attribute, Color, Print, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
    },
    terminal,
    tty::IsTty,
};

use crate::{
    buffer::{Buffer, BufferContent, BufferHandle},
    buffer_position::{BufferPosition, BufferRange},
    client::{Client, TargetClient},
    client_event::{Key, LocalEvent},
    cursor::Cursor,
    editor::{Editor, StatusMessageKind},
    mode::Mode,
    syntax::{HighlightedBuffer, TokenKind},
    theme,
};

use super::{read_keys_from_stdin, Ui, UiResult};

macro_rules! write_command {
    ($writer:expr, $command:expr) => {{
        let _ = handle_command!($writer, $command);
    }};
}

fn convert_event(event: event::Event) -> LocalEvent {
    match event {
        event::Event::Key(e) => match e.code {
            event::KeyCode::Backspace => LocalEvent::Key(Key::Backspace),
            event::KeyCode::Enter => LocalEvent::Key(Key::Enter),
            event::KeyCode::Left => LocalEvent::Key(Key::Left),
            event::KeyCode::Right => LocalEvent::Key(Key::Right),
            event::KeyCode::Up => LocalEvent::Key(Key::Up),
            event::KeyCode::Down => LocalEvent::Key(Key::Down),
            event::KeyCode::Home => LocalEvent::Key(Key::Home),
            event::KeyCode::End => LocalEvent::Key(Key::End),
            event::KeyCode::PageUp => LocalEvent::Key(Key::PageUp),
            event::KeyCode::PageDown => LocalEvent::Key(Key::PageDown),
            event::KeyCode::Tab => LocalEvent::Key(Key::Tab),
            event::KeyCode::Delete => LocalEvent::Key(Key::Delete),
            event::KeyCode::F(f) => LocalEvent::Key(Key::F(f)),
            event::KeyCode::Char('\0') => LocalEvent::None,
            event::KeyCode::Char(c) => match e.modifiers {
                event::KeyModifiers::CONTROL => LocalEvent::Key(Key::Ctrl(c)),
                event::KeyModifiers::ALT => LocalEvent::Key(Key::Alt(c)),
                _ => LocalEvent::Key(Key::Char(c)),
            },
            event::KeyCode::Esc => LocalEvent::Key(Key::Esc),
            _ => LocalEvent::None,
        },
        event::Event::Resize(w, h) => LocalEvent::Resize(w, h),
        _ => LocalEvent::None,
    }
}

const fn convert_color(color: theme::Color) -> Color {
    Color::Rgb {
        r: color.0,
        g: color.1,
        b: color.2,
    }
}

pub struct Tui<W>
where
    W: Write,
{
    write: W,
}

impl<W> Tui<W>
where
    W: Write,
{
    pub fn new(write: W) -> Self {
        Self { write }
    }
}

impl<W> Ui for Tui<W>
where
    W: Write,
{
    fn run_event_loop_in_background(
        &mut self,
        event_sender: mpsc::Sender<LocalEvent>,
    ) -> thread::JoinHandle<()> {
        if io::stdin().is_tty() {
            let size = terminal::size().unwrap_or((0, 0));
            let _ = event_sender.send(LocalEvent::Resize(size.0, size.1));

            thread::spawn(move || {
                loop {
                    let event = match event::read() {
                        Ok(event) => event,
                        Err(_) => break,
                    };

                    if event_sender.send(convert_event(event)).is_err() {
                        break;
                    }
                }

                let _ = event_sender.send(LocalEvent::EndOfInput);
            })
        } else {
            thread::spawn(move || read_keys_from_stdin(event_sender))
        }
    }

    fn init(&mut self) -> UiResult<()> {
        ctrlc::set_handler(|| {}).map_err(Box::new)?;

        write_command!(self.write, terminal::EnterAlternateScreen);
        write_command!(self.write, cursor::Hide);
        self.write.flush()?;
        terminal::enable_raw_mode()?;
        Ok(())
    }

    fn display(&mut self, buffer: &[u8]) -> UiResult<()> {
        self.write.write_all(buffer)?;
        write_command!(self.write, ResetColor);
        self.write.flush()?;
        Ok(())
    }

    fn shutdown(&mut self) -> UiResult<()> {
        write_command!(self.write, terminal::Clear(terminal::ClearType::All));
        write_command!(self.write, terminal::LeaveAlternateScreen);
        write_command!(self.write, ResetColor);
        write_command!(self.write, cursor::Show);
        self.write.flush()?;
        terminal::disable_raw_mode()?;
        Ok(())
    }
}

pub fn render(
    editor: &Editor,
    client: &Client,
    target_client: TargetClient,
    buffer: &mut Vec<u8>,
    status_bar_buf: &mut String,
) {
    let has_focus = target_client == editor.focused_client;
    let client_view = ClientView::from(editor, client);

    draw_buffer(buffer, editor, &client_view, has_focus);
    if has_focus {
        draw_picker(buffer, editor, &client_view);
    }
    draw_statusbar(buffer, editor, &client_view, has_focus, status_bar_buf);
}

struct ClientView<'a> {
    client: &'a Client,
    buffer_handle: Option<BufferHandle>,
    buffer: Option<&'a Buffer>,
    main_cursor_position: BufferPosition,
    cursors: &'a [Cursor],
}

impl<'a> ClientView<'a> {
    pub fn from(editor: &'a Editor, client: &'a Client) -> ClientView<'a> {
        let buffer_view = client
            .current_buffer_view_handle()
            .and_then(|h| editor.buffer_views.get(h));
        let buffer_handle = buffer_view.map(|v| v.buffer_handle);
        let buffer = buffer_handle.and_then(|h| editor.buffers.get(h));

        let main_cursor_position;
        let cursors;
        match buffer_view {
            Some(view) => {
                main_cursor_position = view.cursors.main_cursor().position;
                cursors = &view.cursors[..];
            }
            None => {
                main_cursor_position = BufferPosition::default();
                cursors = &[];
            }
        };

        ClientView {
            client,
            buffer_handle,
            buffer,
            main_cursor_position,
            cursors,
        }
    }
}

fn draw_buffer<W>(write: &mut W, editor: &Editor, client_view: &ClientView, has_focus: bool)
where
    W: Write,
{
    #[derive(Clone, Copy, PartialEq, Eq)]
    enum DrawState {
        Token(TokenKind),
        Selection(TokenKind),
        Highlight,
        Cursor,
    }

    let scroll = client_view.client.scroll;
    let width = client_view.client.viewport_size.0;
    let height = client_view.client.height;
    let theme = &editor.config.theme;

    let cursor_color = if has_focus && matches!(editor.mode, Mode::Insert(_)) {
        convert_color(theme.highlight)
    } else {
        convert_color(theme.cursor)
    };

    let background_color = convert_color(theme.background);
    let token_whitespace_color = convert_color(theme.token_whitespace);
    let token_text_color = convert_color(theme.token_text);
    let token_comment_color = convert_color(theme.token_comment);
    let token_keyword_color = convert_color(theme.token_keyword);
    let token_modifier_color = convert_color(theme.token_type);
    let token_symbol_color = convert_color(theme.token_symbol);
    let token_string_color = convert_color(theme.token_string);
    let token_literal_color = convert_color(theme.token_literal);
    let highlight_color = convert_color(theme.highlight);

    let mut text_color = token_text_color;

    write_command!(write, cursor::MoveTo(0, 0));
    write_command!(write, SetBackgroundColor(background_color));
    write_command!(write, SetForegroundColor(text_color));

    let mut line_index = scroll;
    let mut drawn_line_count = 0;

    let cursors = &client_view.cursors[..];
    let cursors_end_index = cursors.len().saturating_sub(1);

    let buffer_content;
    let highlighted_buffer;
    let search_ranges;
    match client_view.buffer {
        Some(buffer) => {
            buffer_content = buffer.content();
            highlighted_buffer = buffer.highlighted();
            search_ranges = buffer.search_ranges();
        }
        None => {
            buffer_content = BufferContent::empty();
            highlighted_buffer = HighlightedBuffer::empty();
            search_ranges = &[];
        }
    }
    let search_ranges_end_index = search_ranges.len().saturating_sub(1);

    let diagnostics = match client_view.buffer_handle {
        Some(handle) => {
            let mut diagnostics: &[_] = &[];
            for client in editor.lsp.clients() {
                diagnostics = client.diagnostics.buffer_diagnostics(handle);
                if !diagnostics.is_empty() {
                    break;
                }
            }
            diagnostics
        }
        None => &[],
    };
    let diagnostics_end_index = diagnostics.len().saturating_sub(1);

    let mut current_cursor_index = 0;
    let mut current_cursor_position = BufferPosition::default();
    let mut current_cursor_range = BufferRange::default();
    if let Some(cursor) = cursors.get(current_cursor_index) {
        current_cursor_position = cursor.position;
        current_cursor_range = cursor.as_range();
    }

    let mut current_search_range_index = 0;
    let mut current_search_range = BufferRange::default();
    if let Some(&range) = search_ranges.get(current_search_range_index) {
        current_search_range = range;
    }

    let mut current_diagnostic_index = 0;
    let mut current_diagnostic_range = BufferRange::default();
    if let Some(diagnostic) = diagnostics.get(current_diagnostic_index) {
        current_diagnostic_range = diagnostic.utf16_range;
    }

    'lines_loop: for line in buffer_content.lines().skip(line_index) {
        let mut draw_state = DrawState::Token(TokenKind::Text);
        let mut was_inside_diagnostic_range = false;
        let mut column_byte_index = 0;
        let mut x = 0;

        write_command!(write, SetForegroundColor(token_text_color));

        for (char_index, c) in line.as_str().char_indices().chain(iter::once((0, '\0'))) {
            if x >= width {
                write_command!(write, cursor::MoveToNextLine(1));

                drawn_line_count += 1;
                x -= width;

                if drawn_line_count >= height {
                    break 'lines_loop;
                }
            }

            let char_position = BufferPosition::line_col(line_index, column_byte_index);

            let token_kind = if c.is_ascii_whitespace() {
                TokenKind::Whitespace
            } else {
                highlighted_buffer.find_token_kind_at(line_index, char_index)
            };

            text_color = match token_kind {
                TokenKind::Whitespace => token_whitespace_color,
                TokenKind::Text => token_text_color,
                TokenKind::Comment => token_comment_color,
                TokenKind::Keyword => token_keyword_color,
                TokenKind::Type => token_modifier_color,
                TokenKind::Symbol => token_symbol_color,
                TokenKind::String => token_string_color,
                TokenKind::Literal => token_literal_color,
            };

            if current_cursor_range.to < char_position && current_cursor_index < cursors_end_index {
                current_cursor_index += 1;
                let cursor = cursors[current_cursor_index];
                current_cursor_position = cursor.position;
                current_cursor_range = cursor.as_range();
            }
            let inside_cursor_range = current_cursor_range.from <= char_position
                && char_position < current_cursor_range.to;

            if current_search_range.to <= char_position
                && current_search_range_index < search_ranges_end_index
            {
                current_search_range_index += 1;
                current_search_range = search_ranges[current_search_range_index];
            }
            let inside_search_range = current_search_range.from <= char_position
                && char_position < current_search_range.to;

            if current_diagnostic_range.to < char_position
                && current_diagnostic_index < diagnostics_end_index
            {
                current_diagnostic_index += 1;
                current_diagnostic_range = diagnostics[current_diagnostic_index].utf16_range;
            }
            let inside_diagnostic_range = current_diagnostic_range.from <= char_position
                && char_position < current_diagnostic_range.to;

            if inside_diagnostic_range != was_inside_diagnostic_range {
                was_inside_diagnostic_range = inside_diagnostic_range;
                if inside_diagnostic_range {
                    write_command!(write, SetAttribute(Attribute::Underlined));
                } else {
                    write_command!(write, SetAttribute(Attribute::NoUnderline));
                }
            }

            if char_position == current_cursor_position {
                if draw_state != DrawState::Cursor {
                    draw_state = DrawState::Cursor;
                    write_command!(write, SetBackgroundColor(cursor_color));
                    write_command!(write, SetForegroundColor(text_color));
                }
            } else if inside_cursor_range {
                if draw_state != DrawState::Selection(token_kind) {
                    draw_state = DrawState::Selection(token_kind);
                    write_command!(write, SetBackgroundColor(text_color));
                    write_command!(write, SetForegroundColor(background_color));
                }
            } else if inside_search_range {
                if draw_state != DrawState::Highlight {
                    draw_state = DrawState::Highlight;
                    write_command!(write, SetBackgroundColor(highlight_color));
                    write_command!(write, SetForegroundColor(background_color));
                }
            } else if draw_state != DrawState::Token(token_kind) {
                draw_state = DrawState::Token(token_kind);
                write_command!(write, SetBackgroundColor(background_color));
                write_command!(write, SetForegroundColor(text_color));
            }

            match c {
                '\0' => {
                    write_command!(write, Print(' '));
                    x += 1;
                }
                ' ' => {
                    write_command!(write, Print(editor.config.values.visual_space));
                    x += 1;
                }
                '\t' => {
                    write_command!(write, Print(editor.config.values.visual_tab_first));
                    let tab_size = editor.config.values.tab_size.get() as u16;
                    let next_tab_stop = (tab_size - 1) - x % tab_size;
                    for _ in 0..next_tab_stop {
                        write_command!(write, Print(editor.config.values.visual_tab_repeat));
                    }
                    x += next_tab_stop + 1;
                }
                _ => {
                    write_command!(write, Print(c));
                    x += 1;
                }
            }

            column_byte_index += c.len_utf8();
        }

        if x < width {
            write_command!(write, SetBackgroundColor(background_color));
            write_command!(write, terminal::Clear(terminal::ClearType::UntilNewLine));
        }

        write_command!(write, cursor::MoveToNextLine(1));

        line_index += 1;
        drawn_line_count += 1;

        if drawn_line_count >= height {
            break;
        }
    }

    write_command!(write, SetBackgroundColor(background_color));
    write_command!(write, SetForegroundColor(token_whitespace_color));
    for _ in drawn_line_count..height {
        write_command!(write, Print(editor.config.values.visual_empty));
        write_command!(write, terminal::Clear(terminal::ClearType::UntilNewLine));
        write_command!(write, cursor::MoveToNextLine(1));
    }
}

fn draw_picker<W>(write: &mut W, editor: &Editor, client_view: &ClientView)
where
    W: Write,
{
    let cursor = editor.picker.cursor();
    let scroll = editor.picker.scroll();

    let half_width = client_view.client.viewport_size.0 / 2;
    let half_width = half_width.saturating_sub(1) as usize;

    let height = editor
        .picker
        .height(editor.config.values.picker_max_height.get() as _);

    let background_color = convert_color(editor.config.theme.token_text);
    let foreground_color = convert_color(editor.config.theme.token_whitespace);

    write_command!(write, SetBackgroundColor(background_color));
    write_command!(write, SetForegroundColor(foreground_color));

    for (i, entry) in editor
        .picker
        .entries(&editor.word_database)
        .enumerate()
        .skip(scroll)
        .take(height)
    {
        if i == cursor {
            write_command!(write, SetForegroundColor(background_color));
            write_command!(write, SetBackgroundColor(foreground_color));
        } else if i == cursor + 1 {
            write_command!(write, SetBackgroundColor(background_color));
            write_command!(write, SetForegroundColor(foreground_color));
        }

        let mut x = 0;

        macro_rules! print_char {
            ($c:expr) => {
                x += 1;
                match $c {
                    '\t' => write_command!(write, Print(' ')),
                    c => write_command!(write, Print(c)),
                }
            };
        }

        let name_char_count = entry.name.chars().count();
        if name_char_count < half_width {
            for c in entry.name.chars() {
                print_char!(c);
            }
        } else {
            write_command!(write, Print("..."));
            x += 3;
            let name_char_count = name_char_count + 3;
            for c in entry
                .name
                .chars()
                .skip(name_char_count.saturating_sub(half_width))
            {
                print_char!(c);
            }
        }
        for _ in x..half_width {
            write_command!(write, Print(' '));
        }
        write_command!(write, Print('|'));
        x = 0;
        for c in entry.description.chars() {
            if x + 3 > half_width {
                write_command!(write, Print("..."));
                break;
            }
            print_char!(c);
        }

        write_command!(write, terminal::Clear(terminal::ClearType::UntilNewLine));
        write_command!(write, cursor::MoveToNextLine(1));
    }
}

fn draw_statusbar<W>(
    write: &mut W,
    editor: &Editor,
    client_view: &ClientView,
    has_focus: bool,
    buf: &mut String,
) where
    W: Write,
{
    let background_color = convert_color(editor.config.theme.token_text);
    let foreground_color = convert_color(editor.config.theme.background);
    let prompt_background_color = convert_color(editor.config.theme.token_whitespace);
    let prompt_foreground_color = background_color;
    let cursor_color = convert_color(editor.config.theme.cursor);

    if has_focus {
        write_command!(write, SetBackgroundColor(background_color));
        write_command!(write, SetForegroundColor(foreground_color));
    } else {
        write_command!(write, SetBackgroundColor(foreground_color));
        write_command!(write, SetForegroundColor(background_color));
    }

    let x = if has_focus {
        let (status_message_kind, status_message) = editor.status_message.message();
        let status_message = status_message.trim();

        if status_message.is_empty() {
            match editor.mode {
                Mode::Normal(_) => match editor.recording_macro {
                    Some(key) => {
                        let text = "recording macro ";
                        let key = key.to_char();
                        write_command!(write, Print(text));
                        write_command!(write, Print(key));
                        Some(text.len() + 1)
                    }
                    None => Some(0),
                },
                Mode::Insert(_) => {
                    let text = "-- INSERT --";
                    write_command!(write, Print(text));
                    Some(text.len())
                }
                Mode::Picker(_) | Mode::ReadLine(_) | Mode::Script(_) => {
                    let read_line = &editor.read_line;

                    write_command!(write, SetBackgroundColor(prompt_background_color));
                    write_command!(write, SetForegroundColor(prompt_foreground_color));
                    write_command!(write, Print(read_line.prompt()));
                    write_command!(write, SetBackgroundColor(background_color));
                    write_command!(write, SetForegroundColor(foreground_color));
                    write_command!(write, Print(read_line.input()));
                    write_command!(write, SetBackgroundColor(cursor_color));
                    write_command!(write, Print(' '));
                    write_command!(write, SetBackgroundColor(background_color));
                    None
                }
            }
        } else {
            fn print_line<W>(write: &mut W, line: &str)
            where
                W: Write,
            {
                for c in line.chars() {
                    match c {
                        '\t' => write_command!(write, Print("    ")),
                        c => write_command!(write, Print(c)),
                    };
                }
            }

            let prefix = match status_message_kind {
                StatusMessageKind::Info => "",
                StatusMessageKind::Error => "error:",
            };

            let line_count = status_message.lines().count();
            if line_count > 1 {
                if prefix.is_empty() {
                    write_command!(write, cursor::MoveUp((line_count - 1) as _));
                } else {
                    write_command!(write, cursor::MoveUp(line_count as _));
                    write_command!(write, SetBackgroundColor(prompt_background_color));
                    write_command!(write, SetForegroundColor(prompt_foreground_color));
                    write_command!(write, Print(prefix));
                    write_command!(write, terminal::Clear(terminal::ClearType::UntilNewLine));
                    write_command!(write, cursor::MoveToNextLine(1));
                    write_command!(write, SetBackgroundColor(background_color));
                    write_command!(write, SetForegroundColor(foreground_color));
                }

                for (i, line) in status_message.lines().enumerate() {
                    print_line(write, line);
                    if i < line_count - 1 {
                        write_command!(write, terminal::Clear(terminal::ClearType::UntilNewLine));
                        write_command!(write, cursor::MoveToNextLine(1));
                    }
                }
            } else {
                write_command!(write, terminal::Clear(terminal::ClearType::CurrentLine));
                write_command!(write, SetBackgroundColor(prompt_background_color));
                write_command!(write, SetForegroundColor(prompt_foreground_color));
                write_command!(write, Print(prefix));
                write_command!(write, SetBackgroundColor(background_color));
                write_command!(write, SetForegroundColor(foreground_color));
                print_line(write, status_message);
            }

            None
        }
    } else {
        Some(0)
    };

    let buffer_needs_save;
    let buffer_path;
    match client_view.buffer {
        Some(buffer) => {
            buffer_needs_save = buffer.needs_save();
            buffer_path = buffer.path().and_then(|p| p.to_str()).unwrap_or("");
        }
        None => {
            buffer_needs_save = false;
            buffer_path = "<no buffer>";
        }
    };

    buf.clear();
    match x {
        Some(x) => {
            use std::fmt::Write;

            let param_count = match &editor.mode {
                Mode::Normal(state) if has_focus => state.count,
                _ => 0,
            };

            if has_focus {
                if param_count > 0 {
                    let _ = write!(buf, "{}", param_count);
                };
                for key in &editor.buffered_keys {
                    let _ = write!(buf, "{}", key);
                }
                buf.push(' ');
            }

            let title_start = buf.len();
            if buffer_needs_save {
                buf.push('*');
            }
            buf.push_str(buffer_path);
            write_command!(write, terminal::SetTitle(&buf[title_start..]));

            if client_view.buffer.is_some() {
                let line_number = client_view.main_cursor_position.line_index + 1;
                let column_number = client_view.main_cursor_position.column_byte_index + 1;
                let _ = write!(buf, ":{},{}", line_number, column_number);
            }
            buf.push(' ');

            let available_width = client_view.client.viewport_size.0 as usize - x;

            let min_index = buf.len() - buf.len().min(available_width);
            let min_index = buf
                .char_indices()
                .map(|(i, _)| i)
                .filter(|i| *i >= min_index)
                .next()
                .unwrap_or(buf.len());
            let buf = &buf[min_index..];

            for _ in 0..(available_width - buf.len()) {
                write_command!(write, Print(' '));
            }
            write_command!(write, Print(buf));
        }
        None => {
            if buffer_needs_save {
                buf.push('*');
            }
            buf.push_str(buffer_path);
            write_command!(write, terminal::SetTitle(&buf));
        }
    }

    write_command!(write, terminal::Clear(terminal::ClearType::UntilNewLine));
}
