//! This example intends to visualize the half edge output of the voronoi algorithm.
//! Read all about the half edge data structure here:
//! <https://www.boost.org/doc/libs/1_75_0/libs/polygon/doc/voronoi_diagram.htm>

use boostvoronoi::prelude::*;

use fltk::{app, button, draw, enums, frame, group, menu, prelude::*, window};
use fltk_gui::visualizer::VoronoiVisualizer;
use num_traits::AsPrimitive;
use std::cell::{RefCell, RefMut};
use std::rc::Rc;

#[macro_use]
extern crate bitflags;

// frame size
const FH: i32 = 790;
const FW: i32 = 790;

// window size
const WH: i32 = 800;
const WW: i32 = FW + 180;

const COORD_X_OFFSET: i32 = 5;
const COORD_Y_OFFSET: i32 = 5;

bitflags! {
    pub struct ColorFlag: ColorType {
        const EXTERNAL     = 0b00000001;
        const PRIMARY      = 0b00000010;
        const CURVE        = 0b00000100;
        // The edge/vertex belongs to a Cell defined by a segment
        const CELL_SEGMENT = 0b00001000;
        // The edge/vertex belongs to a Cell defined by a point
        const CELL_POINT   = 0b00010000;
        const INFINITE     = 0b00100000;
        // The vertex coincides with a site point
        const SITE_VERTEX  = 0b01000000;
    }
}
fn cast<F: AsPrimitive<T> + Copy + 'static, T: Copy + 'static>(f: F) -> T {
    f.as_()
}

