#![allow(clippy::len_zero, clippy::single_char_pattern, clippy::collapsible_if)]

mod config;
mod toml;

use {
    crate::config::{
        parse_config, Action, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
        ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch,
        SimpleCommand, Status, Theme,
    },
    ahash::{AHashMap, AHashSet},
    error_reporter::Report,
    jay_config::{
        config, config_dir,
        exec::{set_env, unset_env, Command},
        get_workspace,
        input::{get_seat, input_devices, on_new_input_device, InputDevice, Seat},
        is_reload,
        keyboard::{Keymap, ModifiedKeySym},
        logging::set_log_level,
        on_devices_enumerated, on_idle, quit, reload, set_default_workspace_capture, set_idle,
        status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
        switch_to_vt,
        theme::{reset_colors, reset_font, reset_sizes, set_font},
        video::{
            connectors, drm_devices, on_connector_connected, on_graphics_initialized,
            on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api,
            Connector, DrmDevice,
        },
    },
    std::{cell::RefCell, io::ErrorKind, path::PathBuf, rc::Rc},
};

fn default_seat() -> Seat {
    get_seat("default")
}

impl Action {
    fn into_fn(self, state: &Rc<State>) -> Box<dyn FnMut()> {
        let s = state.persistent.seat;
        match self {
            Action::SimpleCommand { cmd } => match cmd {
                SimpleCommand::Focus(dir) => Box::new(move || s.focus(dir)),
                SimpleCommand::Move(dir) => Box::new(move || s.move_(dir)),
                SimpleCommand::Split(axis) => Box::new(move || s.create_split(axis)),
                SimpleCommand::ToggleSplit => Box::new(move || s.toggle_split()),
                SimpleCommand::ToggleMono => Box::new(move || s.toggle_mono()),
                SimpleCommand::ToggleFullscreen => Box::new(move || s.toggle_fullscreen()),
                SimpleCommand::FocusParent => Box::new(move || s.focus_parent()),
                SimpleCommand::Close => Box::new(move || s.close()),
                SimpleCommand::DisablePointerConstraint => {
                    Box::new(move || s.disable_pointer_constraint())
                }
                SimpleCommand::ToggleFloating => Box::new(move || s.toggle_floating()),
                SimpleCommand::Quit => Box::new(quit),
                SimpleCommand::ReloadConfigToml => {
                    let persistent = state.persistent.clone();
                    Box::new(move || load_config(false, &persistent))
                }
                SimpleCommand::ReloadConfigSo => Box::new(reload),
                SimpleCommand::None => Box::new(|| ()),
            },
            Action::Multi { actions } => {
                let mut actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
                Box::new(move || {
                    for action in &mut actions {
                        action();
                    }
                })
            }
            Action::Exec { exec } => Box::new(move || create_command(&exec).spawn()),
            Action::SwitchToVt { num } => Box::new(move || switch_to_vt(num)),
            Action::ShowWorkspace { name } => {
                let workspace = get_workspace(&name);
                Box::new(move || s.show_workspace(workspace))
            }
            Action::MoveToWorkspace { name } => {
                let workspace = get_workspace(&name);
                Box::new(move || s.set_workspace(workspace))
            }
            Action::ConfigureConnector { con } => Box::new(move || {
                for c in connectors() {
                    if con.match_.matches(c) {
                        con.apply(c);
                    }
                }
            }),
            Action::ConfigureInput { input } => {
                let state = state.clone();
                Box::new(move || {
                    for c in input_devices() {
                        if input.match_.matches(c, &state) {
                            input.apply(c);
                        }
                    }
                })
            }
            Action::ConfigureOutput { out } => {
                let state = state.clone();
                Box::new(move || {
                    for c in connectors() {
                        if out.match_.matches(c, &state) {
                            out.apply(c);
                        }
                    }
                })
            }
            Action::SetEnv { env } => Box::new(move || {
                for (k, v) in &env {
                    set_env(k, v);
                }
            }),
            Action::UnsetEnv { env } => Box::new(move || {
                for k in &env {
                    unset_env(k);
                }
            }),
            Action::SetKeymap { map } => {
                let state = state.clone();
                Box::new(move || state.set_keymap(&map))
            }
            Action::SetStatus { status } => {
                let state = state.clone();
                Box::new(move || state.set_status(&status))
            }
            Action::SetTheme { theme } => {
                let state = state.clone();
                Box::new(move || state.apply_theme(&theme))
            }
            Action::SetLogLevel { level } => Box::new(move || set_log_level(level)),
            Action::SetGfxApi { api } => Box::new(move || set_gfx_api(api)),
            Action::ConfigureDirectScanout { enabled } => {
                Box::new(move || set_direct_scanout_enabled(enabled))
            }
            Action::ConfigureDrmDevice { dev } => {
                let state = state.clone();
                Box::new(move || {
                    for d in drm_devices() {
                        if dev.match_.matches(d, &state) {
                            dev.apply(d);
                        }
                    }
                })
            }
            Action::SetRenderDevice { dev } => {
                let state = state.clone();
                Box::new(move || {
                    for d in drm_devices() {
                        if dev.matches(d, &state) {
                            d.make_render_device();
                        }
                    }
                })
            }
            Action::ConfigureIdle { idle } => Box::new(move || set_idle(Some(idle))),
            Action::MoveToOutput { output, workspace } => {
                let state = state.clone();
                Box::new(move || {
                    let output = 'get_output: {
                        for connector in connectors() {
                            if connector.connected() && output.matches(connector, &state) {
                                break 'get_output connector;
                            }
                        }
                        return;
                    };
                    match workspace {
                        Some(ws) => ws.move_to_output(output),
                        None => s.move_to_output(output),
                    }
                })
            }
        }
    }
}

