use crate::audio::sources::metronome;
use crate::audio::sources::synthesizer;
use crate::utils::chrono;
use gtk::prelude::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// -- Constants ---------------------------------------------------------------
const SECOND: i32 = 1000;
const MINUTE: i32 = 60 * SECOND;
const CLEAN_INTERVAL: i32 = 2 * MINUTE; // ms
const PLOT_SPEED: f64 = 30.0 / SECOND as f64; // px/ms
const BAR_HEIGHT: f64 = 15.0; // px
const HEADER_OCTAVE_WIDTH: f64 = 15.0; // px
const HEADER_NOTE_WIDTH: f64 = 25.0; // px
const FONT_SIZE: f64 = 11.0; // px

// -- LogEntry ----------------------------------------------------------------
pub struct LogEntry {
    pub start: i32,
    pub end: i32,
}

// -- Timeline ----------------------------------------------------------------
pub struct Timeline {
    pub widget: gtk::ScrolledWindow,
    log: Mutex<HashMap<usize, Vec<LogEntry>>>,
    notes_number: usize,
    full_height: f64,
    metronome: metronome::Handle,
}

impl Timeline {
    pub fn new(notes_number: usize, metronome: metronome::Handle) -> Arc<Self> {
        let scrolled_window = gtk::ScrolledWindow::new(gtk::NONE_ADJUSTMENT, gtk::NONE_ADJUSTMENT);
        let drawing_area = gtk::DrawingArea::new();
        drawing_area.set_size_request(-1, notes_number as i32 * BAR_HEIGHT as i32);
        scrolled_window.add(&drawing_area);
        scrolled_window.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);

        let timeline = Arc::new(Timeline {
            widget: scrolled_window,
            log: Mutex::new(HashMap::new()),
            notes_number,
            full_height: BAR_HEIGHT * notes_number as f64,
            metronome,
        });

        let timeline_ = timeline.clone();
        drawing_area.connect_draw(move |_, ctx| {
            timeline_.draw(ctx);
            Inhibit(false)
        });

        drawing_area.add_tick_callback(|wgt, _| {
            wgt.queue_draw();
            Continue(true)
        });

