#![feature(rustc_private)]
// #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
#![allow(clippy::new_without_default)]

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

use libterm::TermKey;

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

use crate::{command::Action, config::EditorConfig, state::State};

pub mod buffer;
pub mod command;
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()),
                    _ => None,
                }
            }
            fn write_variable(&mut self, name: IdentifierValue, value: Value) -> bool {
                match name.as_ref() {
                    "red/number" => {
                        let mut config = self.1.borrow_mut();
                        *config.number = value.is_trueish();
                        true
                    }
                    _ => 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) {
            self.state
                .borrow_mut()
                .set_message(format!("{}: {}", path.display(), error));
        }
    }

    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) {
            self.state
                .borrow_mut()
                .set_message(format!("{name}: {error}"));
        }
    }

    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())),
        );
        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 action = self.state.borrow_mut().handle_key(key);

            match action {
                Action::Handler(handler) => {
                    self.evaluate_callback(&handler, &[]);
                }
                Action::Command(command) => {
                    let hook = self.state.borrow().command_hook.clone();
                    if let Some(hook) = hook {
                        let words = command
                            .split(' ')
                            .map(Into::into)
                            .map(Value::String)
                            .collect::<Vec<_>>();

                        self.evaluate_callback(&hook, &words);
                    }
                }
                Action::None => (),
            };
        }
        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}");
    }
}
