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

// -- 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: MetronomeHandle,
}

impl Timeline {
    pub fn new(notes_number: usize, metronome: MetronomeHandle) -> 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: 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(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.get_vadjustment().unwrap();
        let max = self.full_height - (note + 1) as f64 * BAR_HEIGHT;
        let min = max - scroll_pos.get_page_size() + BAR_HEIGHT;
        if scroll_pos.get_value() < min {
            scroll_pos.set_value(min);
        } else if scroll_pos.get_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.get_style_context();
        let state = self.widget.get_state_flags();
        let fg = style.get_color(state);
        let bg = style.lookup_color("theme_bg_color").unwrap();

        // Calculate sizes
        let full_width_px = self.widget.get_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);
            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);
            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);
        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);
            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);
        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);
            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);
            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 { entry.end } else { view_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);
                    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);
                        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,
    }
}