use std::fmt::Write;
use std::{
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
};

use common::build::Builder;
use common::prelude::Instruction;
use compiler::Compiler;
use egui::{Align, Context, Key, Layout, Ui};

use dsa_editor::{CodeEditor, ColorTheme, Syntax};
use egui_file::FileDialog;

use crate::emulator::{
    system::model::{Command, State},
    ui::interface::Component,
};

use assembler::prelude::*;

#[derive(Default)]
pub struct Editor {
    // editor state
    path: Option<PathBuf>,
    unsaved: bool,
    text: String,
    buffer: String,

    // output / loading
    output: Vec<u8>,
    load_offset: u32,
    offset_str: String,

    // cursor - currently unused
    cursor_col: usize,
    cursor_line: usize,

    // file dialogs
    open_file_dialog: Option<FileDialog>,
    save_file_dialog: Option<FileDialog>,

    // other
    visible: bool,
    error: Option<String>,
}

impl Component for Editor {
    fn name(&self) -> &'static str {
        "Editor"
    }

    fn visible(&mut self) -> &mut bool {
        &mut self.visible
    }

    fn category(&self) -> super::interface::Category {
        super::interface::Category::Programming
    }

    fn render(&mut self, state: &mut State, ui: &mut Ui, ctx: &Context) {
        if self.buffer != self.text {
            self.unsaved = true;
        }

        ui.vertical(|ui| {
            // Top bar

            if ui.input(|i| i.key_pressed(Key::S) && i.modifiers.ctrl) {
                self.save();
            }

            self.render_toolbar(state, ui, ctx);

            ui.add_space(4.0); // Add some spacing instead of just a separator
            ui.separator();

            let remaining_height = f32::max(ui.available_height() - 100.0, 100.0);

            ui.allocate_ui_with_layout(
                egui::Vec2::new(ui.available_width(), remaining_height),
                Layout::left_to_right(Align::Min),
                |ui| {
                    self.render_editor(state, ui, ctx);
                    ui.separator();
                    self.render_output(state, ui, ctx);
                },
            );

            self.render_bottom_bar(state, ui, ctx);
        });
    }
}

impl Editor {
    #[must_use]
    pub const fn new() -> Self {
        Self {
            path: None,
            text: String::new(),
            buffer: String::new(),
            output: Vec::new(),
            unsaved: true,
            cursor_col: 1,
            cursor_line: 1,
            visible: false,
            load_offset: 0,
            offset_str: String::new(),
            error: None,
            open_file_dialog: None,
            save_file_dialog: None,
        }
    }

    fn filename(&self) -> &str {
        if let Some(path) = &self.path {
            return path
                .file_name()
                .unwrap_or_else(|| OsStr::new("Unnamed!"))
                .to_str()
                .unwrap_or_else(|| unreachable!("File name should be valid UTF-8."));
        }
        "Unnamed!"
    }

    fn extension(&self) -> &str {
        if let Some(path) = &self.path {
            return path
                .extension()
                .unwrap_or_else(|| OsStr::new("Unknown!"))
                .to_str()
                .unwrap_or_else(|| unreachable!("File name should be valid UTF-8."));
        }
        "Unknown!"
    }

    fn save(&mut self) {
        if self.open_file_dialog.is_some() {
            // TODO: Flash an error stating you can only have one menu open at once.
            self.open_file_dialog = None;
        }

        if let Some(path) = &self.path {
            // Save to existing path
            self.buffer = self.text.clone();

            let text = if path.extension().is_some_and(|ext| ext == "dsb") {
                let mut res = Vec::new();
                for line in self.text.lines() {
                    for line in line.split_whitespace() {
                        match u32::from_str_radix(line, 16) {
                            Ok(num) => res.push(num),
                            Err(e) => {
                                self.error = Some(format!("Failed to parse file: {e}"));
                                return;
                            }
                        }
                    }
                }
                res.into_iter()
                    .flat_map(u32::to_be_bytes)
                    .collect::<Vec<u8>>()
            } else {
                self.text.as_bytes().to_vec()
            };

            if let Err(why) = std::fs::write(path, text) {
                self.error = Some(format!("Failed to save file: {why}"));
            } else {
                self.unsaved = false;
            }
        } else {
            // Open the save dialog.
            let work_dir = std::env::current_dir().unwrap_or_else(|_| {
                dirs::home_dir().expect(
                    "Couldn't get your current working directory or your home directory.",
                )
            });

            if self.save_file_dialog.is_none() {
                let mut dialog = FileDialog::save_file(Some(work_dir));
                dialog.open();
                self.save_file_dialog = Some(dialog);
            }
        }
    }

