use cairo;
use pango;
use pangocairo;
use failure;

use pango::LayoutExt;
pub use pango::Weight;

/// A `Font` is a more convenient, less imperative abstraction over
/// the Pango `FontDescription` type
pub struct Font {
    f: pango::FontDescription,
}

impl Font {
    /// Create a new `Font` from a textual description of the font
    pub fn new(s: &str) -> Font {
        Font { f: pango::FontDescription::from_string(s) }
    }

    /// Set the font size in points
    pub fn size(mut self, s: i32) -> Font {
        self.f.set_size(s * pango::SCALE);
        self
    }

    /// Set the font weight
    pub fn weight(mut self, w: pango::Weight) -> Font {
        self.f.set_weight(w);
        self
    }

    /// Get the underlying pango `FontDescription` value
    pub fn get(self) -> pango::FontDescription {
        self.f
    }
}

/// A `MarkupBuffer` is an abstraction that allows a high-level
/// mostly-correct-by-construction (in theory, anyway) approach to
/// adding Pango-level formatting
pub struct MarkupBuffer {
    buf: String,
}

impl MarkupBuffer {
    /// Create a new empty `MarkupBuffer`
    pub fn new() -> MarkupBuffer {
        MarkupBuffer { buf: String::new() }
    }

    /// Create a view on the `MarkupBuffer` that permits markup of the
    /// added strings
    pub fn markup<'a>(&'a mut self) -> Markup<'a> {
        Markup {
            view: self,
            frags: Vec::new(),
        }
    }

    /// Add a string fragment to the markup buffer
    pub fn push(&mut self, s: &str) {
        self.buf.push_str(s);
    }

    /// Add a single space to the buffer
    pub fn space(&mut self) {
        self.buf.push_str(" ");
    }

    /// Get the underlying string value of the buffer
    fn get(self) -> String {
        self.buf
    }

    /// Draw the buffer to the underlying Cairo surface, with an
    /// optional width for layout purposes
    pub fn show_with_font(
        self,
        ctx: &cairo::Context,
        font: Font,
        width: Option<i32>,
    ) -> Result<(), failure::Error> {
        let layout = pangocairo::functions::create_layout(ctx)
            .ok_or(format_err!("Unable to create Pango layout"))?;
        layout.set_font_description(Some(&font.get()));
        if let Some(w) = width {
            layout.set_width(w * pango::SCALE);
        }
        let content = self.get();
        let (attrs, text, _) = pango::parse_markup(&content, '\0')?;
        layout.set_attributes(&attrs);
        layout.set_text(&text);
        pangocairo::functions::show_layout(ctx, &layout);
        Ok(())
    }
}

enum Frag {
    Weight(pango::Weight),
    Size(u8),
}

/// A `Markup` value is a view into a `MarkupBuffer` that permits
/// extra markup to be applied transparently.
pub struct Markup<'a> {
    view: &'a mut MarkupBuffer,
    frags: Vec<Frag>,
}

impl<'a> Markup<'a> {
    /// Give this view a different weight than usual
    pub fn weight(self, w: pango::Weight) -> Markup<'a> {
        let Markup { view, mut frags } = self;
        frags.push(Frag::Weight(w));
        Markup { view, frags }
    }

    /// Set the font size in points for the view
    pub fn size(self, s: u8) -> Markup<'a> {
        let Markup { view, mut frags } = self;
        frags.push(Frag::Size(s));
        Markup { view, frags }
    }

    /// Add a string with the extra markup supplied
    pub fn push(&mut self, s: &str) {
        self.view.push("<span ");
        for f in self.frags.iter() {
            match f {
                &Frag::Size(s) =>
                    self.view.push(&format!("font='{}' ", s)),
                &Frag::Weight(s) =>
                    self.view.push(&format!(
                        "font_weight='{}' ",
                        weight_to_string(s),
                    )),
            }
        }
        self.view.push(">");
        self.view.push(s);
        self.view.push("</span>");
    }
}

fn weight_to_string(w: Weight) -> &'static str {
    match w {
        Weight::Thin => "thin",
        Weight::Ultralight => "ultralight",
        Weight::Light => "light",
        Weight::Semilight => "semilight",
        Weight::Book => "book",
        Weight::Normal => "normal",
        Weight::Medium => "medium",
        Weight::Semibold => "semibold",
        Weight::Bold => "bold",
        Weight::Ultrabold => "ultrabold",
        Weight::Heavy => "heavy",
        Weight::Ultraheavy => "ultraheavy",
        _ => panic!("Unknown weight: {:?}", w),
    }
}
