use cairo::{
    Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
    TextExtents,
};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
use std::{
    cell::RefCell,
    rc::Rc,
    sync::{Arc, RwLock},
    time::{Duration, Instant},
};

const WIDTH: i32 = 1600;
const HEIGHT: i32 = 600;

#[derive(Clone, Copy, Debug)]
pub enum State {
    Running {
        last_update: Instant,
        deadline: Instant,
        timeout: Option<TimeoutAnimation>,
    },
    Paused {
        time_remaining: Duration,
        timeout: Option<TimeoutAnimation>,
    },
}

impl State {
    fn new(countdown: Duration) -> Self {
        Self::Paused {
            time_remaining: countdown,
            timeout: None,
        }
    }

    fn start(&mut self) {
        if let Self::Paused {
            time_remaining,
            timeout,
        } = self
        {
            *self = Self::Running {
                last_update: Instant::now(),
                deadline: Instant::now() + *time_remaining,
                timeout: *timeout,
            };
        }
    }

    fn pause(&mut self) {
        if let Self::Running {
            deadline, timeout, ..
        } = self
        {
            *self = Self::Paused {
                time_remaining: *deadline - Instant::now(),
                timeout: *timeout,
            }
        }
    }

    fn start_pause(&mut self) {
        match self {
            Self::Running { .. } => self.pause(),
            Self::Paused { .. } => self.start(),
        }
    }