    fn open(&mut self) {
        let work_dir = std::env::current_dir().unwrap_or_else(|_| {
            dirs::home_dir().expect(
                "Couldn't get your current working directory or your home directory.",
            )
        });

        if self.save_file_dialog.is_some() {
            // TODO: Flash an error stating you can only have one menu open at once.
            self.save_file_dialog = None;
        }

        if self.open_file_dialog.is_none() {
            if let Some(p) = &self.path {
                let path = p.parent().map(Path::to_path_buf);
                let mut dialog = FileDialog::open_file(path);
                dialog.open();
                self.open_file_dialog = Some(dialog);
            } else {
                let mut dialog = FileDialog::open_file(Some(work_dir));
                dialog.open();
                self.open_file_dialog = Some(dialog);
            }
        }
    }

    fn handle_file_dialogs(&mut self, ctx: &egui::Context) {
        // Handle open dialog
        if let Some(dialog) = &mut self.open_file_dialog
            && dialog.show(ctx).selected()
        {
            if let Some(file) = dialog.path() {
                // check if the file is a binary file
                if file.extension().is_some_and(|ext| ext == "dsb") {
                    match std::fs::read(file) {
                        Ok(content) => {
                            let mut res = String::new();
                            for (i, b) in content.iter().enumerate() {
                                _ = write!(res, "{b:02x}");
                                if i % 4 == 3 {
                                    res.push('\n');
                                }
                            }
                            self.text = res.clone();
                            self.buffer = res;
                            self.path = Some(file.to_path_buf());
                            self.unsaved = false;
                            self.error = None;
                        }
                        Err(e) => {
                            self.error = Some(format!("Failed to read file: {e}"));
                        }
                    }
                } else {
                    match std::fs::read_to_string(file) {
                        Ok(content) => {
                            self.text = content.clone();
                            self.buffer = content;
                            self.path = Some(file.to_path_buf());
                            self.unsaved = false;
                            self.error = None;
                        }
                        Err(e) => {
                            self.error = Some(format!("Failed to read file: {e}"));
                        }
                    }
                }
            }
            self.open_file_dialog = None;
        }

        // Handle save dialog
        if let Some(dialog) = &mut self.save_file_dialog
            && dialog.show(ctx).selected()
        {
            if let Some(file) = dialog.path() {
                self.buffer = self.text.clone();

                let content = if file.extension().is_some_and(|ext| ext == "dsb") {
                    let mut res = Vec::new();
                    for line in self.text.lines() {
                        for line in line.split_whitespace() {
                            match u32::from_str_radix(line, 16) {
                                Ok(num) => res.push(num),
                                Err(e) => {
                                    self.error =
                                        Some(format!("Failed to parse file: {e}"));
                                    return;
                                }
                            }
                        }
                    }
                    res.into_iter()
                        .flat_map(u32::to_be_bytes)
                        .collect::<Vec<u8>>()
                } else {
                    self.text.clone().as_bytes().to_vec()
                };

                match std::fs::write(file, content) {
                    Ok(()) => {
                        self.path = Some(file.to_path_buf());
                        self.unsaved = false;
                        self.error = None;
                    }
                    Err(e) => {
                        self.error = Some(format!("Failed to save file: {e}"));
                    }
                }
            }
            self.save_file_dialog = None;
        }
    }

    fn render_output(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
        // Output area with synchronized scrolling
        egui::ScrollArea::vertical()
            .id_salt("output_scroll")
            .max_width(400.0)
            .show(ui, |ui| {
                if self.output.is_empty() {
                    ui.label(
                        egui::RichText::new("No output data")
                            .font(egui::FontId::monospace(12.0))
                            .color(egui::Color32::GRAY),
                    );
                    return;
                }

                egui::Grid::new("output_grid")
                    .spacing([5.0, 2.0]) // Horizontal and vertical spacing
                    .num_columns(4)
                    .striped(false)
                    .show(ui, |ui| {
                        // Process bytes in chunks of 4
                        for (line_num, chunk) in self.output.chunks(4).enumerate() {
                            let address = line_num * 4;

                            // Convert chunk to u32 (little-endian)
                            let mut bytes = [0u8; 4];
                            for (i, &byte) in chunk.iter().enumerate() {
                                if i < 4 {
                                    bytes[i] = byte;
                                }
                            }
                            let value = u32::from_be_bytes(bytes);

                            // Address column
                            ui.with_layout(
                                egui::Layout::left_to_right(egui::Align::Center),
                                |ui| {
                                    ui.set_min_width(80.0);
                                    let style = ui.style_mut();
                                    style.visuals.widgets.inactive.bg_fill =
                                        egui::Color32::from_gray(30);
                                    ui.label(
                                        egui::RichText::new(format!("0x{address:04X}"))
                                            .font(egui::FontId::monospace(12.0)),
                                    );
                                },
                            );

                            // Individual bytes column
                            let byte_str = chunk
                                .iter()
                                .map(|b| format!("{b:02X}"))
                                .collect::<Vec<_>>()
                                .join(" ");

                            ui.label(
                                egui::RichText::new(format!("{byte_str:<11}"))
                                    .font(egui::FontId::monospace(12.0))
                                    .color(egui::Color32::from_rgb(200, 200, 255)),
                            );

                            // Hex column
                            ui.label(
                                egui::RichText::new(format!("0x{value:08X}"))
                                    .font(egui::FontId::monospace(12.0))
                                    .color(egui::Color32::from_rgb(255, 200, 200)),
                            );

                            // Instruction column
                            let instruction = Instruction::decode(value).map_or_else(
                                |_| format!("{value:10}"),
                                |instruction| instruction.to_string(),
                            );

                            ui.label(
                                egui::RichText::new(instruction)
                                    .font(egui::FontId::monospace(12.0))
                                    .color(egui::Color32::from_rgb(200, 255, 200)),
                            );

                            ui.end_row();
                        }
                    });
            });
    }

