use std::ops::RangeInclusive;

use crate::{
    buffer::{Buffer, SetMode},
    config::Config,
    error::Error,
    State,
};

pub type CommandFn = fn(&mut State, &[&str]) -> Result<(), Error>;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
    // Editing
    EraseBackward,
    InsertBefore,
    InsertAfter,
    NewlineBefore,
    NewlineAfter,
    BreakLine,
    KillLine,

    // Movement
    MoveFirstLine,
    MoveLastLine,
    MoveCharPrev,
    MoveCharNext,
    MoveLineBack(usize),
    MoveLineForward(usize),
    MoveLineStart,
    MoveLineEnd,
}

static COMMANDS: &[(&str, RangeInclusive<usize>, CommandFn)] = &[
    ("w", 0..=1, cmd_write),
    ("w!", 0..=1, cmd_force_write),
    ("q", 0..=0, cmd_exit),
    ("q!", 0..=0, cmd_force_exit),
    ("e", 1..=1, cmd_edit),
    ("e!", 0..=1, cmd_force_edit),
];

// Commands
fn cmd_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
    if args.len() == 1 && state.buffer().is_modified() && state.buffer().path().is_some() {
        return Err(Error::UnsavedBuffer(
            "Use :w! FILE to force write to another file",
        ));
    }

    cmd_force_write(state, args)
}

fn cmd_force_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
    let buffer = state.buffer_mut();
    if let Some(&path) = args.first() {
        buffer.set_path(path);
    }

    buffer.save()?;

    if let Some(name) = buffer.name() {
        let status = format!("{:?} written", name);
        state.set_status(status);
    }

    Ok(())
}

fn cmd_edit(state: &mut State, args: &[&str]) -> Result<(), Error> {
    if state.buffer().is_modified() {
        return Err(Error::UnsavedBuffer("Use :e! [FILE] to open another file"));
    }

    cmd_force_edit(state, args)
}

fn cmd_force_edit(state: &mut State, args: &[&str]) -> Result<(), Error> {
    if let Some(&path) = args.first() {
        state.buffer_mut().reopen(path).map_err(Error::OpenError)
    } else if let Some(path) = state.buffer().path().cloned() {
        state.buffer_mut().reopen(path).map_err(Error::OpenError)
    } else {
        Err(Error::NoPath)
    }
}

fn cmd_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
    let buffer = state.buffer();

    if buffer.is_modified() {
        return Err(Error::UnsavedBuffer("Use :q! to force exit"));
    }

    state.exit();
    Ok(())
}

fn cmd_force_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
    state.exit();
    Ok(())
}

pub fn execute(state: &mut State, command: String) -> Result<(), Error> {
    let words = command.split(' ').collect::<Vec<_>>();
    let Some((&cmd, args)) = words.split_first() else {
        return Ok(());
    };

    for (name, nargs, f) in COMMANDS.iter() {
        if *name == cmd {
            if !nargs.contains(&args.len()) {
                todo!();
            }

            return f(state, args);
        }
    }

    Err(Error::UnknownCommand(cmd.into()))
}

pub fn perform(buffer: &mut Buffer, config: &Config, action: Action) -> Result<(), Error> {
    match action {
        // Editing
        Action::EraseBackward => buffer.erase_backward(config),
        Action::InsertBefore => buffer.set_mode(config, SetMode::InsertBefore),
        Action::InsertAfter => buffer.set_mode(config, SetMode::InsertAfter),
        Action::NewlineBefore => buffer.newline_before(),
        Action::NewlineAfter => buffer.newline_after(false),
        Action::BreakLine => buffer.newline_after(true),
        Action::KillLine => buffer.kill_line(config),
        // Movement
        Action::MoveCharPrev => buffer.move_cursor(config, -1, 0),
        Action::MoveCharNext => buffer.move_cursor(config, 1, 0),
        Action::MoveLineBack(count) => buffer.move_cursor(config, 0, -(count as isize)),
        Action::MoveLineForward(count) => buffer.move_cursor(config, 0, count as isize),
        Action::MoveLineStart => buffer.set_column(config, 0),
        Action::MoveLineEnd => buffer.to_line_end(config),
        Action::MoveFirstLine => buffer.to_first_line(config),
        Action::MoveLastLine => buffer.to_last_line(config),
    }
    Ok(())
}
