#![feature(rustc_private, if_let_guard)]
#![allow(clippy::new_without_default)]

use std::{
    cell::RefCell,
    env,
    io::{self, IsTerminal},
    path::{Path, PathBuf},
    rc::Rc,
};

use libterm::TermKey;

use error::Error;
use lysp::{
    error::MachineError,
    vm::{
        Value,
        env::{Environment, EnvironmentAccessHook},
        machine::Machine,
        value::{
            IdentifierValue, StringValue,
            convert::{AnyFunction, TryFromValue},
        },
    },
};

use crate::{config::EditorConfig, script::AsValue, state::State};

pub mod buffer;
pub mod config;
pub mod error;
pub mod highlight;
pub mod keymap;
pub mod state;
pub mod text;

mod script;

#[derive(Clone, Copy, PartialEq)]
pub enum TopMode {
    Normal,
    Command,
}

pub struct Editor {
    machine: Machine,
    state: Rc<RefCell<State>>,
    env: Rc<Environment>,
}

impl Editor {
    pub fn new<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
        #[allow(unused)]
        struct ConfigAccessHook(Rc<RefCell<State>>, Rc<RefCell<EditorConfig>>);

        impl EnvironmentAccessHook for ConfigAccessHook {
            fn read_variable(&mut self, name: &str) -> Option<Value> {
                let config = self.1.borrow();

                match name {
                    "red/number" => Some((*config.number).into()),
                    "red/bottom-margin" => Some((*config.bottom_margin).into()),
                    _ => None,
                }
            }
            fn write_variable(
                &mut self,
                name: &IdentifierValue,
                value: &Value,
            ) -> Result<bool, MachineError> {
                match name.as_ref() {
                    "red/number" => {
                        let mut config = self.1.borrow_mut();
                        *config.number = value.is_trueish();
                        Ok(true)
                    }
                    "red/bottom-margin" => {
                        let value = usize::try_from_value(value)?;
                        let mut config = self.1.borrow_mut();
                        *config.bottom_margin = value;
                        Ok(true)
                    }
                    _ => Ok(false),
                }
            }
        }

        let config = Rc::new(RefCell::new(EditorConfig::default()));
        let state = Rc::new(RefCell::new(State::open(path, config.clone())?));
        let machine = Machine::default();
        let env = Rc::new(Environment::default());
        env.set_access_hook(Some(Box::new(ConfigAccessHook(state.clone(), config))));

        Ok(Self {
            state,
            machine,
            env,
        })
    }

    pub fn defun<S, F>(&self, name: S, function: F)
    where
        S: Into<IdentifierValue>,
        F: Fn(
                &mut Machine,
                &Rc<Environment>,
                &Rc<RefCell<State>>,
                &[Value],
            ) -> Result<Value, Error>
            + 'static,
    {
        let state = self.state.clone();
        self.env.defun_native(name, "", move |vm, env, args| {
            match function(vm, env, &state, args) {
                Ok(value) => Ok(value),
                Err(error) => Err(MachineError::Custom(error.into())),
            }
        });
    }
    pub fn defmacro<S, F>(&self, name: S, function: F)
    where
        S: Into<IdentifierValue>,
        F: Fn(
                &mut Machine,
                &Rc<Environment>,
                &Rc<RefCell<State>>,
                &[Value],
            ) -> Result<Value, Error>
            + 'static,
    {
        let state = self.state.clone();
        self.env.defmacro_native(name, "", move |vm, env, args| {
            match function(vm, env, &state, args) {
                Ok(value) => Ok(value),
                Err(error) => Err(MachineError::Custom(error.into())),
            }
        });
    }

    pub fn evaluate_file<P: AsRef<Path>>(&mut self, path: P) {
        let path = path.as_ref();
        if let Err(error) = self.machine.load_file(Default::default(), &self.env, path) {
            log::error!("{}: {}", path.display(), error);
            self.env
                .set_global_value(
                    "_red/current-message",
                    Value::String(format!("{}: {}", path.display(), error).into()),
                )
                .ok();
        }
    }

    pub fn evaluate_callback(&mut self, function: &AnyFunction, args: &[Value]) {
        let name = function.name();
        if let Err(error) = function.invoke(&mut self.machine, &self.env, args) {
            log::error!("{name}: {error}");
            self.env
                .set_global_value(
                    "_red/current-message",
                    Value::String(format!("{name}: {error}").into()),
                )
                .ok();
        }
    }

    pub fn run(&mut self) -> Result<(), Error> {
        script::setup_env(self);

        let runtime_directory: PathBuf;
        #[cfg(feature = "runtime")]
        {
            runtime_directory = PathBuf::from("runtime").canonicalize().unwrap();
        }
        #[cfg(any(not(feature = "runtime"), rust_analyzer))]
        {
            runtime_directory = "/usr/share/red/runtime".into();
        }

        self.env
            .set_global_value(
                "red/runtime-directory",
                StringValue::from(format!("{}", runtime_directory.display())),
            )
            .ok();
        self.evaluate_file(runtime_directory.join("core.lysp"));

        loop {
            let exited = self.state.borrow().exited();

            if exited {
                break;
            }

            // TODO pre-render hook
            let (width, height) = self.state.borrow_mut().display()?;

            let post_render_hook = self.state.borrow().post_render_hook.clone();
            if let Some(post_render_hook) = post_render_hook {
                self.evaluate_callback(&post_render_hook, &[width.into(), height.into()]);
            }

            self.state.borrow_mut().finish_display()?;

            let key = self.state.borrow_mut().wait_for_events()?;

            if let TermKey::Char('Q') = key {
                break;
            }

            let (key_hook, top_mode, buffer_mode) = {
                let state = self.state.borrow();
                let (top_mode, buffer_mode) = state.mode();
                let key_hook = state.key_hook.clone();

                (key_hook, top_mode, buffer_mode)
            };

            if let Some(key_hook) = key_hook {
                let key = key.as_value();
                let top_mode = top_mode.as_value();
                let buffer_mode = buffer_mode.as_value();
                self.evaluate_callback(
                    &key_hook,
                    &[top_mode.into(), buffer_mode.into(), key.into()],
                );
            }
        }
        Ok(())
    }
}

fn main() {
    logsink::setup_logging(false);

    let args = env::args().collect::<Vec<_>>();
    if args.len() > 2 {
        eprintln!("Usage: red [FILE]");
        return;
    }

    if !io::stdin().is_terminal() {
        eprintln!("Not a tty");
        return;
    }

    let path = args.get(1);
    let mut editor = Editor::new(path).unwrap();
    let result = editor.run();
    editor.state.borrow_mut().cleanup();

    if let Err(error) = result {
        eprintln!("Error: {error}");
    }
}
