use crate::widgets as w;
use std::time;

mod defaults {
    pub const BG_COLOR: (f64, f64, f64) = (0.1, 0.1, 0.1);
    pub const FG_COLOR: (f64, f64, f64) = (1.0, 1.0, 1.0);

    pub const FONT_FAMILY: &str = "Fira Mono";
    pub const FONT_SIZE: &str = "18";
}

pub struct Config {
    left: Vec<WidgetWrapper>,
    right: Vec<WidgetWrapper>,
    bg_color: (f64, f64, f64),
    fg_color: (f64, f64, f64),
    font: String,
    height: i32,
    buffer: i32,
}

pub struct WidgetWrapper {
    update: Option<(time::Duration, time::SystemTime)>,
    widget: Box<dyn w::Widget>,
}

impl WidgetWrapper {
    fn new(mut widget: Box<dyn w::Widget>) -> WidgetWrapper {
        let update = if let Some(f) = widget.update_frequency() {
            widget.update();
            Some((time::Duration::new(f, 0), time::SystemTime::now()))
        } else {
            None
        };
        WidgetWrapper { update, widget }
    }

    fn update(&mut self) {
        if let Some((freq, ref mut last)) = self.update {
            if let Ok(since) = last.elapsed() {
                if since > freq {
                    self.widget.update();
                    *last = time::SystemTime::now();
                }
            }
        }
    }
}

pub fn color_from_hex(input: &str) -> Result<(f64, f64, f64), failure::Error> {
    let s = input.trim_start_matches("0x");
    let s = s.trim_start_matches(|c| !"ABCDEFabcdef0123456789".contains(c));
    match s.len() {
        6 => {
            let r = i64::from_str_radix(&s[0..2], 16)? as f64 / 255.0;
            let g = i64::from_str_radix(&s[2..4], 16)? as f64 / 255.0;
            let b = i64::from_str_radix(&s[4..6], 16)? as f64 / 255.0;
            Ok((r, g, b))
        }
        3 => {
            let r = i64::from_str_radix(&s[0..1], 16)? as f64 / 255.0;
            let g = i64::from_str_radix(&s[1..2], 16)? as f64 / 255.0;
            let b = i64::from_str_radix(&s[2..3], 16)? as f64 / 255.0;
            Ok((r, g, b))
        }
        _ => bail!("Unable to parse {} as a hex color literal", input),
    }
}

impl Config {
    pub fn from_toml(input: toml::Value) -> Result<Config, failure::Error> {
        let mut conf = Config {
            left: Vec::new(),
            right: Vec::new(),
            bg_color: defaults::BG_COLOR,
            fg_color: defaults::FG_COLOR,
            font: format!("{} {}", defaults::FONT_FAMILY, defaults::FONT_SIZE),
            height: 0,
            buffer: 0,
        };
        let table = input
            .as_table()
            .ok_or_else(|| format_err!("invalid config"))?;
        let widgets = &table["widgets"];
        let mut target = &mut conf.left;
        for section in widgets
            .as_array()
            .ok_or_else(|| format_err!("invalid config"))?
        {
            let section = section
                .as_table()
                .ok_or_else(|| format_err!("invalid config"))?;
            let name = section["name"]
                .as_str()
                .ok_or_else(|| format_err!("invalid config"))?;
            if name == "sep" {
                target = &mut conf.right;
            } else {
                target.push(WidgetWrapper::new(w::mk_widget(name, section)?));
            }
        }

        if let Some(color) = table.get("background") {
            conf.bg_color = color_from_hex(
                color
                    .as_str()
                    .ok_or_else(|| format_err!("`background` not a str"))?,
            )?;
        }
        if let Some(color) = table.get("foreground") {
            conf.fg_color = color_from_hex(
                color
                    .as_str()
                    .ok_or_else(|| format_err!("`foreground` not a str"))?,
            )?;
        }
        if let Some(font) = table.get("font") {
            conf.font = font
                .as_str()
                .ok_or_else(|| format_err!("`font` not a str"))?
                .to_string();
        }
        conf.right.reverse();

        let text_height = conf.calc_text_height();
        let buffer = text_height / 4;
        conf.height = conf.calc_text_height() + buffer * 2;
        conf.buffer = buffer;
        Ok(conf)
    }

    pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Config, failure::Error> {
        let body = std::fs::read_to_string(path)?;
        let val = body.parse::<toml::Value>()?;
        Config::from_toml(val)
    }

    pub fn find_config() -> Result<Config, failure::Error> {
        if let Some(p) = xdg::BaseDirectories::new()?.find_config_file("knurling/knurling.toml") {
            return Config::from_file(p);
        }
        Err(format_err!("Unable to find `knurling.toml`"))
    }

    pub fn draw(
        &self,
        ctx: &cairo::Context,
        layout: &pango::Layout,
        stdin: &str,
        size: w::Size,
    ) -> Result<(), failure::Error> {
        // paint the background
        {
            let (r, g, b) = self.bg_color;
            ctx.set_source_rgb(r, g, b);
        }
        ctx.paint();

        // set up a struct with everything that widgets need to draw
        let d = w::Drawing {
            ctx,
            lyt: &layout,
            size,
            stdin,
            buffer: self.buffer as f64,
        };

        let mut offset = 10;
        for w in self.left.iter() {
            // set the foreground color for drawing
            {
                let (r, g, b) = self.fg_color;
                ctx.set_source_rgb(r, g, b);
            }
            offset += 10 + w.widget.draw(&d, w::Located::FromLeft(offset));
        }
        offset = 10;
        for w in self.right.iter() {
            // set the foreground color for drawing
            {
                let (r, g, b) = self.fg_color;
                ctx.set_source_rgb(r, g, b);
            }
            offset += 10 + w.widget.draw(&d, w::Located::FromRight(offset));
        }

        Ok(())
    }

    pub fn update(&mut self) {
        for w in self.left.iter_mut() {
            w.update()
        }
        for w in self.right.iter_mut() {
            w.update()
        }
    }

    pub fn font(&self) -> &str {
        &self.font
    }

    pub fn get_height(&self) -> i32 {
        self.height
    }

    fn calc_text_height(&self) -> i32 {
        use pango::LayoutExt;

        // we get the height here by making a fake surface, rendering
        // some text using our chosen font to it, and seeing how big it ends up being
        let surf = cairo::ImageSurface::create(cairo::Format::Rgb24, 0, 0).unwrap();
        let ctx = cairo::Context::new(&surf);
        let layout = pangocairo::functions::create_layout(&ctx).unwrap();
        layout.set_width(800 * pango::SCALE);
        let mut font = pango::FontDescription::from_string(self.font());
        font.set_weight(pango::Weight::Bold);
        layout.set_font_description(&font);
        layout.set_text("lj");
        let (_, h) = layout.get_size();
        h / pango::SCALE
    }
}
