use std::collections::{HashMap, VecDeque};

use anyhow::{Result, bail};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::model::MacroDef;

#[derive(Debug, Clone)]
enum Token {
    Raw(char),
    Command { name: String, args: Vec<String> },
}

#[derive(Debug, Clone)]
struct Program {
    name: String,
    tokens: Vec<Token>,
    labels: HashMap<String, usize>,
}

#[derive(Debug, Clone)]
struct LoopState {
    variable: String,
    current: f64,
    final_value: f64,
    increment: f64,
    body: usize,
    end_label: String,
}

#[derive(Debug, Clone)]
struct Frame {
    program: String,
    pc: usize,
    locals: HashMap<String, String>,
    arg_count: usize,
    loops: Vec<LoopState>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptKind {
    Text,
    Category,
    File,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MenuChoice {
    pub label: String,
    pub target: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MacroAction {
    Key(KeyEvent),
    Prompt {
        kind: PromptKind,
        prompt: String,
        variable: String,
    },
    Menu {
        title: String,
        prompt: String,
        choices: Vec<MenuChoice>,
    },
    Message {
        title: String,
        message: String,
    },
    GetKey {
        variable: String,
    },
    IfKey {
        target: String,
    },
    Selection {
        prompt: String,
    },
    Finished,
}

#[derive(Debug, Clone, Default)]
pub struct MacroContext {
    pub date: String,
    pub time: String,
    pub filename: String,
    pub filepath: String,
    pub view_name: String,
    pub mode: String,
    pub highlight_type: String,
    pub highlight_value: String,
    pub mark_count: usize,
    pub marked_in_view: usize,
    pub clipboard: String,
    pub depth: usize,
    pub key_hit: bool,
    pub prompt: String,
}

pub struct MacroRuntime {
    programs: HashMap<String, Program>,
    frames: Vec<Frame>,
    globals: HashMap<String, String>,
    pending_keys: VecDeque<KeyEvent>,
    waiting: bool,
    steps: usize,
    error_handler: Option<String>,
    break_handler: Option<String>,
    last_error: Option<String>,
}

impl MacroRuntime {
    pub fn new(entry: &str, macros: &[MacroDef], globals: HashMap<String, String>) -> Result<Self> {
        let mut programs = HashMap::new();
        for definition in macros {
            let program = parse(&definition.source)?;
            programs.insert(program.name.to_lowercase(), program);
        }
        let entry = entry.to_lowercase();
        if !programs.contains_key(&entry) {
            bail!("macro “{entry}” does not exist")
        }
        Ok(Self {
            programs,
            frames: vec![Frame {
                program: entry,
                pc: 0,
                locals: HashMap::new(),
                arg_count: 0,
                loops: vec![],
            }],
            globals,
            pending_keys: VecDeque::new(),
            waiting: false,
            steps: 0,
            error_handler: None,
            break_handler: None,
            last_error: None,
        })
    }

    pub fn globals(&self) -> &HashMap<String, String> {
        &self.globals
    }

    pub fn resume_value(&mut self, variable: &str, value: String) -> Result<()> {
        self.set_variable(variable, value)?;
        self.waiting = false;
        Ok(())
    }

    pub fn resume_menu(&mut self, target: &str) -> Result<()> {
        self.jump_to_label(target)?;
        self.waiting = false;
        Ok(())
    }

    pub fn resume_message(&mut self) {
        self.waiting = false;
    }

    pub fn resume_if_key(&mut self, target: &str, key_hit: bool) -> Result<()> {
        if key_hit {
            self.jump_to_label(target)?;
        }
        self.waiting = false;
        Ok(())
    }

    pub fn trigger_break(&mut self) -> Result<bool> {
        let Some(handler) = self.break_handler.clone() else {
            return Ok(false);
        };
        self.push_handler(&handler)?;
        Ok(true)
    }

    pub fn next_action(&mut self, context: &MacroContext) -> Result<Option<MacroAction>> {
        if self.waiting {
            return Ok(None);
        }
        if let Some(key) = self.pending_keys.pop_front() {
            return Ok(Some(MacroAction::Key(key)));
        }
        loop {
            self.steps += 1;
            if self.steps > 100_000 {
                bail!("macro stopped after 100,000 instructions (possible infinite loop)")
            }
            let Some(frame) = self.frames.last_mut() else {
                return Ok(Some(MacroAction::Finished));
            };
            let program = self
                .programs
                .get(&frame.program)
                .ok_or_else(|| anyhow::anyhow!("macro program disappeared"))?;
            let Some(token) = program.tokens.get(frame.pc).cloned() else {
                self.frames.pop();
                continue;
            };
            frame.pc += 1;
            match token {
                Token::Raw(character) => {
                    return Ok(Some(MacroAction::Key(KeyEvent::new(
                        KeyCode::Char(character),
                        KeyModifiers::NONE,
                    ))));
                }
                Token::Command { name, args } => {
                    match self.execute_command(&name, &args, context) {
                        Ok(Some(action)) => return Ok(Some(action)),
                        Ok(None) => {}
                        Err(error) => {
                            let Some(handler) = self.error_handler.clone() else {
                                return Err(error);
                            };
                            self.last_error = Some(format!("{error:#}"));
                            self.push_handler(&handler)?;
                        }
                    }
                }
            }
        }
    }

    fn execute_command(
        &mut self,
        name: &str,
        args: &[String],
        context: &MacroContext,
    ) -> Result<Option<MacroAction>> {
        if let Some(key) = named_key(name) {
            let repeat = args
                .first()
                .map(|value| self.resolve(value, context))
                .transpose()?
                .and_then(|value| value.parse::<usize>().ok())
                .unwrap_or(1)
                .clamp(1, 10_000);
            self.pending_keys.extend(std::iter::repeat_n(key, repeat));
            return Ok(self.pending_keys.pop_front().map(MacroAction::Key));
        }
        match name {
            "COMMENT" | "DEBUGON" | "DEBUGOFF" | "WINDOWSOFF" | "WINDOWSON" | "WINDOWSUPD"
            | "SPEED" => {}
            "DEFSTR" => {
                for variable in args {
                    self.set_variable(variable, String::new())?;
                }
            }
            "DEFINT" | "DEFFLOAT" => {
                for variable in args {
                    self.set_variable(variable, "0".into())?;
                }
            }
            "LET" => {
                require_args(name, args, 2)?;
                let value = self.resolve_expression_or_text(&args[1], context)?;
                self.set_variable(&args[0], value)?;
            }
            "CLEAR" => {
                if args
                    .first()
                    .is_some_and(|value| value.eq_ignore_ascii_case("ALL"))
                {
                    if let Some(frame) = self.frames.last_mut() {
                        frame.locals.clear();
                    }
                    self.globals.clear();
                } else {
                    for variable in args {
                        let current = self.variable(variable, context).unwrap_or_default();
                        self.set_variable(
                            variable,
                            if current.parse::<f64>().is_ok() {
                                "0".into()
                            } else {
                                String::new()
                            },
                        )?;
                    }
                }
            }
            "UNDEF" => {
                for variable in args {
                    if variable.eq_ignore_ascii_case("ALL") {
                        if let Some(frame) = self.frames.last_mut() {
                            frame.locals.clear();
                        }
                        self.globals.clear();
                    } else if variable.starts_with("%%") {
                        self.globals.remove(&variable.to_uppercase());
                    } else if let Some(frame) = self.frames.last_mut() {
                        frame.locals.remove(&variable.to_uppercase());
                    }
                }
            }
            "APPEND" => {
                require_args(name, args, 2)?;
                let mut value = self.variable(&args[0], context).unwrap_or_default();
                value.push_str(&self.resolve(&args[1], context)?);
                self.set_variable(&args[0], value)?;
            }
            "LENGTH" => {
                require_args(name, args, 2)?;
                let length = self.resolve(&args[1], context)?.chars().count();
                self.set_variable(&args[0], length.to_string())?;
            }
            "FIND" => {
                require_args(name, args, 4)?;
                let haystack = self.resolve(&args[1], context)?;
                let needle = self.resolve(&args[2], context)?;
                let start = self
                    .resolve(&args[3], context)?
                    .parse::<usize>()
                    .unwrap_or(1);
                let found = haystack
                    .get(start.saturating_sub(1)..)
                    .and_then(|tail| tail.find(&needle))
                    .map_or(0, |position| position + start);
                self.set_variable(&args[0], found.to_string())?;
            }
            "LEFTSTR" | "RIGHTSTR" | "MIDSTR" => {
                let required = if name == "MIDSTR" { 4 } else { 3 };
                require_args(name, args, required)?;
                let source = self.resolve(&args[1], context)?;
                let characters = source.chars().collect::<Vec<_>>();
                let (start, width) = if name == "MIDSTR" {
                    (
                        self.resolve(&args[2], context)?
                            .parse::<usize>()
                            .unwrap_or(1)
                            .saturating_sub(1),
                        self.resolve(&args[3], context)?
                            .parse::<usize>()
                            .unwrap_or(0),
                    )
                } else {
                    let width = self
                        .resolve(&args[2], context)?
                        .parse::<usize>()
                        .unwrap_or(0);
                    let start = if name == "RIGHTSTR" {
                        characters.len().saturating_sub(width)
                    } else {
                        0
                    };
                    (start, width)
                };
                let result = characters
                    .iter()
                    .skip(start)
                    .take(width)
                    .collect::<String>();
                self.set_variable(&args[0], result)?;
            }
            "TYPE" => {
                require_args(name, args, 1)?;
                let value = self.resolve(&args[0], context)?;
                self.pending_keys.extend(
                    value.chars().map(|character| {
                        KeyEvent::new(KeyCode::Char(character), KeyModifiers::NONE)
                    }),
                );
                return Ok(self.pending_keys.pop_front().map(MacroAction::Key));
            }
            "BRANCH" => {
                require_args(name, args, 1)?;
                self.jump_to_label(&args[0])?;
            }
            "IF" => {
                require_args(name, args, 2)?;
                if self.evaluate_condition(&args[0], context)? {
                    self.jump_to_label(&args[1])?;
                }
            }
            "IFKEY" => {
                require_args(name, args, 1)?;
                self.waiting = true;
                return Ok(Some(MacroAction::IfKey {
                    target: args[0].clone(),
                }));
            }
            "IFEQ" | "IFNOTEQ" => {
                require_args(name, args, 3)?;
                let equal = self.resolve(&args[0], context)? == self.resolve(&args[1], context)?;
                if equal == (name == "IFEQ") {
                    self.jump_to_label(&args[2])?;
                }
            }
            "FOR" => {
                require_args(name, args, 5)?;
                let variable = args[0].clone();
                let initial = self.evaluate_number(&args[1], context)?;
                let final_value = self.evaluate_number(&args[2], context)?;
                let increment = self.evaluate_number(&args[3], context)?;
                if increment == 0.0 {
                    bail!("FOR increment cannot be zero")
                }
                self.set_variable(&variable, format_number(initial))?;
                let body = self.frames.last().map_or(0, |frame| frame.pc);
                self.frames
                    .last_mut()
                    .expect("frame exists")
                    .loops
                    .push(LoopState {
                        variable,
                        current: initial,
                        final_value,
                        increment,
                        body,
                        end_label: args[4].to_lowercase(),
                    });
            }
            "LABEL" => {
                require_args(name, args, 1)?;
                let label = args[0].to_lowercase();
                let repeat = self
                    .frames
                    .last()
                    .and_then(|frame| frame.loops.last())
                    .is_some_and(|loop_state| loop_state.end_label == label);
                if repeat {
                    let (variable, value, body, keep_looping) = {
                        let loop_state = self
                            .frames
                            .last_mut()
                            .and_then(|frame| frame.loops.last_mut())
                            .expect("loop exists");
                        loop_state.current += loop_state.increment;
                        let keep = if loop_state.increment > 0.0 {
                            loop_state.current <= loop_state.final_value
                        } else {
                            loop_state.current >= loop_state.final_value
                        };
                        (
                            loop_state.variable.clone(),
                            format_number(loop_state.current),
                            loop_state.body,
                            keep,
                        )
                    };
                    if keep_looping {
                        self.set_variable(&variable, value)?;
                        self.frames.last_mut().expect("frame exists").pc = body;
                    } else {
                        self.frames.last_mut().expect("frame exists").loops.pop();
                    }
                }
            }
            "CALL" => {
                require_args(name, args, 1)?;
                let target = self.resolve(&args[0], context)?.to_lowercase();
                self.push_call(&target, &args[1..], context)?;
            }
            "GOTO" => {
                require_args(name, args, 1)?;
                let target = self.resolve(&args[0], context)?.to_lowercase();
                self.frames.pop();
                self.push_call(&target, &[], context)?;
            }
            "RETURN" => {
                self.frames.pop();
            }
            "QUIT" => {
                if args
                    .first()
                    .is_some_and(|argument| argument.eq_ignore_ascii_case("ERROR"))
                {
                    bail!("macro requested an error exit")
                }
                self.frames.clear();
            }
            "INPUTTEXT" | "INPUTCAT" | "INPUTFILE" => {
                require_args(name, args, 2)?;
                let prompt = self.resolve(&args[0], context)?;
                self.waiting = true;
                return Ok(Some(MacroAction::Prompt {
                    kind: match name {
                        "INPUTCAT" => PromptKind::Category,
                        "INPUTFILE" => PromptKind::File,
                        _ => PromptKind::Text,
                    },
                    prompt,
                    variable: args[1].clone(),
                }));
            }
            "ALERT" | "LARGEBOX" => {
                require_args(name, args, usize::from(name == "ALERT") + 1)?;
                let (title, message) = if args.len() >= 2 {
                    (
                        self.resolve(&args[0], context)?,
                        self.resolve(&args[1], context)?,
                    )
                } else {
                    ("Macro".into(), self.resolve(&args[0], context)?)
                };
                self.waiting = true;
                return Ok(Some(MacroAction::Message { title, message }));
            }
            "GETKEY" => {
                require_args(name, args, 1)?;
                self.waiting = true;
                return Ok(Some(MacroAction::GetKey {
                    variable: args[0].clone(),
                }));
            }
            "SELECTION" => {
                require_args(name, args, 1)?;
                return Ok(Some(MacroAction::Selection {
                    prompt: self.resolve(&args[0], context)?,
                }));
            }
            "LOTUSMENU" => {
                require_args(name, args, 4)?;
                let count = self
                    .resolve(&args[0], context)?
                    .parse::<usize>()
                    .unwrap_or(0);
                let choices = (0..count)
                    .filter_map(|index| {
                        let offset = 1 + index * 3;
                        Some(MenuChoice {
                            label: self.resolve(args.get(offset)?, context).ok()?,
                            target: args.get(offset + 2)?.to_lowercase(),
                        })
                    })
                    .collect::<Vec<_>>();
                self.waiting = true;
                return Ok(Some(MacroAction::Menu {
                    title: "Macro Menu".into(),
                    prompt: "Choose a command".into(),
                    choices,
                }));
            }
            "ROLLMENU" => {
                require_args(name, args, 5)?;
                let count = self
                    .resolve(&args[2], context)?
                    .parse::<usize>()
                    .unwrap_or(0);
                let choices = (0..count)
                    .filter_map(|index| {
                        let offset = 3 + index * 2;
                        Some(MenuChoice {
                            label: self.resolve(args.get(offset)?, context).ok()?,
                            target: args.get(offset + 1)?.to_lowercase(),
                        })
                    })
                    .collect::<Vec<_>>();
                self.waiting = true;
                return Ok(Some(MacroAction::Menu {
                    title: self.resolve(&args[0], context)?,
                    prompt: self.resolve(&args[1], context)?,
                    choices,
                }));
            }
            "ONERROR" | "ONBREAK" => {
                require_args(name, args, 1)?;
                let handler = self.resolve(&args[0], context)?.to_lowercase();
                if !self.programs.contains_key(&handler) {
                    bail!("handler macro “{handler}” does not exist")
                }
                if name == "ONERROR" {
                    self.error_handler = Some(handler);
                } else {
                    self.break_handler = Some(handler);
                }
            }
            other => bail!("unknown macro command {{{other}}}"),
        }
        Ok(None)
    }

    fn push_call(&mut self, target: &str, args: &[String], context: &MacroContext) -> Result<()> {
        if self.frames.len() >= 64 {
            bail!("macro call depth exceeded 64")
        }
        if !self.programs.contains_key(target) {
            bail!("called macro “{target}” does not exist")
        }
        let mut locals = HashMap::new();
        for (index, argument) in args.iter().enumerate() {
            locals.insert(format!("%{}", index + 1), self.resolve(argument, context)?);
        }
        self.frames.push(Frame {
            program: target.into(),
            pc: 0,
            locals,
            arg_count: args.len(),
            loops: vec![],
        });
        Ok(())
    }

    fn push_handler(&mut self, target: &str) -> Result<()> {
        if self.frames.len() >= 64 {
            bail!("macro call depth exceeded 64")
        }
        if !self.programs.contains_key(target) {
            bail!("handler macro “{target}” does not exist")
        }
        self.frames.push(Frame {
            program: target.into(),
            pc: 0,
            locals: HashMap::new(),
            arg_count: 0,
            loops: vec![],
        });
        Ok(())
    }

    fn jump_to_label(&mut self, label: &str) -> Result<()> {
        let frame = self
            .frames
            .last_mut()
            .ok_or_else(|| anyhow::anyhow!("macro has no active frame"))?;
        let program = self
            .programs
            .get(&frame.program)
            .ok_or_else(|| anyhow::anyhow!("macro program disappeared"))?;
        frame.pc = *program
            .labels
            .get(&label.to_lowercase())
            .ok_or_else(|| anyhow::anyhow!("label “{label}” does not exist"))?;
        Ok(())
    }

    fn set_variable(&mut self, name: &str, value: String) -> Result<()> {
        let name = name.trim().to_uppercase();
        if name.starts_with("%%") {
            self.globals.insert(name, value);
        } else if name.starts_with('%') {
            self.frames
                .last_mut()
                .ok_or_else(|| anyhow::anyhow!("macro has no active frame"))?
                .locals
                .insert(name, value);
        } else {
            bail!("macro variable “{name}” must begin with % or %%")
        }
        Ok(())
    }

    fn variable(&self, name: &str, context: &MacroContext) -> Option<String> {
        let name = name.trim().to_uppercase();
        if name.starts_with("%%") {
            self.globals.get(&name).cloned()
        } else if name.starts_with('%') {
            self.frames.last()?.locals.get(&name).cloned()
        } else if name.starts_with('#') {
            self.special_variable(&name, context)
        } else {
            None
        }
    }

    fn special_variable(&self, name: &str, context: &MacroContext) -> Option<String> {
        Some(match name {
            "#ARGCOUNT" => self.frames.last()?.arg_count.to_string(),
            "#CLIPBOARD" => context.clipboard.clone(),
            "#DATE" => context.date.clone(),
            "#DEPTH" => context.depth.to_string(),
            "#ERRMSG" => self.last_error.clone().unwrap_or_default(),
            "#ERRNO" => usize::from(self.last_error.is_some()).to_string(),
            "#FALSE" => "0".into(),
            "#FILENAME" => context.filename.clone(),
            "#FILEPATH" => context.filepath.clone(),
            "#HIGHLIGHT_TYPE" => context.highlight_type.clone(),
            "#HIGHLIGHT_VALUE" => context.highlight_value.clone(),
            "#KEYHIT" => usize::from(context.key_hit).to_string(),
            "#MARK_COUNT" => context.mark_count.to_string(),
            "#MARKED_IN_VIEW" => context.marked_in_view.to_string(),
            "#MODE" => context.mode.clone(),
            "#NULL" => String::new(),
            "#PROMPT" => context.prompt.clone(),
            "#TIME" => context.time.clone(),
            "#TRUE" => "1".into(),
            "#VIEWNAME" => context.view_name.clone(),
            value if value.starts_with("#ASCII(") && value.ends_with(')') => {
                let number = value[7..value.len() - 1].parse::<u8>().ok()?;
                char::from(number).to_string()
            }
            _ => return None,
        })
    }

    fn resolve(&self, value: &str, context: &MacroContext) -> Result<String> {
        let value = value.trim();
        if (value.starts_with('%') || value.starts_with('#'))
            && !value.contains(char::is_whitespace)
        {
            return self
                .variable(value, context)
                .ok_or_else(|| anyhow::anyhow!("undefined macro variable “{value}”"));
        }
        Ok(value.trim_matches('"').to_owned())
    }

    fn resolve_expression_or_text(&self, value: &str, context: &MacroContext) -> Result<String> {
        if let Ok(number) = self.evaluate_number(value, context) {
            Ok(format_number(number))
        } else {
            self.resolve(value, context)
        }
    }

    fn evaluate_condition(&self, expression: &str, context: &MacroContext) -> Result<bool> {
        let expression = expression
            .trim()
            .strip_prefix('(')
            .and_then(|value| value.strip_suffix(')'))
            .unwrap_or(expression.trim());
        for operator in ["<=", ">=", "<>", "=", "<", ">"] {
            if let Some((left, right)) = expression.split_once(operator) {
                let left_text = self.resolve_expression_or_text(left, context)?;
                let right_text = self.resolve_expression_or_text(right, context)?;
                let numeric = left_text
                    .parse::<f64>()
                    .ok()
                    .zip(right_text.parse::<f64>().ok());
                return Ok(match (operator, numeric) {
                    ("<=", Some((left, right))) => left <= right,
                    (">=", Some((left, right))) => left >= right,
                    ("<", Some((left, right))) => left < right,
                    (">", Some((left, right))) => left > right,
                    ("=", Some((left, right))) => (left - right).abs() < f64::EPSILON,
                    ("<>", Some((left, right))) => (left - right).abs() >= f64::EPSILON,
                    ("=", None) => left_text.eq_ignore_ascii_case(&right_text),
                    ("<>", None) => !left_text.eq_ignore_ascii_case(&right_text),
                    ("<", None) => left_text < right_text,
                    (">", None) => left_text > right_text,
                    ("<=", None) => left_text <= right_text,
                    (">=", None) => left_text >= right_text,
                    _ => false,
                });
            }
        }
        Ok(self.evaluate_number(expression, context)? != 0.0)
    }

    fn evaluate_number(&self, expression: &str, context: &MacroContext) -> Result<f64> {
        NumericParser::new(expression, |name| {
            self.variable(name, context)
                .and_then(|value| value.parse::<f64>().ok())
        })
        .parse()
    }
}

fn parse(source: &str) -> Result<Program> {
    let mut tokens = Vec::new();
    let mut characters = source.chars().peekable();
    let mut line_start = true;
    while let Some(character) = characters.next() {
        if matches!(character, '\r' | '\n') {
            line_start = true;
            continue;
        }
        if line_start && matches!(character, ' ' | '\t') {
            continue;
        }
        line_start = false;
        if character == '{' {
            let mut contents = String::new();
            let mut closed = false;
            for next in characters.by_ref() {
                if next == '}' {
                    closed = true;
                    break;
                }
                contents.push(next);
            }
            if !closed {
                bail!("unterminated macro command")
            }
            let mut parts = contents.split(';').map(|part| part.trim().to_owned());
            let name = parts.next().unwrap_or_default().to_uppercase();
            tokens.push(Token::Command {
                name,
                args: parts.collect(),
            });
        } else {
            tokens.push(Token::Raw(character));
        }
    }
    let Some(Token::Command { name, args }) = tokens.first() else {
        bail!("macro source must start with {{macroname}}")
    };
    if !args.is_empty() || name.is_empty() {
        bail!("first macro entry must be {{macroname}}")
    }
    let macro_name = name.to_lowercase();
    tokens.remove(0);
    let labels = tokens
        .iter()
        .enumerate()
        .filter_map(|(index, token)| match token {
            Token::Command { name, args } if name == "LABEL" => {
                args.first().map(|label| (label.to_lowercase(), index + 1))
            }
            _ => None,
        })
        .collect();
    Ok(Program {
        name: macro_name,
        tokens,
        labels,
    })
}

pub fn macro_name(source: &str) -> Result<String> {
    Ok(parse(source)?.name)
}

pub fn parse_key_binding(binding: &str) -> Result<KeyEvent> {
    let binding = binding.trim();
    let contents = binding
        .strip_prefix('{')
        .and_then(|value| value.strip_suffix('}'))
        .unwrap_or(binding);
    named_key(&contents.to_uppercase())
        .ok_or_else(|| anyhow::anyhow!("unsupported macro key binding “{binding}”"))
}

pub fn key_to_source(key: KeyEvent) -> String {
    if let KeyCode::Char(character) = key.code {
        if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
            return character.to_string();
        }
        if key.modifiers.contains(KeyModifiers::CONTROL) {
            return format!("{{Ctl{}}}", character.to_ascii_uppercase());
        }
        if key.modifiers.contains(KeyModifiers::ALT) {
            return format!("{{Alt{}}}", character.to_ascii_uppercase());
        }
    }
    let modifier = if key.modifiers.contains(KeyModifiers::CONTROL) {
        "Ctl"
    } else if key.modifiers.contains(KeyModifiers::ALT) {
        "Alt"
    } else if key.modifiers.contains(KeyModifiers::SHIFT) {
        "Shf"
    } else {
        ""
    };
    let name = match key.code {
        KeyCode::Enter => "ENTER".into(),
        KeyCode::Esc => "ESC".into(),
        KeyCode::Backspace => "BS".into(),
        KeyCode::Tab => "TAB".into(),
        KeyCode::BackTab => "ShfTAB".into(),
        KeyCode::Up => "UP".into(),
        KeyCode::Down => "DOWN".into(),
        KeyCode::Left => "LEFT".into(),
        KeyCode::Right => "RIGHT".into(),
        KeyCode::Home => "HOME".into(),
        KeyCode::End => "END".into(),
        KeyCode::PageUp => "PGUP".into(),
        KeyCode::PageDown => "PGDN".into(),
        KeyCode::Insert => "INS".into(),
        KeyCode::Delete => "DEL".into(),
        KeyCode::F(number) => format!("{modifier}F{number}"),
        _ => "UNKNOWN".into(),
    };
    format!("{{{name}}}")
}

fn named_key(name: &str) -> Option<KeyEvent> {
    let upper = name.to_uppercase();
    if upper == "SHFTAB" {
        return Some(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
    }
    let (modifiers, base) = if let Some(base) = upper.strip_prefix("CTL") {
        (KeyModifiers::CONTROL, base)
    } else if let Some(base) = upper.strip_prefix("ALT") {
        (KeyModifiers::ALT, base)
    } else if let Some(base) = upper.strip_prefix("SHF") {
        (KeyModifiers::SHIFT, base)
    } else {
        (KeyModifiers::NONE, upper.as_str())
    };
    let code = match base {
        "ENTER" => KeyCode::Enter,
        "ESC" => KeyCode::Esc,
        "BS" => KeyCode::Backspace,
        "TAB" => KeyCode::Tab,
        "UP" => KeyCode::Up,
        "DOWN" => KeyCode::Down,
        "LEFT" => KeyCode::Left,
        "RIGHT" => KeyCode::Right,
        "HOME" => KeyCode::Home,
        "END" => KeyCode::End,
        "PGUP" => KeyCode::PageUp,
        "PGDN" => KeyCode::PageDown,
        "INS" => KeyCode::Insert,
        "DEL" => KeyCode::Delete,
        value if value.starts_with('F') => KeyCode::F(value[1..].parse().ok()?),
        value if value.chars().count() == 1 => {
            KeyCode::Char(value.chars().next()?.to_ascii_lowercase())
        }
        _ => return None,
    };
    Some(KeyEvent::new(code, modifiers))
}

fn require_args(command: &str, args: &[String], count: usize) -> Result<()> {
    if args.len() < count {
        bail!("{{{command}}} requires at least {count} argument(s)")
    }
    Ok(())
}

fn format_number(value: f64) -> String {
    if value.fract().abs() < f64::EPSILON {
        format!("{value:.0}")
    } else {
        value.to_string()
    }
}

struct NumericParser<'a, F> {
    input: &'a [u8],
    position: usize,
    variable: F,
}

impl<'a, F: Fn(&str) -> Option<f64>> NumericParser<'a, F> {
    fn new(input: &'a str, variable: F) -> Self {
        Self {
            input: input.as_bytes(),
            position: 0,
            variable,
        }
    }

    fn parse(mut self) -> Result<f64> {
        let value = self.expression()?;
        self.whitespace();
        if self.position != self.input.len() {
            bail!("invalid numeric expression")
        }
        Ok(value)
    }

    fn expression(&mut self) -> Result<f64> {
        let mut value = self.term()?;
        loop {
            self.whitespace();
            if self.consume(b'+') {
                value += self.term()?;
            } else if self.consume(b'-') {
                value -= self.term()?;
            } else {
                return Ok(value);
            }
        }
    }

    fn term(&mut self) -> Result<f64> {
        let mut value = self.factor()?;
        loop {
            self.whitespace();
            if self.consume(b'*') {
                value *= self.factor()?;
            } else if self.consume(b'/') {
                value /= self.factor()?;
            } else {
                return Ok(value);
            }
        }
    }

    fn factor(&mut self) -> Result<f64> {
        self.whitespace();
        if self.consume(b'(') {
            let value = self.expression()?;
            self.whitespace();
            if !self.consume(b')') {
                bail!("missing closing parenthesis")
            }
            return Ok(value);
        }
        let negative = self.consume(b'-');
        let start = self.position;
        while self.position < self.input.len()
            && (self.input[self.position].is_ascii_alphanumeric()
                || matches!(self.input[self.position], b'%' | b'.' | b'_'))
        {
            self.position += 1;
        }
        if start == self.position {
            bail!("expected number or variable")
        }
        let token = std::str::from_utf8(&self.input[start..self.position])?;
        let value = token
            .parse::<f64>()
            .ok()
            .or_else(|| (self.variable)(token))
            .ok_or_else(|| anyhow::anyhow!("“{token}” is not numeric"))?;
        Ok(if negative { -value } else { value })
    }

    fn whitespace(&mut self) {
        while self
            .input
            .get(self.position)
            .is_some_and(u8::is_ascii_whitespace)
        {
            self.position += 1;
        }
    }

    fn consume(&mut self, expected: u8) -> bool {
        if self.input.get(self.position) == Some(&expected) {
            self.position += 1;
            true
        } else {
            false
        }
    }
}

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

    fn definition(source: &str) -> MacroDef {
        MacroDef {
            id: 1,
            name: macro_name(source).unwrap(),
            source: source.into(),
            key_binding: String::new(),
        }
    }

    #[test]
    fn parses_and_runs_the_original_search_example() {
        let source = "{searchmac}\n{inputtext;Enter text to search for;%search}\n\
                      {altf6}\n{type;%search}\n{enter;2}";
        let mut runtime =
            MacroRuntime::new("searchmac", &[definition(source)], HashMap::new()).unwrap();
        let context = MacroContext::default();
        let Some(MacroAction::Prompt { variable, .. }) = runtime.next_action(&context).unwrap()
        else {
            panic!("expected prompt");
        };
        runtime.resume_value(&variable, "quarterly".into()).unwrap();
        assert_eq!(
            runtime.next_action(&context).unwrap(),
            Some(MacroAction::Key(KeyEvent::new(
                KeyCode::F(6),
                KeyModifiers::ALT
            )))
        );
        let mut typed = String::new();
        for _ in 0..9 {
            let Some(MacroAction::Key(key)) = runtime.next_action(&context).unwrap() else {
                panic!("expected typed key");
            };
            if let KeyCode::Char(character) = key.code {
                typed.push(character);
            }
        }
        assert_eq!(typed, "quarterly");
    }

    #[test]
    fn supports_variables_arithmetic_loops_and_branches() {
        let source = "{math}{defint;%i;%sum}{let;%sum;0}{for;%i;1;3;1;done}\
                      {let;%sum;(%sum+%i)}{label;done}{if;(%sum=6);good}\
                      x{quit}{label;good}y";
        let mut runtime = MacroRuntime::new("math", &[definition(source)], HashMap::new()).unwrap();
        let context = MacroContext::default();
        let Some(MacroAction::Key(key)) = runtime.next_action(&context).unwrap() else {
            panic!("expected result key");
        };
        assert_eq!(key.code, KeyCode::Char('y'));
    }

    #[test]
    fn records_and_parses_key_bindings_in_agenda_notation() {
        let key = KeyEvent::new(KeyCode::F(3), KeyModifiers::ALT);
        assert_eq!(key_to_source(key), "{AltF3}");
        assert_eq!(
            parse_key_binding("{CtlA}").unwrap().code,
            KeyCode::Char('a')
        );
        assert_eq!(
            parse_key_binding("{CtlA}").unwrap().modifiers,
            KeyModifiers::CONTROL
        );
        assert_eq!(
            parse_key_binding("{CtlPgUp}").unwrap(),
            KeyEvent::new(KeyCode::PageUp, KeyModifiers::CONTROL)
        );
        assert_eq!(
            parse_key_binding("{ShfTab}").unwrap(),
            KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT)
        );
    }