bitflags! {
    #[derive(Debug,Clone, Copy)]
    pub struct DrawFilterFlag: u32 {
        /// Edges considered to be outside all closed input geometry
        const EXTERNAL =      0b000000000000001;
        const PRIMARY =       0b000000000000010;
        const CURVE =         0b000000000000100;
        const VERTICES=       0b000000000001000;
        /// All edges
        const EDGES=          0b000000000010000;
        const SECONDARY =     0b000000000100000;
        /// Input geometry points
        const INPUT_POINT =   0b000000001000000;
        /// Input geometry segments
        const INPUT_SEGMENT = 0b000000010000000;
        /// Draw infinite edges
        const INFINITE =      0b001000000000000;
        /// Draw curves as straight lines
        const CURVE_LINE =    0b010000000000000;
        const SITE_VERTEX =   0b100000000000000;
        const DRAW_ALL =      0b111111111111111;
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Example {
    File,
    Simple,
    Complex,
    Clean,
}

#[derive(Debug, Clone, Copy)]
pub enum GuiMessage {
    Filter(DrawFilterFlag),
    MenuChoice(Example),
}

/// The integer input type used by the voronoi builder
type IType = i32;

struct SharedData {
    draw_flag: DrawFilterFlag,
    last_message: Option<GuiMessage>,
    visualizer: VoronoiVisualizer<IType>,
    last_click: Option<Point<IType>>,
}

/// The Offscreen is slightly offset from the window.
/// This method gives the mouse coordinates usable inside the offscreen.
#[inline(always)]
fn offscreen_event_coords() -> (i32, i32) {
    let pos = app::event_coords();
    (pos.0 - COORD_X_OFFSET, pos.1 - COORD_Y_OFFSET)
}

fn main() -> Result<(), BvError> {
    let app = app::App::default();
    let mut wind = window::Window::default()
        .with_size(WW, WH)
        .center_screen()
        .with_label("Boost voronoi ported to Rust");

    let mut frame = frame::Frame::new(COORD_X_OFFSET, COORD_Y_OFFSET, FW, FH, "");
    frame.set_color(enums::Color::Black);
    frame.set_frame(enums::FrameType::DownBox);

    let mut pack = group::Pack::new(COORD_X_OFFSET + FW, COORD_Y_OFFSET, 170, WH, "");
    pack.set_spacing(5);

    let mut menu_but = menu::MenuButton::default()
        .with_size(170, 25)
        .with_label("Menu");
    menu_but.set_frame(enums::FrameType::PlasticUpBox);

    let mut input_points_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("input points");
    input_points_button.toggle(true);
    input_points_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut input_segments_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("input segments");
    input_segments_button.toggle(true);
    input_segments_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut external_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("externals");
    external_button.toggle(true);
    external_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut vertices_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("vertices (all)");
    vertices_button.toggle(true);
    vertices_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut site_vertices_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("site vertices");
    site_vertices_button.toggle(true);
    site_vertices_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut edges_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("edges (all)");
    edges_button.toggle(true);
    edges_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut infinite_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("infinite edges");
    infinite_button.toggle(true);
    infinite_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut curved_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("arc edges");
    curved_button.toggle(true);
    curved_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut curved_as_lines_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("arc as lines");
    curved_as_lines_button.toggle(false);
    curved_as_lines_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut primary_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("primary edges");
    primary_button.toggle(true);
    primary_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut secondary_button = button::RoundButton::default()
        .with_size(180, 25)
        .with_label("secondary edges");
    secondary_button.toggle(true);
    secondary_button.set_frame(enums::FrameType::PlasticUpBox);

    let mut pack_x = group::Pack::default()
        .with_size(180, 25)
        .below_of(&secondary_button, 25);
    pack_x.set_type(group::PackType::Horizontal);
    let _ = frame::Frame::default().with_size(10, 25).with_label(" x:");
    let mut x_label = frame::Frame::default().with_size(160, 25).with_label("");
    x_label.set_align(enums::Align::Right | enums::Align::Inside);
    pack_x.end();

    let mut pack_y = group::Pack::default()
        .with_size(180, 25)
        .below_of(&pack_x, 25);
    pack_y.set_type(group::PackType::Horizontal);
    let _ = frame::Frame::default().with_size(10, 25).with_label(" y:");
    let mut y_label = frame::Frame::default().with_size(160, 25).with_label("");
    y_label.set_align(enums::Align::Right | enums::Align::Inside);
    pack_y.end();

    pack.end();

    wind.set_color(enums::Color::White);
    wind.end();
    wind.show();
    let offs = draw::Offscreen::new(frame.width(), frame.height()).unwrap();
    #[cfg(not(target_os = "macos"))]
    {
        offs.begin();
        draw::set_draw_color(enums::Color::White);
        draw::draw_rectf(0, 0, FW, FH);
        offs.end();
    }
    let offs = Rc::from(RefCell::from(offs));
    let offs_rc = Rc::clone(&offs);

    let shared_data_rc = Rc::new(RefCell::new(SharedData {
        // draw all except 'curve as straight line' as default
        draw_flag: DrawFilterFlag::DRAW_ALL ^ DrawFilterFlag::CURVE_LINE,
        last_message: None,
        visualizer: VoronoiVisualizer::default(),
        last_click: None,
    }));

    let (sender, receiver) = app::channel::<GuiMessage>();

    menu_but.add_emit(
        "From file",
        enums::Shortcut::None,
        menu::MenuFlag::Normal,
        sender,
        GuiMessage::MenuChoice(Example::File),
    );
    menu_but.add_emit(
        "Simple",
        enums::Shortcut::None,
        menu::MenuFlag::Normal,
        sender,
        GuiMessage::MenuChoice(Example::Simple),
    );
    menu_but.add_emit(
        "Complex",
        enums::Shortcut::None,
        menu::MenuFlag::Normal,
        sender,
        GuiMessage::MenuChoice(Example::Complex),
    );
    menu_but.add_emit(
        "Clean",
        enums::Shortcut::None,
        menu::MenuFlag::Normal,
        sender,
        GuiMessage::MenuChoice(Example::Clean),
    );

    external_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::EXTERNAL));
    infinite_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::INFINITE));
    primary_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::PRIMARY));
    secondary_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::SECONDARY));
    input_points_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::INPUT_POINT));
    input_segments_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::INPUT_SEGMENT));
    curved_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::CURVE));
    vertices_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::VERTICES));
    site_vertices_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::SITE_VERTEX));
    edges_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::EDGES));
    curved_as_lines_button.emit(sender, GuiMessage::Filter(DrawFilterFlag::CURVE_LINE));

    {
        // initialize visualizer
        let cl = Rc::clone(&shared_data_rc);
        let mut shared_data_bm = cl.borrow_mut();
        shared_data_bm.visualizer.read_data(Example::Complex);
        let _ = shared_data_bm.visualizer.build()?;
        shared_data_bm.visualizer.re_calculate_affine()?;
    }

    let shared_data_c = Rc::clone(&shared_data_rc);
    // This is called whenever the window is drawn and redrawn
    wind.draw(move |_| {
        if let Ok(data_b) = shared_data_c.try_borrow() {
            // todo, move the actual drawing away from draw() function, only keep the off-screen blit.
            offs_rc.borrow_mut().begin();

            draw::set_draw_color(enums::Color::White);
            draw::draw_rectf(0, 0, FW, FH);
            let _ = data_b.visualizer.draw(&data_b);
            offs_rc.borrow_mut().end();

            if offs_rc.borrow().is_valid() {
                offs_rc.borrow().copy(5, 5, FW, FH, 0, 0);
            } else {
                // this will almost never be called
                let data_b = shared_data_c.borrow();
                offs_rc.borrow_mut().begin();
                draw::set_draw_color(enums::Color::Yellow);
                draw::draw_rectf(5, 5, FW, FH);
                let _ = data_b.visualizer.draw(&data_b);
                offs_rc.borrow_mut().end();
            }
        }
    });

    let shared_data_c = Rc::clone(&shared_data_rc);
    let mut mouse_drag: Option<(i32, i32)> = None;

    wind.handle(move |_, ev| match ev {
        enums::Event::MouseWheel => {
            let mouse_position = offscreen_event_coords();
            let mut shared_data_bm = shared_data_c.borrow_mut();
            let event_dy = match app::event_dy() {
                app::MouseWheel::Up => 3,
                app::MouseWheel::Down => -3,
                _ => 0,
            };
            let reverse_middle = shared_data_bm
                .visualizer
                .affine
                .reverse_transform::<IType>(mouse_position.0 as f64, mouse_position.1 as f64);
            if reverse_middle.is_err() {
                println!("{:?}", reverse_middle.err().unwrap());
                return false;
            }
            let reverse_middle = reverse_middle.unwrap();
            if event_dy != 0 {
                let scale_mod = 1.01_f64.powf(event_dy as f64);
                shared_data_bm.visualizer.affine.scale[0] *= scale_mod;
                shared_data_bm.visualizer.affine.scale[1] *= scale_mod;
            }
            let new_middle = shared_data_bm
                .visualizer
                .affine
                .transform(reverse_middle[0] as f64, reverse_middle[1] as f64);
            // When zooming we want the center of screen remain at the same relative position.
            shared_data_bm.visualizer.affine.to_offset[0] +=
                mouse_position.0 as f64 - new_middle[0];
            shared_data_bm.visualizer.affine.to_offset[1] +=
                mouse_position.1 as f64 - new_middle[1];

            //println!("mouse wheel at dy:{:?} scale:{:?}", event_dy, shared_data_bm.visualizer.affine.scale);
            app::redraw();
            true
        }
        enums::Event::Drag => {
            let mouse_position = offscreen_event_coords();

            if mouse_drag.is_none() {
                mouse_drag = Some(mouse_position);
            } else {
                let md = mouse_drag.unwrap();
                let mut shared_data_bm = shared_data_c.borrow_mut();
                shared_data_bm.visualizer.affine.to_offset[0] += (mouse_position.0 - md.0) as f64;
                shared_data_bm.visualizer.affine.to_offset[1] += (mouse_position.1 - md.1) as f64;
                mouse_drag = Some(mouse_position);
                app::redraw();
            }
            true
        }
        enums::Event::Released => {
            let mouse_position = offscreen_event_coords();
            if mouse_drag.is_some() {
                mouse_drag = None;
            } else if app::event_key_down(enums::Key::from_char('L'))
                || app::event_key_down(enums::Key::from_char('S'))
            {
                let mut shared_data_bm = shared_data_c.borrow_mut();
                let point = shared_data_bm
                    .visualizer
                    .affine
                    .reverse_transform(mouse_position.0 as f64, mouse_position.1 as f64);
                if point.is_err() {
                    println!("{:?}", point.err().unwrap());
                    return false;
                }
                let point = Point::from(point.unwrap());
                if let Some(last_point) = shared_data_bm.last_click {
                    let line = Line {
                        start: last_point,
                        end: point,
                    };

                    if !shared_data_bm.visualizer.self_intersecting_check(&line) {
                        shared_data_bm.visualizer.segment_data_.push(line);

                        let _ = shared_data_bm.visualizer.build().unwrap();

                        if app::event_key_down(enums::Key::from_char('L')) {
                            shared_data_bm.last_click = None;
                        } else {
                            shared_data_bm.last_click = Some(point);
                        }
                        app::redraw();
                    }
                } else {
                    shared_data_bm.last_click = Some(point);
                }
            } else if app::event_x() < FW {
                let mut shared_data_bm = shared_data_c.borrow_mut();
                {
                    let mouse_position = offscreen_event_coords();
                    let point = shared_data_bm
                        .visualizer
                        .affine
                        .reverse_transform(mouse_position.0 as f64, mouse_position.1 as f64);
                    if point.is_err() {
                        println!("{:?}", point.err().unwrap());
                        return false;
                    }
                    let point = Point::from(point.unwrap());
                    //println!(
                    //    "Before added a point len: {} ",
                    //    shared_data_bm.visualizer.point_data_.len()
                    //);
                    shared_data_bm.visualizer.point_data_.push(point);
                    //println!(
                    //    "Added a point new len: {} ",
                    //    shared_data_bm.visualizer.point_data_.len()
                    //);
                }
                println!(
                    "After added a point new len: {} ",
                    shared_data_bm.visualizer.point_data_.len()
                );
                let _ = shared_data_bm.visualizer.build().unwrap();

                shared_data_bm.last_click = None;
                app::redraw();
            }
            true
        }
        enums::Event::KeyDown => {
            if app::event_key_down(enums::Key::from_char('C')) {
                let mut shared_data_bm = shared_data_c.borrow_mut();
                shared_data_bm.last_click = None;
                shared_data_bm.visualizer.segment_data_.clear();
                shared_data_bm.visualizer.point_data_.clear();
                shared_data_bm.visualizer.diagram.clear();
                app::redraw();
            }
            false
        }
        enums::Event::KeyUp => {
            let mut shared_data_bm = shared_data_c.borrow_mut();
            shared_data_bm.last_click = None;
            false
        }
        enums::Event::Move => {
            // only update coordinate when hovering over the graphics
            let mouse_position = offscreen_event_coords();
            if mouse_position.0 < FW {
                if let Ok(shared_data_b) = shared_data_c.try_borrow() {
                    // todo: IDK why the borrow sometimes fails
                    let point = shared_data_b.visualizer.affine.reverse_transform::<IType>(
                        mouse_position.0 as f64,
                        mouse_position.1 as f64,
                    );
                    if let Ok(point) = point {
                        x_label.set_label(&point[0].to_string());
                        y_label.set_label(&point[1].to_string());
                    } else {
                        x_label.set_label("?");
                        y_label.set_label("?");
                    }
                } else {
                    println!("fltk-gui: shared_data_c.try_borrow() failed (ignored)");
                }
            }
            false
        }
        _ => false,
    });

    let shared_data_c = Rc::clone(&shared_data_rc);
    while app.wait() {
        if let Some(msg) = receiver.recv() {
            let mut shared_data_bm: RefMut<_> = shared_data_c.borrow_mut();
            match msg {
                GuiMessage::MenuChoice(v) => {
                    let new_title = shared_data_bm.visualizer.read_data(v);
                    {
                        let w = &mut wind;
                        w.set_label(new_title.as_str());
                    }
                    let _ = shared_data_bm.visualizer.build()?;
                    let _ = shared_data_bm.visualizer.re_calculate_affine();
                    //app::redraw();
                }
                GuiMessage::Filter(flag) => {
                    shared_data_bm.draw_flag ^= flag;
                }
            }
            shared_data_bm.last_message = Some(msg);
            app::redraw();
        }
    }
    Ok(())
}

pub(crate) mod fltk_gui {
    pub(crate) mod demo_data;
    pub(crate) mod visualizer;
}