fn apply_recursive_match<'a, U>(
    type_name: &str,
    list: &'a AHashMap<String, U>,
    active: &mut AHashSet<&'a str>,
    name: &'a str,
    matches: impl FnOnce(&'a U, &mut AHashSet<&'a str>) -> bool,
) -> bool {
    match list.get(name) {
        None => {
            log::warn!("{type_name} with name {name} does not exist");
            false
        }
        Some(m) => {
            if active.insert(name) {
                let matches = matches(m, active);
                active.remove(name);
                matches
            } else {
                log::warn!("Recursion while evaluating match for {type_name} {name}");
                false
            }
        }
    }
}

impl ConfigDrmDevice {
    fn apply(&self, d: DrmDevice) {
        if let Some(api) = self.gfx_api {
            d.set_gfx_api(api);
        }
        if let Some(dse) = self.direct_scanout_enabled {
            d.set_direct_scanout_enabled(dse);
        }
    }
}

impl DrmDeviceMatch {
    fn matches(&self, d: DrmDevice, state: &State) -> bool {
        self.matches_(d, state, &mut AHashSet::new())
    }

    fn matches_<'a>(
        &'a self,
        d: DrmDevice,
        state: &'a State,
        active: &mut AHashSet<&'a str>,
    ) -> bool {
        match self {
            DrmDeviceMatch::Any(m) => m.iter().any(|m| m.matches_(d, state, active)),
            DrmDeviceMatch::All {
                name,
                syspath,
                vendor,
                vendor_name,
                model,
                model_name,
                devnode,
            } => {
                if let Some(name) = name {
                    let matches = apply_recursive_match(
                        "drm device",
                        &state.drm_devices,
                        active,
                        name,
                        |m, active| m.matches_(d, state, active),
                    );
                    if !matches {
                        return false;
                    }
                }
                if let Some(syspath) = syspath {
                    if d.syspath() != *syspath {
                        return false;
                    }
                }
                if let Some(devnode) = devnode {
                    if d.devnode() != *devnode {
                        return false;
                    }
                }
                if let Some(model) = model_name {
                    if d.model() != *model {
                        return false;
                    }
                }
                if let Some(vendor) = vendor_name {
                    if d.vendor() != *vendor {
                        return false;
                    }
                }
                if let Some(vendor) = vendor {
                    if d.pci_id().vendor != *vendor {
                        return false;
                    }
                }
                if let Some(model) = model {
                    if d.pci_id().model != *model {
                        return false;
                    }
                }
                true
            }
        }
    }
}

