use cairo::{
    Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, Pattern,
    TextExtents,
};

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

impl AsymLineCutout {
    pub 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.start_x
                        + self.start_length
                        + self.height
                        + self.cutout_length
                        + (self.height / 2.)
                        + self.end_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.start_y
                        + self.start_length
                        + self.height
                        + self.cutout_length
                        + (self.height / 2.)
                        + self.end_length,
                );
            }
            _ => panic!("unknown orientation"),
        }
    }
}

// Represents an asymetrical line that starts at one location, then a 45-degree angle and then
// another line afterwards.
pub struct AsymLine {
    // Will this be drawn left-to-right or up-to-down?
    pub orientation: gtk::Orientation,

    // Starting address
    pub start_x: f64,
    pub start_y: f64,

    // Length of the first segment
    pub start_length: f64,

    // Height to dodge over to the next section
    pub height: f64,

    // Total length of the entire line.
    pub end_length: f64,

    // When normal, the angle dodge is upwards. When inverted, the angle dodge is downwards.
    pub invert: bool,
}

impl AsymLine {
    pub 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.end_length,
                    self.start_y + dodge,
                );
            }
            gtk::Orientation::Vertical => {}
            _ => panic!("unknown orientation"),
        }
    }
}

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

impl SlashMeter {
    pub 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"),
        }
    }
}

/// Represents a pen for drawing a pattern. This is good for complex patterns that may require
/// multiple identical steps.
pub trait Pen {
    /// Move the pen to a location.
    fn move_to(&self, x: f64, y: f64);

    /// Draw a line from the current location to the specified destination.
    fn line_to(&self, x: f64, y: f64);

    /// Instantiate the line.
    fn stroke(&self);

    /// Convert all of the drawing into a pattern that can be painted to a drawing context.
    fn finish(self) -> Pattern;
}

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

impl GlowPen {
    pub 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()
    }
}

pub struct Text<'a> {
    content: Vec<String>,
    context: &'a Context,
}

impl<'a> Text<'a> {
    pub fn new(content: String, context: &'a Context, size: f64, width: f64) -> Self {
        context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
        context.set_font_size(size);

        let lines = word_wrap(content, context, width);

        Self { content: lines, context }
    }

    pub fn extents(&self) -> TextExtents {
        self.context.text_extents(&self.content[0]).unwrap()
    }

    pub fn draw(&self) {
        let mut baseline = 0.;
        for line in self.content.iter() {
            baseline += self.context.text_extents(line).unwrap().height() + 10.;
            self.context.move_to(0., baseline);
            let _ = self.context.show_text(line);
        }
    }
}

fn word_wrap(content: String, context: &Context, max_width: f64) -> Vec<String> {
    let mut lines = vec![];
    let words: Vec<&str> = content.split_whitespace().collect();
    let mut start: usize = 0;
    let mut line = String::new();

    for idx in 0..words.len() + 1 {
        line = words[start..idx].join(" ");
        let extents = context.text_extents(&line).unwrap();
        if extents.width() > max_width {
            let line = words[start..idx-1].join(" ");
            start = idx-1;
            lines.push(line.clone());
        }
    }
    if !line.is_empty() {
        lines.push(line);
    }
    lines
}