    fn run(&mut self, now: Instant) {
        if let Self::Running {
            last_update,
            deadline,
            timeout,
        } = self
        {
            *last_update = now;
            if let Some(ref mut timeout) = timeout {
                // TODO: figure out the actual number of frames
                timeout.tick(1);
            }
            if *last_update > *deadline && timeout.is_none() {
                *timeout = Some(TimeoutAnimation {
                    intensity: 1.,
                    duration: 1.,
                    ascending: false,
                });
            }
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub struct TimeoutAnimation {
    intensity: f64,
    duration: f64,
    ascending: bool,
}

impl TimeoutAnimation {
    fn tick(&mut self, frames_elapsed: u8) {
        let step_size = 1. / (self.duration * 60.);
        if self.ascending {
            self.intensity += step_size * frames_elapsed as f64;
            if self.intensity > 1. {
                self.intensity = 1.0;
                self.ascending = false;
            }
        } else {
            self.intensity -= step_size * frames_elapsed as f64;
            if self.intensity < 0. {
                self.intensity = 0.0;
                self.ascending = true;
            }
        }
    }
}

pub struct SplashPrivate {
    text: Rc<RefCell<String>>,
    background: Rc<RefCell<Pattern>>,
    time_extents: Rc<RefCell<Option<TextExtents>>>,
    width: Rc<RefCell<i32>>,
    height: Rc<RefCell<i32>>,

    state: Rc<RefCell<State>>,
}

impl SplashPrivate {
    fn set_text(&self, text: String) {
        *self.text.borrow_mut() = text;
    }

    fn set_state(&self, state: State) {
        *self.state.borrow_mut() = state;
    }

    fn redraw_background(&self) {
        let pen = GlowPen::new(
            *self.width.borrow(),
            *self.height.borrow(),
            2.,
            8.,
            (0.7, 0., 1.),
        );

        let background =
            ImageSurface::create(Format::Rgb24, *self.width.borrow(), *self.height.borrow())
                .unwrap();
        let context = Context::new(background).unwrap();
        context.push_group();
        context.set_source_rgb(0., 0., 0.);
        let _ = context.paint();

        context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);

        {
            context.set_source_rgb(0.7, 0., 1.);

            let hashtag = "#CodingTogether";
            context.set_font_size(64.);
            let extents = context.text_extents(hashtag).unwrap();

            context.move_to(20., extents.height() + 40.);
            let _ = context.show_text(hashtag);

            AsymLine {
                orientation: gtk::Orientation::Horizontal,
                start_x: 10.,
                start_y: extents.height() + 10.,
                start_length: 0.,
                height: extents.height() / 2.,
                total_length: extents.width() + extents.height() / 2.,
                invert: false,
            }
            .draw(&pen);
            pen.stroke();

            AsymLine {
                orientation: gtk::Orientation::Horizontal,
                start_x: 20.,
                start_y: extents.height() + 60.,
                start_length: extents.width(),
                height: extents.height() / 2.,
                total_length: extents.width() + extents.height() / 2.,
                invert: false,
            }
            .draw(&pen);
            pen.stroke();
        }

        {
            context.set_font_size(128.);

            let center_x = *self.width.borrow() as f64 / 2.;
            let center_y = *self.height.borrow() as f64 / 2.;

            let title_extents = context.text_extents(&self.text.borrow()).unwrap();
            let title_width = title_extents.width();
            let title_height = title_extents.height();

            {
                let start_length = center_x - title_width / 2. - title_height - 20.;

                let title_cutout = AsymLineCutout {
                    orientation: gtk::Orientation::Horizontal,
                    start_x: 20.,
                    start_y: center_y - 20. - title_height / 2.,
                    start_length,
                    total_length: *self.width.borrow() as f64 - 120.,
                    cutout_length: title_width,
                    height: title_height,
                    invert: false,
                };

                title_cutout.draw(&pen);
                pen.stroke();
            }

            {
                let title_baseline_x = center_x - title_width / 2.;
                let title_baseline_y = center_y - 20.;

                let gradient = LinearGradient::new(
                    title_baseline_x,
                    title_baseline_y - title_height,
                    title_baseline_x,
                    title_baseline_y,
                );
                gradient.add_color_stop_rgb(0.2, 0.7, 0.0, 1.0);
                gradient.add_color_stop_rgb(0.8, 0.2, 0.0, 1.0);
                context.move_to(title_baseline_x, title_baseline_y);
                let _ = context.set_source(gradient);
                let _ = context.show_text(&self.text.borrow());
            }
        }

        {
            AsymLine {
                orientation: gtk::Orientation::Horizontal,
                start_x: 100.,
                start_y: *self.height.borrow() as f64 / 2. + 100.,
                start_length: 400.,
                height: 50.,
                total_length: 650.,
                invert: true,
            }
            .draw(&pen);
            pen.stroke();
        }

        {
            context.set_source_rgb(0.7, 0., 1.);
            AsymLine {
                orientation: gtk::Orientation::Horizontal,
                start_x: *self.width.borrow() as f64 / 2. + 100.,
                start_y: *self.height.borrow() as f64 / 2. + 200.,
                start_length: 600.,
                height: 50.,
                total_length: 650.,
                invert: false,
            }
            .draw(&pen);
            pen.stroke();
        }

        let tracery = pen.finish();
        let _ = context.set_source(tracery);
        let _ = context.paint();

        let background = context.pop_group().unwrap();

        *self.background.borrow_mut() = background;
    }
}

#[glib::object_subclass]
impl ObjectSubclass for SplashPrivate {
    const NAME: &'static str = "Splash";
    type Type = Splash;
    type ParentType = gtk::DrawingArea;

    fn new() -> SplashPrivate {
        // Set up a default plain black background
        let background = ImageSurface::create(Format::Rgb24, WIDTH, HEIGHT).unwrap();
        let context = Context::new(background).unwrap();
        context.push_group();
        context.set_source_rgb(0., 0., 0.);
        let _ = context.paint();
        let background = context.pop_group().unwrap();

        SplashPrivate {
            text: Rc::new(RefCell::new(String::from(""))),
            background: Rc::new(RefCell::new(background)),
            time_extents: Rc::new(RefCell::new(None)),
            width: Rc::new(RefCell::new(WIDTH)),
            height: Rc::new(RefCell::new(HEIGHT)),

            state: Rc::new(RefCell::new(State::new(Duration::ZERO))),
        }
    }
}
impl ObjectImpl for SplashPrivate {}
impl WidgetImpl for SplashPrivate {}
impl DrawingAreaImpl for SplashPrivate {}

glib::wrapper! {
    pub struct Splash(ObjectSubclass<SplashPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}

impl Splash {
    pub fn new(text: String, state: State) -> Self {
        let s: Self = Object::builder().build();
        s.set_width_request(WIDTH);
        s.set_height_request(HEIGHT);

        s.imp().set_text(text);
        s.imp().set_state(state);
        s.imp().redraw_background();

        s.set_draw_func({
            let s = s.clone();
            move |_, context, width, height| {
                let background = s.imp().background.borrow();
                let _ = context.set_source(&*background);
                let _ = context.paint();

                let state = *s.imp().state.borrow();

                let time = match state {
                    State::Running { deadline, .. } => deadline - Instant::now(),
                    State::Paused { time_remaining, .. } => time_remaining,
                };
                let minutes = time.as_secs() / 60;
                let seconds = time.as_secs() % 60;

                let center_x = width as f64 / 2.;
                let center_y = height as f64 / 2.;

                {
                    context.select_font_face(
                        "Alegreya Sans SC",
                        FontSlant::Normal,
                        FontWeight::Bold,
                    );
                    context.set_font_size(128.);

                    let time = format!("{:02}' {:02}\"", minutes, seconds);

                    let time_extents = context.text_extents(&time).unwrap();

                    let mut saved_extents = s.imp().time_extents.borrow_mut();
                    if saved_extents.is_none() {
                        *saved_extents = Some(time_extents);
                    }

                    let time_baseline_x = center_x - time_extents.width() / 2.;
                    let time_baseline_y = center_y + 100.;

                    let gradient = LinearGradient::new(
                        time_baseline_x,
                        time_baseline_y - time_extents.height(),
                        time_baseline_x,
                        time_baseline_y,
                    );
                    let (running, timeout_animation) = match state {
                        State::Running { timeout, .. } => (true, timeout),
                        State::Paused { timeout, .. } => (false, timeout),
                    };
                    match timeout_animation {
                        Some(ref animation) => {
                            gradient.add_color_stop_rgba(0.2, 0.2, 0.0, 1.0, animation.intensity);
                            gradient.add_color_stop_rgba(0.8, 0.7, 0.0, 1.0, animation.intensity);
                            let _ = context.set_source(gradient);
                        }
                        None => {
                            if running {
                                gradient.add_color_stop_rgb(0.2, 0.2, 0.0, 1.0);
                                gradient.add_color_stop_rgb(0.8, 0.7, 0.0, 1.0);
                                let _ = context.set_source(gradient);
                            } else {
                                context.set_source_rgb(0.3, 0.3, 0.3);
                            }
                        }
                    }
                    context.move_to(time_baseline_x, time_baseline_y);
                    let _ = context.show_text(&time);
                };

                if let Some(extents) = *s.imp().time_extents.borrow() {
                    context.set_source_rgb(0.7, 0.0, 1.0);
                    let time_meter = SlashMeter {
                        orientation: gtk::Orientation::Horizontal,
                        start_x: center_x + extents.width() / 2. + 50.,
                        start_y: center_y + 100.,
                        count: 5,
                        fill_count: minutes as u8,
                        height: 60.,
                        length: 100.,
                    };
                    time_meter.draw(context);
                }
            }
        });

        s.connect_resize(|s, width, height| {
            *s.imp().width.borrow_mut() = width;
            *s.imp().height.borrow_mut() = height;
            s.imp().redraw_background();
        });

        s
    }

    pub fn set_state(&self, state: State) {
        self.imp().set_state(state);
        self.queue_draw();
    }
}

struct AsymLineCutout {
    orientation: gtk::Orientation,
    start_x: f64,
    start_y: f64,
    start_length: f64,
    total_length: f64,
    cutout_length: f64,
    height: f64,
    invert: bool,
}

impl AsymLineCutout {
    fn draw(&self, pen: &impl Pen) {
        let dodge = if self.invert {
            self.height
        } else {
            -self.height
        };
        match self.orientation {
            gtk::Orientation::Horizontal => {
                pen.move_to(self.start_x, self.start_y);
                pen.line_to(self.start_x + self.start_length, self.start_y);
                pen.line_to(
                    self.start_x + self.start_length + self.height,
                    self.start_y + dodge,
                );
                pen.line_to(
                    self.start_x + self.start_length + self.height + self.cutout_length,
                    self.start_y + dodge,
                );
                pen.line_to(
                    self.start_x
                        + self.start_length
                        + self.height
                        + self.cutout_length
                        + (self.height / 2.),
                    self.start_y + dodge / 2.,
                );
                pen.line_to(self.total_length, self.start_y + dodge / 2.);
            }
            gtk::Orientation::Vertical => {
                pen.move_to(self.start_x, self.start_y);
                pen.line_to(self.start_x, self.start_y + self.start_length);
                pen.line_to(
                    self.start_x + dodge,
                    self.start_y + self.start_length + self.height,
                );
                pen.line_to(
                    self.start_x + dodge,
                    self.start_y + self.start_length + self.height + self.cutout_length,
                );
                pen.line_to(
                    self.start_x + dodge / 2.,
                    self.start_y
                        + self.start_length
                        + self.height
                        + self.cutout_length
                        + (self.height / 2.),
                );
                pen.line_to(self.start_x + dodge / 2., self.total_length);
            }
            _ => panic!("unknown orientation"),
        }
    }
}

struct AsymLine {
    orientation: gtk::Orientation,
    start_x: f64,
    start_y: f64,
    start_length: f64,
    height: f64,
    total_length: f64,
    invert: bool,
}

impl AsymLine {
    fn draw(&self, pen: &impl Pen) {
        let dodge = if self.invert {
            self.height
        } else {
            -self.height
        };
        match self.orientation {
            gtk::Orientation::Horizontal => {
                pen.move_to(self.start_x, self.start_y);
                pen.line_to(self.start_x + self.start_length, self.start_y);
                pen.line_to(
                    self.start_x + self.start_length + self.height,
                    self.start_y + dodge,
                );
                pen.line_to(self.start_x + self.total_length, self.start_y + dodge);
            }
            gtk::Orientation::Vertical => {}
            _ => panic!("unknown orientation"),
        }
    }
}

struct SlashMeter {
    orientation: gtk::Orientation,
    start_x: f64,
    start_y: f64,
    count: u8,
    fill_count: u8,
    height: f64,
    length: f64,
}

impl SlashMeter {
    fn draw(&self, context: &Context) {
        match self.orientation {
            gtk::Orientation::Horizontal => {
                let angle: f64 = 0.8;
                let run = self.height / angle.tan();
                let width = self.length / (self.count as f64 * 2.);

                for c in 0..self.count {
                    context.set_line_width(1.);

                    let start_x = self.start_x + c as f64 * width * 2.;
                    context.move_to(start_x, self.start_y);
                    context.line_to(start_x + run, self.start_y - self.height);
                    context.line_to(start_x + run + width, self.start_y - self.height);
                    context.line_to(start_x + width, self.start_y);
                    context.line_to(start_x, self.start_y);
                    if c < self.fill_count {
                        let _ = context.fill();
                    } else {
                        let _ = context.stroke();
                    }
                }
            }
            gtk::Orientation::Vertical => {}
            _ => panic!("unknown orientation"),
        }
    }
}

trait Pen {
    fn move_to(&self, x: f64, y: f64);
    fn line_to(&self, x: f64, y: f64);
    fn stroke(&self);

    fn finish(self) -> Pattern;
}

struct GlowPen {
    blur_context: Context,
    draw_context: Context,
}

impl GlowPen {
    fn new(
        width: i32,
        height: i32,
        line_width: f64,
        blur_line_width: f64,
        color: (f64, f64, f64),
    ) -> Self {
        let blur_context =
            Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
        blur_context.set_line_width(blur_line_width);
        blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
        blur_context.push_group();
        blur_context.set_line_cap(LineCap::Round);

        let draw_context =
            Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
        draw_context.set_line_width(line_width);
        draw_context.set_source_rgb(color.0, color.1, color.2);
        draw_context.push_group();
        draw_context.set_line_cap(LineCap::Round);

        Self {
            blur_context,
            draw_context,
        }
    }
}

impl Pen for GlowPen {
    fn move_to(&self, x: f64, y: f64) {
        self.blur_context.move_to(x, y);
        self.draw_context.move_to(x, y);
    }

    fn line_to(&self, x: f64, y: f64) {
        self.blur_context.line_to(x, y);
        self.draw_context.line_to(x, y);
    }

    fn stroke(&self) {
        self.blur_context.stroke().expect("to draw the blur line");
        self.draw_context
            .stroke()
            .expect("to draw the regular line");
    }

    fn finish(self) -> Pattern {
        let foreground = self.draw_context.pop_group().unwrap();
        self.blur_context.set_source(foreground).unwrap();
        self.blur_context.paint().unwrap();
        self.blur_context.pop_group().unwrap()
    }
}

fn main() {
    let app = gtk::Application::builder()
        .application_id("com.luminescent-dreams.cyberpunk-splash")
        .flags(gio::ApplicationFlags::HANDLES_OPEN)
        .build();

    app.add_main_option(
        "title",
        glib::char::Char::from(b't'),
        glib::OptionFlags::IN_MAIN,
        glib::OptionArg::String,
        "",
        None,
    );
    app.add_main_option(
        "countdown",
        glib::char::Char::from(b'c'),
        glib::OptionFlags::IN_MAIN,
        glib::OptionArg::String,
        "",
        None,
    );

    let state = Arc::new(RwLock::new(State::new(Duration::from_secs(5 * 60))));
    let title = Arc::new(RwLock::new("".to_owned()));

    app.connect_command_line(|_, _args| {
        println!("connect_command_line");
        1
    });
    app.connect_handle_local_options({
        let title = title.clone();
        let state = state.clone();
        move |_, options| {
            println!("connect_handle_local_options");
            *title.write().unwrap() = options.lookup::<String>("title").unwrap().unwrap();
            let countdown = match options.lookup::<String>("countdown") {
                Ok(Some(countdown_str)) => {
                    let parts = countdown_str.split(':').collect::<Vec<&str>>();
                    match parts.len() {
                        2 => {
                            let minutes = parts[0].parse::<u64>().unwrap();
                            let seconds = parts[1].parse::<u64>().unwrap();
                            Duration::from_secs(minutes * 60 + seconds)
                        }
                        1 => {
                            let seconds = parts[1].parse::<u64>().unwrap();
                            Duration::from_secs(seconds)
                        }
                        _ => Duration::from_secs(300),
                    }
                }
                _ => Duration::from_secs(300),
            };
            match *state.write().unwrap() {
                State::Running {
                    ref mut deadline, ..
                } => *deadline = Instant::now() + countdown,
                State::Paused {
                    ref mut time_remaining,
                    ..
                } => *time_remaining = countdown,
            }
            -1
        }
    });

    app.connect_open(move |app, files, args| {
        println!("called open");
        println!("files: {}", files.len());
        println!("args: {}", args);

        app.activate();
    });

    app.connect_activate(move |app| {
        let (gtk_tx, gtk_rx) =
            gtk::glib::MainContext::channel::<State>(gtk::glib::Priority::DEFAULT);

        let window = gtk::ApplicationWindow::new(app);
        window.present();

        let splash = Splash::new(title.read().unwrap().clone(), *state.read().unwrap());

        window.set_child(Some(&splash));

        window.connect_maximized_notify(|window| {
            window.fullscreen();
        });

        let keyboard_events = EventControllerKey::new();
        keyboard_events.connect_key_released({
            let window = window.clone();
            let state = state.clone();
            move |_, key, _, _| {
                let name = key
                    .name()
                    .map(|s| s.as_str().to_owned())
                    .unwrap_or("".to_owned());
                match name.as_ref() {
                    "Escape" => window.unfullscreen(),
                    "space" => state.write().unwrap().start_pause(),
                    _ => {}
                }
            }
        });
        window.add_controller(keyboard_events);

        gtk_rx.attach(None, move |state| {
            splash.set_state(state);
            glib::ControlFlow::Continue
        });

        std::thread::spawn({
            let state = state.clone();
            move || {
                state.write().unwrap().start();
                loop {
                    std::thread::sleep(Duration::from_millis(1000 / 60));
                    state.write().unwrap().run(Instant::now());
                    let _ = gtk_tx.send(*state.read().unwrap());
                }
            }
        });
    });

    app.run();
}