impl InputMatch {
    fn matches(&self, d: InputDevice, state: &State) -> bool {
        self.matches_(d, state, &mut AHashSet::new())
    }

    fn matches_<'a>(
        &'a self,
        d: InputDevice,
        state: &'a State,
        active: &mut AHashSet<&'a str>,
    ) -> bool {
        match self {
            InputMatch::Any(m) => m.iter().any(|m| m.matches_(d, state, active)),
            InputMatch::All {
                tag,
                name,
                syspath,
                devnode,
                is_keyboard,
                is_pointer,
                is_touch,
                is_tablet_tool,
                is_tablet_pad,
                is_gesture,
                is_switch,
            } => {
                if let Some(name) = name {
                    if d.name() != *name {
                        return false;
                    }
                }
                if let Some(tag) = tag {
                    let matches = apply_recursive_match(
                        "input device",
                        &state.input_devices,
                        active,
                        tag,
                        |m, active| m.matches_(d, state, active),
                    );
                    if !matches {
                        return false;
                    }
                }
                if let Some(syspath) = syspath {
                    if d.syspath() != *syspath {
                        return false;
                    }
                }
                if let Some(devnode) = devnode {
                    if d.devnode() != *devnode {
                        return false;
                    }
                }
                macro_rules! check_cap {
                    ($is:expr, $cap:ident) => {
                        if let Some(is) = *$is {
                            if d.has_capability(jay_config::input::capability::$cap) != is {
                                return false;
                            }
                        }
                    };
                }
                check_cap!(is_keyboard, CAP_KEYBOARD);
                check_cap!(is_pointer, CAP_POINTER);
                check_cap!(is_touch, CAP_TOUCH);
                check_cap!(is_tablet_tool, CAP_TABLET_TOOL);
                check_cap!(is_tablet_pad, CAP_TABLET_PAD);
                check_cap!(is_gesture, CAP_GESTURE);
                check_cap!(is_switch, CAP_SWITCH);
                true
            }
        }
    }
}

impl Input {
    fn apply(&self, c: InputDevice) {
        if let Some(v) = self.accel_profile {
            c.set_accel_profile(v);
        }
        if let Some(v) = self.accel_speed {
            c.set_accel_speed(v);
        }
        if let Some(v) = self.tap_enabled {
            c.set_tap_enabled(v);
        }
        if let Some(v) = self.tap_drag_enabled {
            c.set_drag_enabled(v);
        }
        if let Some(v) = self.tap_drag_lock_enabled {
            c.set_drag_lock_enabled(v);
        }
        if let Some(v) = self.left_handed {
            c.set_left_handed(v);
        }
        if let Some(v) = self.natural_scrolling {
            c.set_natural_scrolling_enabled(v);
        }
        if let Some(v) = self.px_per_wheel_scroll {
            c.set_px_per_wheel_scroll(v);
        }
        if let Some(v) = self.transform_matrix {
            c.set_transform_matrix(v);
        }
    }
}

impl OutputMatch {
    fn matches(&self, c: Connector, state: &State) -> bool {
        if !c.connected() {
            return false;
        }
        self.matches_(c, state, &mut AHashSet::new())
    }