    fn render_editor(&mut self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
        let available_width = ui.available_width();
        let syntax = match self.extension() {
            "dsa" => Syntax::dsa(),
            "dsc" => Syntax::dsc(),
            _ => Syntax::default(),
        };

        let mut editor = CodeEditor::default()
            .id_source("editor")
            .with_fontsize(12.0)
            .with_rows(0)
            .with_theme(ColorTheme::default())
            .with_syntax(syntax)
            .with_numlines(true)
            .desired_width(available_width - 500.0);

        editor.show(ui, &mut self.text);
    }

    fn render_bottom_bar(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
        ui.horizontal(|ui| {
            // error display
            ui.label(
                egui::RichText::new(self.error.clone().unwrap_or_default())
                    .color(egui::Color32::RED),
            );

            // line and col
            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                ui.label(format!("Ln {}, Col {}", self.cursor_line, self.cursor_col));
            });
        });
    }

    fn build(&mut self) {
        if let Some(path) = &self.path {
            match path.extension().and_then(|ext| ext.to_str()) {
                Some("dsa") => {
                    let mut assembler = Assembler::new(path);
                    assembler.start(());

                    // Or block until done
                    self.output = match assembler.output() {
                        Ok(instructions) => instructions,
                        Err(e) => {
                            self.error = Some(e.to_string());
                            return;
                        }
                    };
                }
                Some("dsc") => {
                    let dsa_path = Path::new(path).with_extension("dsa");
                    let mut compiler = Compiler::new(path);

                    let is_lib = false;
                    compiler.start(is_lib);

                    if let Err(e) = compiler.write_result(&dsa_path) {
                        self.error = Some(e.to_string());
                        return;
                    }

                    let mut assembler = Assembler::new(&dsa_path);
                    assembler.start(());

                    // Or block until done
                    self.output = match assembler.output() {
                        Ok(instructions) => instructions,
                        Err(e) => {
                            self.error = Some(format!("Assembler error: {e}"));
                            return;
                        }
                    };
                }
                Some("dsb") => {
                    if let Ok(bytes) = fs::read(path) {
                        self.output = bytes;
                    } else {
                        self.error = Some("Failed to read file".to_string());
                    }
                }
                _ => {
                    self.error = Some(format!("Invalid file type: {}", self.filename()));
                }
            }
        }
    }

    fn render_toolbar(&mut self, state: &State, ui: &mut Ui, ctx: &Context) {
        self.handle_file_dialogs(ctx);

        ui.horizontal(|ui| {
            ui.label(format!("File type: {}", self.extension()));
            ui.label(format!("Filename: {}", self.filename()));
            ui.label(format!("Unsaved: {}", self.unsaved));

            // number of lines in the file
            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                let line_count = self.text.lines().count();
                ui.label(format!("Lines: {line_count}"));
            });
        });

        ui.horizontal(|ui| {
            ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0);
            ui.spacing_mut().item_spacing.x = 6.0;

            // Opens a file
            if ui.button("Open").clicked() {
                self.open();
            }

            // Saves the current file
            if ui.button("Save").clicked() {
                self.save();
            }

            // builds the current file
            if ui.button("Build").clicked() && !self.unsaved {
                self.build();
            }

            // Loads the generated binary into the assembler at the provided offset
            if ui.button("Load").clicked() {
                if self.error.is_some() {
                    self.error =
                        Some("Can't load program at invalid offset!".to_string());
                }

                state
                    .cmd_sender
                    .send(Command::Write(self.load_offset, self.output.clone()))
                    .unwrap_or_else(|_| {
                        self.error = Some("Failed to send command".to_string());
                    });
            }

            // Entry widget to enter a load offset
            if ui.text_edit_singleline(&mut self.offset_str).changed() {
                if let Some(offset) = parse_address(&self.offset_str) {
                    self.load_offset = offset;
                    self.error = None;
                } else {
                    self.error = Some("Invalid offset".to_string());
                }
            }
        });
    }
}

fn parse_address(address: &str) -> Option<u32> {
    address.strip_prefix("0x").map_or_else(
        || {
            address.strip_prefix("0b").map_or_else(
                || {
                    address.strip_prefix("0o").map_or_else(
                        || address.parse::<u32>().ok(),
                        |oct| u32::from_str_radix(oct, 8).ok(),
                    )
                },
                |bin| u32::from_str_radix(bin, 2).ok(),
            )
        },
        |hex| u32::from_str_radix(hex, 16).ok(),
    )
}