        timeline
    }

    pub fn log_note_start(&self, note: usize) {
        // Log note
        let mut log = self.log.lock().unwrap();
        let note_log = log.entry(note).or_insert_with(Vec::new);
        note_log.push(LogEntry {
            start: chrono::elapsed_ms() as i32,
            end: 0,
        });

        // Adjust scroll to display new note
        let scroll_pos = self.widget.vadjustment();
        let max = self.full_height - (note + 1) as f64 * BAR_HEIGHT;
        let min = max - scroll_pos.page_size() + BAR_HEIGHT;
        if scroll_pos.value() < min {
            scroll_pos.set_value(min);
        } else if scroll_pos.value() > max {
            scroll_pos.set_value(max);
        }
        self.widget.set_vadjustment(Some(&scroll_pos));
    }

    pub fn log_note_end(&self, note: usize) {
        if let Some(note_log) = self.log.lock().unwrap().get_mut(&note) {
            if let Some(log_entry) = note_log.last_mut() {
                if log_entry.end == 0 {
                    log_entry.end = chrono::elapsed_ms() as i32;
                }
            }
        }
    }

    fn draw(&self, ctx: &cairo::Context) {
        let style = self.widget.style_context();
        let state = self.widget.state_flags();
        let fg = style.color(state);
        let bg = style.lookup_color("theme_bg_color").unwrap();

        // Calculate sizes
        let full_width_px = self.widget.allocated_width() as f64;
        let full_height_px = self.full_height;
        ctx.scale(full_width_px, full_height_px);

        let pxh = 1.0 / full_height_px; // Pixel vertical relative size
        let pxw = 1.0 / full_width_px; // Pixel horizontal relative size

        let view_width_px = full_width_px - HEADER_OCTAVE_WIDTH - HEADER_NOTE_WIDTH;
        let header_octave_width = HEADER_OCTAVE_WIDTH * pxw;
        let header_note_width = HEADER_NOTE_WIDTH * pxw;
        let header_width = header_octave_width + header_note_width;
        let bar_height = BAR_HEIGHT * pxh;

        let view_duration = (view_width_px / PLOT_SPEED) as i32;
        let view_end = chrono::elapsed_ms() as i32;
        let view_start = view_end - view_duration;
        let step = PLOT_SPEED * pxw;

        // Select font
        ctx.select_font_face("Sans", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
        ctx.set_font_matrix(cairo::Matrix {
            xx: FONT_SIZE * pxw,
            yy: FONT_SIZE * pxh,
            x0: 0.0,
            y0: 0.0,
            xy: 0.0,
            yx: 0.0,
        });

        // Draw plot background
        let color = gradation(bg, fg, 0.1);
        ctx.set_source_rgb(color.red, color.green, color.blue);
        for i in (0..self.notes_number).step_by(2) {
            let y = 1.0 - (i + 1) as f64 * bar_height;
            ctx.rectangle(0.0, y, 1.0, bar_height);
            let _ = ctx.fill();
        }

        // Draw octave separators
        let color = gradation(bg, fg, 0.3);
        ctx.set_source_rgb(color.red, color.green, color.blue);
        ctx.set_line_width(1.0 * pxh);
        let mut i = synthesizer::utils::get_c_next_octave(0);
        while i < self.notes_number as usize {
            let y = 1.0 - i as f64 * bar_height;
            ctx.move_to(0.0, y);
            ctx.line_to(1.0, y);
            let _ = ctx.stroke();
            i = synthesizer::utils::get_c_next_octave(i);
        }

        // Draw octave headers
        let color = gradation(bg, fg, 0.3);
        ctx.set_source_rgb(color.red, color.green, color.blue);
        ctx.rectangle(0.0, 0.0, header_octave_width, 1.0);
        let _ = ctx.fill();
        ctx.set_source_rgb(fg.red, fg.green, fg.blue);
        let mut i = 0;
        while i < self.notes_number {
            let y = 1.0 - i as f64 * bar_height;
            ctx.move_to(3.0 * pxw, y - 3.0 * pxh);
            let _ = ctx.show_text(synthesizer::utils::get_octave_name(i));
            i = synthesizer::utils::get_c_next_octave(i);
        }

        // Draw note headers
        ctx.set_source_rgba(fg.red, fg.green, fg.blue, 0.1);
        ctx.rectangle(header_octave_width, 0.0, header_note_width, 1.0);
        let _ = ctx.fill();
        ctx.set_source_rgb(fg.red, fg.green, fg.blue);
        for i in 0..self.notes_number {
            let y = 1.0 - i as f64 * bar_height;
            ctx.move_to(header_octave_width + 3.0 * pxw, y - 3.0 * pxh);
            let _ = ctx.show_text(synthesizer::utils::get_note_name(i));
        }

        // Draw time-ticks
        let bpm = self.metronome.get_bpm() as i32;
        let (meter_numerator, _) = self.metronome.get_meter().as_tuple();
        let beat_timestamp = self.metronome.get_timestamp() as i32;

        let color = gradation(bg, fg, 0.3);
        ctx.set_source_rgb(color.red, color.green, color.blue);
        let small_period = MINUTE / bpm;
        let big_period = small_period * meter_numerator as i32;
        let start = view_duration % small_period + small_period
            - (view_end - beat_timestamp) % small_period;
        let end = view_duration;
        for x in (start..end).step_by(small_period as usize) {
            if (x + view_start - beat_timestamp) % big_period == 0 {
                ctx.set_line_width(1.0 * pxw);
            } else {
                ctx.set_line_width(0.5 * pxw);
            }
            let x = x as f64 * step + header_width;
            ctx.move_to(x, 0.0);
            ctx.line_to(x, 1.0);
            let _ = ctx.stroke();
        }

        // Plot notes
        let need_cleaning = view_end % CLEAN_INTERVAL == 0;
        for (note, log) in self.log.lock().unwrap().iter_mut() {
            if need_cleaning {
                log.retain(|e| e.end != 0 && e.end < view_start);
            }
            let y = 1.0 - f64::from(*note as u32 + 1) * bar_height;
            let mut x_last = header_width;
            for entry in log.iter() {
                let color = gradation(bg, fg, 0.7);
                ctx.set_source_rgb(color.red, color.green, color.blue);
                let start = entry.start;
                let end = if entry.end == 0 { view_end } else { entry.end };
                if end > view_start {
                    let x = (start - view_start).max(0) as f64 * step + header_width;
                    let bar_width = (end - view_start) as f64 * step - x + header_width;
                    ctx.rectangle(x, y, bar_width, bar_height);
                    let _ = ctx.fill();
                    if x >= x_last + 25.0 * pxw {
                        let label = synthesizer::utils::get_note_name(*note);
                        ctx.move_to(
                            x - (10 * label.len()) as f64 * pxw,
                            y + bar_height - 3.0 * pxh,
                        );
                        let _ = ctx.show_text(label);
                    }
                    x_last = x + bar_width;
                }
            }
        }
    }
}

fn gradation(c1: gdk::RGBA, c2: gdk::RGBA, ratio: f64) -> gdk::RGBA {
    gdk::RGBA {
        red: c1.red + (c2.red - c1.red) * ratio,
        green: c1.green + (c2.green - c1.green) * ratio,
        blue: c1.blue + (c2.blue - c1.blue) * ratio,
        alpha: c1.alpha + (c2.alpha - c1.alpha) * ratio,
    }
}