    fn matches_<'a>(
        &'a self,
        c: Connector,
        state: &'a State,
        active: &mut AHashSet<&'a str>,
    ) -> bool {
        match self {
            OutputMatch::Any(m) => m.iter().any(|m| m.matches_(c, state, active)),
            OutputMatch::All {
                name,
                connector,
                serial_number,
                manufacturer,
                model,
            } => {
                if let Some(name) = name {
                    let matches = apply_recursive_match(
                        "output",
                        &state.outputs,
                        active,
                        name,
                        |m, active| m.matches_(c, state, active),
                    );
                    if !matches {
                        return false;
                    }
                }
                if let Some(connector) = &connector {
                    if c.name() != *connector {
                        return false;
                    }
                }
                if let Some(serial_number) = &serial_number {
                    if c.serial_number() != *serial_number {
                        return false;
                    }
                }
                if let Some(manufacturer) = &manufacturer {
                    if c.manufacturer() != *manufacturer {
                        return false;
                    }
                }
                if let Some(model) = &model {
                    if c.model() != *model {
                        return false;
                    }
                }
                true
            }
        }
    }
}

impl ConnectorMatch {
    fn matches(&self, c: Connector) -> bool {
        if !c.exists() {
            return false;
        }
        match self {
            ConnectorMatch::Any(m) => m.iter().any(|m| m.matches(c)),
            ConnectorMatch::All { connector } => {
                if let Some(connector) = &connector {
                    if c.name() != *connector {
                        return false;
                    }
                }
                true
            }
        }
    }
}

impl ConfigConnector {
    fn apply(&self, c: Connector) {
        c.set_enabled(self.enabled);
    }
}

impl Output {
    fn apply(&self, c: Connector) {
        if self.x.is_some() || self.y.is_some() {
            let (old_x, old_y) = c.position();
            c.set_position(self.x.unwrap_or(old_x), self.y.unwrap_or(old_y));
        }
        if let Some(scale) = self.scale {
            c.set_scale(scale);
        }
        if let Some(transform) = self.transform {
            c.set_transform(transform);
        }
        if let Some(mode) = &self.mode {
            let modes = c.modes();
            let m = modes.iter().find(|m| {
                if m.width() != mode.width || m.height() != mode.height {
                    return false;
                }
                match mode.refresh_rate {
                    None => true,
                    Some(rr) => m.refresh_rate() as f64 / 1000.0 == rr,
                }
            });
            match m {
                None => {
                    log::warn!("Output {} does not support mode {mode}", c.name());
                }
                Some(m) => c.set_mode(m.width(), m.height(), Some(m.refresh_rate())),
            }
        }
    }
}

struct State {
    outputs: AHashMap<String, OutputMatch>,
    drm_devices: AHashMap<String, DrmDeviceMatch>,
    input_devices: AHashMap<String, InputMatch>,
    persistent: Rc<PersistentState>,
    keymaps: AHashMap<String, Keymap>,
}

impl Drop for State {
    fn drop(&mut self) {
        for keymap in self.keymaps.values() {
            keymap.destroy();
        }
    }
}

impl State {
    fn unbind_all(&self) {
        let mut binds = self.persistent.binds.borrow_mut();
        for bind in binds.drain() {
            self.persistent.seat.unbind(bind);
        }
    }

    fn apply_shortcuts(
        self: &Rc<Self>,
        shortcuts: impl IntoIterator<Item = (ModifiedKeySym, Action)>,
    ) {
        let mut binds = self.persistent.binds.borrow_mut();
        for (key, value) in shortcuts {
            if let Action::SimpleCommand {
                cmd: SimpleCommand::None,
            } = value
            {
                self.persistent.seat.unbind(key);
                binds.remove(&key);
            } else {
                self.persistent.seat.bind(key, value.into_fn(self));
                binds.insert(key);
            }
        }
    }

    fn set_keymap(&self, map: &ConfigKeymap) {
        let map = match map {
            ConfigKeymap::Named(n) => match self.keymaps.get(n) {
                None => {
                    log::warn!("Unknown keymap {n}");
                    return;
                }
                Some(m) => *m,
            },
            ConfigKeymap::Defined { map, .. } => *map,
            ConfigKeymap::Literal(map) => *map,
        };
        self.persistent.seat.set_keymap(map);
    }

