use crate::ui::command::parser::{
    ARG_ALL_KEY, ARG_COMMAND, ARG_DEBUG_COMMAND, ASYNC_COMMAND, ASYNC_COMMAND_BACKTRACE_SUBCOMMAND,
    ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT, ASYNC_COMMAND_STEP_OUT_SUBCOMMAND,
    ASYNC_COMMAND_STEP_OUT_SUBCOMMAND_SHORT, ASYNC_COMMAND_STEP_OVER_SUBCOMMAND,
    ASYNC_COMMAND_STEP_OVER_SUBCOMMAND_SHORT, ASYNC_COMMAND_TASK_SUBCOMMAND,
    BACKTRACE_ALL_SUBCOMMAND, BACKTRACE_COMMAND, BACKTRACE_COMMAND_SHORT, BREAK_COMMAND,
    BREAK_COMMAND_SHORT, CALL_COMMAND, CONTINUE_COMMAND, CONTINUE_COMMAND_SHORT, FRAME_COMMAND,
    FRAME_COMMAND_INFO_SUBCOMMAND, FRAME_COMMAND_SWITCH_SUBCOMMAND, HELP_COMMAND,
    HELP_COMMAND_SHORT, MEMORY_COMMAND, MEMORY_COMMAND_READ_SUBCOMMAND, MEMORY_COMMAND_SHORT,
    MEMORY_COMMAND_WRITE_SUBCOMMAND, ORACLE_COMMAND, REGISTER_COMMAND,
    REGISTER_COMMAND_INFO_SUBCOMMAND, REGISTER_COMMAND_READ_SUBCOMMAND, REGISTER_COMMAND_SHORT,
    REGISTER_COMMAND_WRITE_SUBCOMMAND, RUN_COMMAND, RUN_COMMAND_SHORT, SHARED_LIB_COMMAND,
    SHARED_LIB_COMMAND_INFO_SUBCOMMAND, SOURCE_COMMAND, SOURCE_COMMAND_DISASM_SUBCOMMAND,
    SOURCE_COMMAND_FUNCTION_SUBCOMMAND, STEP_INSTRUCTION_COMMAND, STEP_INTO_COMMAND,
    STEP_INTO_COMMAND_SHORT, STEP_OUT_COMMAND, STEP_OUT_COMMAND_SHORT, STEP_OVER_COMMAND,
    STEP_OVER_COMMAND_SHORT, SYMBOL_COMMAND, THREAD_COMMAND, THREAD_COMMAND_CURRENT_SUBCOMMAND,
    THREAD_COMMAND_INFO_SUBCOMMAND, THREAD_COMMAND_SWITCH_SUBCOMMAND, TRIGGER_COMMAND,
    TRIGGER_COMMAND_ANY_TRIGGER_SUBCOMMAND, TRIGGER_COMMAND_BRKPT_TRIGGER_SUBCOMMAND,
    TRIGGER_COMMAND_INFO_SUBCOMMAND, TRIGGER_COMMAND_WP_TRIGGER_SUBCOMMAND, VAR_COMMAND,
    VAR_DEBUG_COMMAND, VAR_LOCAL_KEY, WATCH_COMMAND, WATCH_COMMAND_SHORT, WATCH_INFO_SUBCOMMAND,
    WATCH_REMOVE_SUBCOMMAND, WATCH_REMOVE_SUBCOMMAND_SHORT,
};
use chumsky::prelude::{any, choice, just};
use chumsky::text::whitespace;
use chumsky::{Parser, extra, text};
use crossterm::style::{Color, Stylize};
use log::warn;
use rustyline::completion::{Completer, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::HistoryHinter;
use rustyline::history::{FileHistory, MemHistory};
use rustyline::line_buffer::LineBuffer;
use rustyline::{Changeset, CompletionType, Config, Context, Editor, ExternalPrinter};
use rustyline_derive::{Helper, Hinter, Validator};
use std::borrow::Cow;
use std::borrow::Cow::{Borrowed, Owned};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use trie_rs::{Trie, TrieBuilder};

struct CommandHint {
    short: Option<String>,
    long: String,
    subcommands: Vec<String>,
}

impl CommandHint {
    fn long(&self) -> String {
        self.long.clone()
    }

    fn display_with_short(&self) -> String {
        if let Some(ref short) = self.short {
            if self.long.starts_with(short) {
                format!(
                    "{}{}",
                    short.clone().bold().underlined(),
                    &self.long[short.len()..]
                )
            } else {
                format!("{}|{}", &self.long, short.clone().bold().underlined())
            }
        } else {
            self.long()
        }
    }
}

impl From<&str> for CommandHint {
    fn from(value: &str) -> Self {
        CommandHint {
            short: None,
            long: value.to_string(),
            subcommands: vec![],
        }
    }
}

impl From<(&str, &str)> for CommandHint {
    fn from((short, long): (&str, &str)) -> Self {
        CommandHint {
            short: Some(short.to_string()),
            long: long.to_string(),
            subcommands: vec![],
        }
    }
}

pub struct CommandCompleter {
    commands: Vec<CommandHint>,
    subcommand_hints: HashMap<String, Vec<String>>,
    file_hints: Trie<u8>,
    var_hints: Trie<u8>,
    vars: Vec<String>,
    arg_hints: Trie<u8>,
    args: Vec<String>,
}

impl CommandCompleter {
    fn new(commands: impl IntoIterator<Item = CommandHint>) -> Self {
        let commands: Vec<CommandHint> = commands.into_iter().collect();
        let subcommand_hints = commands
            .iter()
            .flat_map(|cmd| {
                let mut hints = vec![(cmd.long.clone(), cmd.subcommands.clone())];
                if let Some(ref short) = cmd.short {
                    hints.push((short.clone(), cmd.subcommands.clone()));
                }
                hints
            })
            .collect::<HashMap<String, Vec<String>>>();

        Self {
            commands,
            subcommand_hints,
            file_hints: TrieBuilder::new().build(),
            var_hints: TrieBuilder::new().build(),
            arg_hints: TrieBuilder::new().build(),
            args: vec![],
            vars: vec![],
        }
    }

    pub fn replace_file_hints(&mut self, files: impl IntoIterator<Item = PathBuf>) {
        let mut builder = TrieBuilder::new();
        files.into_iter().for_each(|path: PathBuf| {
            let file_name = path
                .file_name()
                .and_then(|oss| oss.to_str())
                .unwrap_or_default()
                .to_owned();
            builder.push(file_name);
        });
        self.file_hints = builder.build();
    }

    pub fn replace_local_var_hints(&mut self, variables: impl IntoIterator<Item = String>) {
        let mut builder = TrieBuilder::new();
        self.vars = variables.into_iter().collect();
        self.vars.iter().for_each(|var| {
            builder.push(var);
        });
        self.vars.push(VAR_LOCAL_KEY.underlined().to_string());
        builder.push(VAR_LOCAL_KEY);
        self.var_hints = builder.build();
    }

    pub fn replace_arg_hints(&mut self, args: impl IntoIterator<Item = String>) {
        let mut builder = TrieBuilder::new();
        self.args = args.into_iter().collect();
        self.args.iter().for_each(|arg| {
            builder.push(arg);
        });
        self.args.push(ARG_ALL_KEY.underlined().to_string());
        builder.push(ARG_ALL_KEY);
        self.arg_hints = builder.build();
    }
}

#[derive(Debug)]
enum CompletableCommand<'a> {
    Breakpoint(&'a str),
    PrintVariables(&'a str),
    PrintArguments(&'a str),
    Unrecognized(&'a str, Option<&'a str>),
}

impl<'a> CompletableCommand<'a> {
    fn recognize(line: &'a str) -> Option<CompletableCommand<'a>> {
        let op = just::<_, _, extra::Default>;

        let bp = op(BREAK_COMMAND)
            .or(op(BREAK_COMMAND_SHORT))
            .then(whitespace().at_least(1))
            .ignore_then(any().repeated().to_slice())
            .map(CompletableCommand::Breakpoint);

        let var = op(VAR_DEBUG_COMMAND)
            .or(op(VAR_COMMAND))
            .then(whitespace().at_least(1))
            .ignore_then(any().repeated().to_slice())
            .map(CompletableCommand::PrintVariables);

        let arg = op(ARG_DEBUG_COMMAND)
            .or(op(ARG_COMMAND))
            .then(whitespace().at_least(1))
            .ignore_then(any().repeated().to_slice())
            .map(CompletableCommand::PrintArguments);

        let other = text::ident()
            .then_ignore(whitespace().at_least(1))
            .then(text::ident().or_not())
            .map(|(s1, s2): (&str, Option<&str>)| CompletableCommand::Unrecognized(s1.trim(), s2))
            .padded();

        let r = choice((bp, var, arg, other)).parse(line);
        r.into_result().ok()
    }
}

impl Completer for CommandCompleter {
    type Candidate = Pair;

    fn complete(
        &self,
        line: &str,
        _pos: usize,
        _ctx: &Context<'_>,
    ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
        fn pairs_from_variants(
            variants: impl Iterator<Item = impl ToString>,
            line: &str,
            tpl: &str,
            replacement_suffix: &str,
        ) -> (usize, Vec<Pair>) {
            let pos = line.len() - tpl.len();
            let pairs = variants.map(|v| Pair {
                display: v.to_string(),
                replacement: v.to_string() + replacement_suffix,
            });
            (pos, pairs.collect())
        }

        match CompletableCommand::recognize(line) {
            Some(CompletableCommand::Breakpoint(maybe_file)) => {
                if maybe_file.trim().is_empty() {
                    return Ok((0, vec![]));
                }

                let variants = self.file_hints.predictive_search(maybe_file);
                if !variants.is_empty() {
                    let variants_iter = variants.iter().map(|var| {
                        std::str::from_utf8(var.as_slice()).expect("invalid utf-8 string")
                    });
                    return Ok(pairs_from_variants(variants_iter, line, maybe_file, ":"));
                }
            }
            Some(CompletableCommand::PrintVariables(maybe_var)) => {
                if maybe_var.trim().is_empty() {
                    return Ok(pairs_from_variants(self.vars.iter(), line, maybe_var, ""));
                }

                let variants = self.var_hints.predictive_search(maybe_var);
                if !variants.is_empty() {
                    let variants_iter = variants.iter().map(|var| {
                        std::str::from_utf8(var.as_slice()).expect("invalid utf-8 string")
                    });
                    return Ok(pairs_from_variants(variants_iter, line, maybe_var, ""));
                }
            }
            Some(CompletableCommand::PrintArguments(maybe_arg)) => {
                if maybe_arg.trim().is_empty() {
                    return Ok(pairs_from_variants(self.args.iter(), line, maybe_arg, ""));
                }

                let variants = self.arg_hints.predictive_search(maybe_arg);
                if !variants.is_empty() {
                    let variants_iter = variants.iter().map(|var| {
                        std::str::from_utf8(var.as_slice()).expect("invalid utf-8 string")
                    });
                    return Ok(pairs_from_variants(variants_iter, line, maybe_arg, ""));
                }
            }
            Some(CompletableCommand::Unrecognized(cmd, mb_subcmd_part)) => {
                if let Some(subcommands) = self.subcommand_hints.get(cmd) {
                    let pos = cmd.len() + 1;
                    let subcmd_part = mb_subcmd_part.unwrap_or_default();
                    let subcommands = subcommands
                        .iter()
                        .filter(|&subcmd| subcmd.starts_with(subcmd_part))
                        .map(|subcmd| Pair {
                            display: subcmd.to_string(),
                            replacement: subcmd.to_string(),
                        })
                        .collect();

                    return Ok((pos, subcommands));
                }
            }
            _ => {}
        }

        let pairs = self
            .commands
            .iter()
            .filter(|&cmd| cmd.long.starts_with(line))
            .map(|cmd| Pair {
                display: cmd.display_with_short(),
                replacement: cmd.long(),
            })
            .collect();
        Ok((0, pairs))
    }
}

#[derive(Helper, Hinter, Validator)]
pub struct RLHelper {
    pub completer: Arc<Mutex<CommandCompleter>>,
    #[rustyline(Hinter)]
    hinter: HistoryHinter,
    pub colored_prompt: String,
}

impl Completer for RLHelper {
    type Candidate = <CommandCompleter as Completer>::Candidate;

    fn complete(
        &self,
        line: &str,
        pos: usize,
        ctx: &Context<'_>,
    ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
        self.completer.lock().unwrap().complete(line, pos, ctx)
    }

    fn update(&self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) {
        self.completer
            .lock()
            .unwrap()
            .update(line, start, elected, cl)
    }
}

impl Highlighter for RLHelper {
    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
        &'s self,
        prompt: &'p str,
        default: bool,
    ) -> Cow<'b, str> {
        if default {
            Borrowed(&self.colored_prompt)
        } else {
            Borrowed(prompt)
        }
    }

    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
        Owned(format!("{}", hint.with(Color::Grey)))
    }
}

pub enum BSEditor {
    InMem(Editor<RLHelper, MemHistory>),
    InFile(Editor<RLHelper, FileHistory>),
}

macro_rules! call_editor {
    ($self: expr, $fn_name: tt, $($arg: expr),*) => {
        match $self {
            BSEditor::InMem(editor) => editor.$fn_name($($arg),*),
            BSEditor::InFile(editor) => editor.$fn_name($($arg),*),
        }
    };
}

impl BSEditor {
    const HISTORY_FILE_NAME: &str = ".config/bs/history";

    fn history_file() -> anyhow::Result<PathBuf> {
        let path = home::home_dir().ok_or(anyhow::anyhow!("home dir not found"))?;
        Ok(path.join(Self::HISTORY_FILE_NAME))
    }

    pub fn helper_mut(&mut self) -> Option<&mut RLHelper> {
        call_editor!(self, helper_mut,)
    }

    pub fn readline(&mut self, prompt: &str) -> rustyline::Result<String> {
        call_editor!(self, readline, prompt)
    }

    pub fn add_history_entry<S>(&mut self, line: S) -> rustyline::Result<bool>
    where
        S: AsRef<str> + Into<String>,
    {
        call_editor!(self, add_history_entry, line)
    }

    pub fn create_external_printer(&mut self) -> rustyline::Result<impl ExternalPrinter + 'static> {
        call_editor!(self, create_external_printer,)
    }

    pub fn new(promt: &str, oracles: &[&str], save_history_in_file: bool) -> anyhow::Result<Self> {
        let config: Config = Config::builder()
            .history_ignore_space(true)
            .completion_type(CompletionType::List)
            .build();

        let commands = [
            VAR_COMMAND.into(),
            ARG_COMMAND.into(),
            VAR_DEBUG_COMMAND.into(),
            ARG_DEBUG_COMMAND.into(),
            (CONTINUE_COMMAND_SHORT, CONTINUE_COMMAND).into(),
            CommandHint {
                short: None,
                long: FRAME_COMMAND.to_string(),
                subcommands: vec![
                    FRAME_COMMAND_INFO_SUBCOMMAND.to_string(),
                    FRAME_COMMAND_SWITCH_SUBCOMMAND.to_string(),
                ],
            },
            (RUN_COMMAND_SHORT, RUN_COMMAND).into(),
            STEP_INSTRUCTION_COMMAND.into(),
            (STEP_INTO_COMMAND_SHORT, STEP_INTO_COMMAND).into(),
            (STEP_OUT_COMMAND_SHORT, STEP_OUT_COMMAND).into(),
            (STEP_OVER_COMMAND_SHORT, STEP_OVER_COMMAND).into(),
            SYMBOL_COMMAND.into(),
            (BREAK_COMMAND_SHORT, BREAK_COMMAND).into(),
            CommandHint {
                short: Some(WATCH_COMMAND_SHORT.to_string()),
                long: WATCH_COMMAND.to_string(),
                subcommands: vec![
                    WATCH_REMOVE_SUBCOMMAND.to_string(),
                    WATCH_REMOVE_SUBCOMMAND_SHORT.to_string(),
                    WATCH_INFO_SUBCOMMAND.to_string(),
                ],
            },
            CommandHint {
                short: Some(BACKTRACE_COMMAND_SHORT.to_string()),
                long: BACKTRACE_COMMAND.to_string(),
                subcommands: vec![BACKTRACE_ALL_SUBCOMMAND.to_string()],
            },
            CommandHint {
                short: Some(MEMORY_COMMAND_SHORT.to_string()),
                long: MEMORY_COMMAND.to_string(),
                subcommands: vec![
                    MEMORY_COMMAND_READ_SUBCOMMAND.to_string(),
                    MEMORY_COMMAND_WRITE_SUBCOMMAND.to_string(),
                ],
            },
            CommandHint {
                short: Some(REGISTER_COMMAND_SHORT.to_string()),
                long: REGISTER_COMMAND.to_string(),
                subcommands: vec![
                    REGISTER_COMMAND_READ_SUBCOMMAND.to_string(),
                    REGISTER_COMMAND_WRITE_SUBCOMMAND.to_string(),
                    REGISTER_COMMAND_INFO_SUBCOMMAND.to_string(),
                ],
            },
            (HELP_COMMAND_SHORT, HELP_COMMAND).into(),
            CommandHint {
                short: None,
                long: THREAD_COMMAND.to_string(),
                subcommands: vec![
                    THREAD_COMMAND_INFO_SUBCOMMAND.to_string(),
                    THREAD_COMMAND_SWITCH_SUBCOMMAND.to_string(),
                    THREAD_COMMAND_CURRENT_SUBCOMMAND.to_string(),
                ],
            },
            CommandHint {
                short: None,
                long: SHARED_LIB_COMMAND.to_string(),
                subcommands: vec![SHARED_LIB_COMMAND_INFO_SUBCOMMAND.to_string()],
            },
            CommandHint {
                short: None,
                long: SOURCE_COMMAND.to_string(),
                subcommands: vec![
                    SOURCE_COMMAND_DISASM_SUBCOMMAND.to_string(),
                    SOURCE_COMMAND_FUNCTION_SUBCOMMAND.to_string(),
                ],
            },
            CommandHint {
                short: None,
                long: TRIGGER_COMMAND.to_string(),
                subcommands: vec![
                    TRIGGER_COMMAND_INFO_SUBCOMMAND.to_string(),
                    TRIGGER_COMMAND_ANY_TRIGGER_SUBCOMMAND.to_string(),
                    TRIGGER_COMMAND_BRKPT_TRIGGER_SUBCOMMAND.to_string(),
                    TRIGGER_COMMAND_WP_TRIGGER_SUBCOMMAND.to_string(),
                ],
            },
            CommandHint {
                short: None,
                long: ASYNC_COMMAND.to_string(),
                subcommands: vec![
                    ASYNC_COMMAND_BACKTRACE_SUBCOMMAND.to_string(),
                    ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT.to_string(),
                    ASYNC_COMMAND_TASK_SUBCOMMAND.to_string(),
                    ASYNC_COMMAND_BACKTRACE_SUBCOMMAND.to_string() + " all",
                    ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT.to_string() + " all",
                    ASYNC_COMMAND_STEP_OVER_SUBCOMMAND.to_string(),
                    ASYNC_COMMAND_STEP_OVER_SUBCOMMAND_SHORT.to_string(),
                    ASYNC_COMMAND_STEP_OUT_SUBCOMMAND.to_string(),
                    ASYNC_COMMAND_STEP_OUT_SUBCOMMAND_SHORT.to_string(),
                ],
            },
            CALL_COMMAND.into(),
            CommandHint {
                short: None,
                long: ORACLE_COMMAND.to_string(),
                subcommands: oracles.iter().map(ToString::to_string).collect(),
            },
            ("q", "quit").into(),
        ];

        let h = RLHelper {
            completer: Arc::new(Mutex::new(CommandCompleter::new(commands))),
            hinter: HistoryHinter {},
            colored_prompt: format!("{}", promt.with(Color::DarkGreen)),
        };

        if save_history_in_file {
            let fh = FileHistory::with_config(&config);
            let mut editor = Editor::with_history(config, fh)?;
            editor.set_helper(Some(h));
            _ = editor.load_history(&Self::history_file()?);

            Ok(Self::InFile(editor))
        } else {
            let mut editor = Editor::with_history(config, MemHistory::new())?;
            editor.set_helper(Some(h));
            Ok(Self::InMem(editor))
        }
    }
}

impl Drop for BSEditor {
    fn drop(&mut self) {
        if let Self::InFile(editor) = self
            && let Err(e) = Self::history_file()
                .and_then(|file| editor.save_history(&file).map_err(anyhow::Error::from))
        {
            warn!("Failed to save command history: {e}");
        }
    }
}