    #[test]
    fn supports_ifkey_special_ascii_and_setting_selection() {
        let source = "{control}{ifkey;pressed}x{quit}{label;pressed}\
                      {type;#ascii(65)}{selection;Priority}";
        let mut runtime =
            MacroRuntime::new("control", &[definition(source)], HashMap::new()).unwrap();
        let context = MacroContext::default();
        let Some(MacroAction::IfKey { target }) = runtime.next_action(&context).unwrap() else {
            panic!("expected IFKEY check");
        };
        runtime.resume_if_key(&target, true).unwrap();
        let Some(MacroAction::Key(key)) = runtime.next_action(&context).unwrap() else {
            panic!("expected #ASCII output");
        };
        assert_eq!(key.code, KeyCode::Char('A'));
        assert_eq!(
            runtime.next_action(&context).unwrap(),
            Some(MacroAction::Selection {
                prompt: "Priority".into()
            })
        );
    }

    #[test]
    fn onerror_and_onbreak_invoke_handler_macros() {
        let main = definition("{main}{onerror;handler}{unknown}x");
        let mut handler = definition("{handler}y{return}");
        handler.id = 2;
        let context = MacroContext::default();
        let mut runtime =
            MacroRuntime::new("main", &[main.clone(), handler.clone()], HashMap::new()).unwrap();
        let Some(MacroAction::Key(key)) = runtime.next_action(&context).unwrap() else {
            panic!("expected error handler output");
        };
        assert_eq!(key.code, KeyCode::Char('y'));
        let Some(MacroAction::Key(key)) = runtime.next_action(&context).unwrap() else {
            panic!("expected main macro to resume");
        };
        assert_eq!(key.code, KeyCode::Char('x'));

        let mut break_main = definition("{breakmain}{onbreak;handler}x");
        break_main.id = 3;
        let mut runtime =
            MacroRuntime::new("breakmain", &[break_main, handler], HashMap::new()).unwrap();
        assert_eq!(
            runtime.next_action(&context).unwrap().unwrap(),
            MacroAction::Key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE))
        );
        assert!(runtime.trigger_break().unwrap());
        let Some(MacroAction::Key(key)) = runtime.next_action(&context).unwrap() else {
            panic!("expected break handler output");
        };
        assert_eq!(key.code, KeyCode::Char('y'));
    }
}