    fn set_status(&self, status: &Option<Status>) {
        set_status("");
        match status {
            None => unset_status_command(),
            Some(s) => {
                set_i3bar_separator(s.separator.as_deref().unwrap_or(" | "));
                set_status_command(s.format, create_command(&s.exec))
            }
        }
    }

    fn apply_theme(&self, theme: &Theme) {
        use jay_config::theme::{colors::*, sized::*};
        macro_rules! color {
            ($colorable:ident, $field:ident) => {
                if let Some(color) = theme.$field {
                    $colorable.set_color(color)
                }
            };
        }
        color!(
            ATTENTION_REQUESTED_BACKGROUND_COLOR,
            attention_requested_bg_color
        );
        color!(BACKGROUND_COLOR, bg_color);
        color!(BAR_BACKGROUND_COLOR, bar_bg_color);
        color!(BAR_STATUS_TEXT_COLOR, bar_status_text_color);
        color!(BORDER_COLOR, border_color);
        color!(
            CAPTURED_FOCUSED_TITLE_BACKGROUND_COLOR,
            captured_focused_title_bg_color
        );
        color!(
            CAPTURED_UNFOCUSED_TITLE_BACKGROUND_COLOR,
            captured_unfocused_title_bg_color
        );
        color!(
            FOCUSED_INACTIVE_TITLE_BACKGROUND_COLOR,
            focused_inactive_title_bg_color
        );
        color!(
            FOCUSED_INACTIVE_TITLE_TEXT_COLOR,
            focused_inactive_title_text_color
        );
        color!(FOCUSED_TITLE_BACKGROUND_COLOR, focused_title_bg_color);
        color!(FOCUSED_TITLE_TEXT_COLOR, focused_title_text_color);
        color!(SEPARATOR_COLOR, separator_color);
        color!(UNFOCUSED_TITLE_BACKGROUND_COLOR, unfocused_title_bg_color);
        color!(UNFOCUSED_TITLE_TEXT_COLOR, unfocused_title_text_color);
        macro_rules! size {
            ($sized:ident, $field:ident) => {
                if let Some(size) = theme.$field {
                    $sized.set(size);
                }
            };
        }
        size!(BORDER_WIDTH, border_width);
        size!(TITLE_HEIGHT, title_height);
        if let Some(font) = &theme.font {
            set_font(font);
        }
    }
}

#[derive(Eq, PartialEq, Hash)]
struct OutputId {
    manufacturer: String,
    model: String,
    serial_number: String,
}

struct PersistentState {
    seen_outputs: RefCell<AHashSet<OutputId>>,
    default: Config,
    seat: Seat,
    binds: RefCell<AHashSet<ModifiedKeySym>>,
}

fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
    let mut path = PathBuf::from(config_dir());
    path.push("config.toml");
    let config = match std::fs::read(&path) {
        Ok(input) => match parse_config(&input, |e| {
            log::warn!("Error while parsing {}: {}", path.display(), Report::new(e))
        }) {
            None if initial_load => {
                log::warn!("Using default config instead");
                persistent.default.clone()
            }
            None => {
                log::warn!("Ignoring config reload");
                return;
            }
            Some(c) => c,
        },
        Err(e) if e.kind() == ErrorKind::NotFound => {
            log::info!("{} does not exist. Using default config.", path.display());
            persistent.default.clone()
        }
        Err(e) => {
            log::warn!("Could not load {}: {}", path.display(), Report::new(e));
            log::warn!("Ignoring config reload");
            return;
        }
    };
    let mut outputs = AHashMap::new();
    for output in &config.outputs {
        if let Some(name) = &output.name {
            let prev = outputs.insert(name.clone(), output.match_.clone());
            if prev.is_some() {
                log::warn!("Duplicate output name {name}");
            }
        }
    }
    let mut keymaps = AHashMap::new();
    for keymap in config.keymaps {
        match keymap {
            ConfigKeymap::Defined { name, map } => {
                keymaps.insert(name, map);
            }
            _ => log::warn!("Keymap is not in defined form in top-level context"),
        }
    }
    let mut input_devices = AHashMap::new();
    for input in &config.inputs {
        if let Some(tag) = &input.tag {
            let prev = input_devices.insert(tag.clone(), input.match_.clone());
            if prev.is_some() {
                log::warn!("Duplicate input tag {tag}");
            }
        }
    }
    let mut named_drm_device = AHashMap::new();
    for drm_device in &config.drm_devices {
        if let Some(name) = &drm_device.name {
            let prev = named_drm_device.insert(name.clone(), drm_device.match_.clone());
            if prev.is_some() {
                log::warn!("Duplicate drm device name {name}");
            }
        }
    }
    let state = Rc::new(State {
        outputs,
        drm_devices: named_drm_device,
        input_devices,
        persistent: persistent.clone(),
        keymaps,
    });
    state.set_status(&config.status);
    match config.on_graphics_initialized {
        None => on_graphics_initialized(|| ()),
        Some(a) => on_graphics_initialized(a.into_fn(&state)),
    }
    match config.on_idle {
        None => on_idle(|| ()),
        Some(a) => on_idle(a.into_fn(&state)),
    }
    state.unbind_all();
    state.apply_shortcuts(config.shortcuts);
    if let Some(keymap) = config.keymap {
        state.set_keymap(&keymap);
    }
    on_new_connector(move |c| {
        for connector in &config.connectors {
            if connector.match_.matches(c) {
                connector.apply(c);
            }
        }
    });
    on_connector_connected({
        let state = state.clone();
        move |c| {
            let id = OutputId {
                manufacturer: c.manufacturer(),
                model: c.model(),
                serial_number: c.serial_number(),
            };
            if state.persistent.seen_outputs.borrow_mut().insert(id) {
                for output in &config.outputs {
                    if output.match_.matches(c, &state) {
                        output.apply(c);
                    }
                }
            }
        }
    });
    set_default_workspace_capture(config.workspace_capture);
    for (k, v) in config.env {
        set_env(&k, &v);
    }
    if initial_load && !is_reload() {
        if let Some(on_startup) = config.on_startup {
            on_startup.into_fn(&state)();
        }
        if let Some(level) = config.log_level {
            set_log_level(level);
        }
        if let Some(idle) = config.idle {
            set_idle(Some(idle));
        }
    }
    on_devices_enumerated({
        let state = state.clone();
        move || {
            if let Some(dev) = config.render_device {
                for d in drm_devices() {
                    if dev.matches(d, &state) {
                        d.make_render_device();
                        return;
                    }
                }
            }
        }
    });
    reset_colors();
    reset_font();
    reset_sizes();
    state.apply_theme(&config.theme);
    if let Some(api) = config.gfx_api {
        set_gfx_api(api);
    }
    if let Some(dse) = config.direct_scanout_enabled {
        set_direct_scanout_enabled(dse);
    }
    on_new_drm_device({
        let state = state.clone();
        move |d| {
            for dev in &config.drm_devices {
                if dev.match_.matches(d, &state) {
                    dev.apply(d);
                }
            }
        }
    });
    on_new_input_device({
        let state = state.clone();
        move |c| {
            for input in &config.inputs {
                if input.match_.matches(c, &state) {
                    input.apply(c);
                }
            }
        }
    });
}

fn create_command(exec: &Exec) -> Command {
    let mut command = Command::new(&exec.prog);
    for arg in &exec.args {
        command.arg(arg);
    }
    for (k, v) in &exec.envs {
        command.env(k, v);
    }
    command
}

const DEFAULT: &[u8] = include_bytes!("default-config.toml");

pub fn configure() {
    let default = parse_config(DEFAULT, |e| {
        panic!("Could not parse the default config: {}", Report::new(e))
    });
    let persistent = Rc::new(PersistentState {
        seen_outputs: Default::default(),
        default: default.unwrap(),
        seat: default_seat(),
        binds: Default::default(),
    });
    load_config(true, &persistent);
}

config!(configure);
