///|
struct Element {
  run : (@immut/hashmap.HashMap[String, String]) -> String
}

///|
pub impl Show for Element with to_string(self) {
  (self.run)(@immut/hashmap.new())
}

///|
pub impl Add for Element with add(self, other) {
  Element::new(fn(attrs) {
    let self_str = (self.run)(attrs)
    let other_str = (other.run)(attrs)
    self_str + other_str
  })
}

///|
pub impl Show for Element with output(self, logger) {
  logger.write_string(self.to_string())
}

///|
fn html_escape(s : String) -> String {
  let buf = @buffer.new()
  for i in s {
    match i {
      '&' => buf.write_string("&")
      '<' => buf.write_string("<")
      '>' => buf.write_string(">")
      '"' => buf.write_string(""")
      '\'' => buf.write_string("'")
      ch => buf.write_char(ch)
    }
  }
  buf.to_string()
}

///|
pub fn Element::make(self : Element, name : String) -> Element {
  Element::new(fn(attrs) {
    let at = fold_map_with_key(init="", build_attr, attrs)
    "<\{name}\{at}>\{(self.run)(@immut/hashmap.new())}"
  })
}

///|
pub fn Element::new(
  run : (@immut/hashmap.HashMap[String, String]) -> String,
) -> Element {
  { run, }
}

///|
pub fn Element::make_empty(name : String) -> Element {
  Element::new(fn(attrs) {
    let at = fold_map_with_key(init="", build_attr, attrs)
    "<\{name}\{at}/>"
  })
}

///|
pub fn Element::doc_type(name : String) -> Element {
  Element::new(fn(attrs) {
    let at = fold_map_with_key(init="", build_attr, attrs)
    ""
  })
}

///|
pub fn with_attr(self : Element, attr : Array[Attribute]) -> Element {
  Element::new(fn(a) { (self.run)(a.union(build_map(attr))) })
}

///|
fn build_attr(key : String, val : String) -> String {
  " \{key}" + (if val == "" { "" } else { "=\"\{html_escape(val)}\"" })
}

///|
fn[M : Add, K, V] fold_map_with_key(
  init~ : M,
  f : (K, V) -> M,
  m : @immut/hashmap.HashMap[K, V],
) -> M {
  m
  .iter()
  .fold(init~, fn(acc, entry) {
    let (k, v) = entry
    acc + f(k, v)
  })
}

///|
fn build_map(arr : Array[Attribute]) -> @immut/hashmap.HashMap[String, String] {
  let map = @immut/hashmap.new()
  let l = arr.length()
  loop (l, map) {
    (0, acc) => acc
    (i, acc) => {
      let val = arr[i - 1]
      continue (i - 1, acc.add(val.attr, val.value))
    }
  }
}

///|
pub fn term(name : String, arr : Array[Attribute], child? : Element) -> Element {
  match child {
    None => Element::make_empty(name).with_attr(arr)
    Some(child) => child.make(name).with_attr(arr)
  }
}

///|
pub fn Element::str(str : String) -> Element {
  Element::new(fn(_) { str })
}

///|
pub fn svg11(self : Element) -> Element {
  svg(
    [
      Version["1.1"],
      new("xmlns", "http://www.w3.org/2000/svg"),
      new("xmlns:xlink", "http://www.w3.org/1999/xlink"),
    ],
    child=self,
  )
}

///|
/// SVG element creation functions
pub fn doc_type() -> Element {
  Element::doc_type("svg")
}

///|
/// a element
pub fn a(attrs : Array[Attribute], child? : Element) -> Element {
  term("a", attrs, child?)
}

///|
/// circle element
pub fn circle(attrs : Array[Attribute], child? : Element) -> Element {
  term("circle", attrs, child?)
}

///|
/// clipPath element or attribute
pub fn clipPath(attrs : Array[Attribute], child? : Element) -> Element {
  term("clipPath", attrs, child?)
}

///|
/// color-profile element
pub fn colorProfile(attrs : Array[Attribute], child? : Element) -> Element {
  term("color-profile", attrs, child?)
}

///|
/// cursor element
pub fn cursor(attrs : Array[Attribute], child? : Element) -> Element {
  term("cursor", attrs, child?)
}

///|
/// defs element
pub fn defs(attrs : Array[Attribute], child? : Element) -> Element {
  term("defs", attrs, child?)
}

///|
/// desc element
pub fn desc(attrs : Array[Attribute], child? : Element) -> Element {
  term("desc", attrs, child?)
}

///|
/// ellipse element
pub fn ellipse(attrs : Array[Attribute], child? : Element) -> Element {
  term("ellipse", attrs, child?)
}

///|
/// feBlend element
pub fn feBlend(attrs : Array[Attribute], child? : Element) -> Element {
  term("feBlend", attrs, child?)
}

///|
/// feColorMatrix element
pub fn feColorMatrix(attrs : Array[Attribute], child? : Element) -> Element {
  term("feColorMatrix", attrs, child?)
}

///|
/// feComponentTransfer element
pub fn feComponentTransfer(
  attrs : Array[Attribute],
  child? : Element,
) -> Element {
  term("feComponentTransfer", attrs, child?)
}

///|
/// feComposite element
pub fn feComposite(attrs : Array[Attribute], child? : Element) -> Element {
  term("feComposite", attrs, child?)
}

///|
/// feConvolveMatrix element
pub fn feConvolveMatrix(attrs : Array[Attribute], child? : Element) -> Element {
  term("feConvolveMatrix", attrs, child?)
}

///|
/// feDiffuseLighting element
pub fn feDiffuseLighting(attrs : Array[Attribute], child? : Element) -> Element {
  term("feDiffuseLighting", attrs, child?)
}

///|
/// feDisplacementMap element
pub fn feDisplacementMap(attrs : Array[Attribute], child? : Element) -> Element {
  term("feDisplacementMap", attrs, child?)
}

///|
/// feDistantLight element
pub fn feDistantLight(attrs : Array[Attribute], child? : Element) -> Element {
  term("feDistantLight", attrs, child?)
}

///|
/// feFlood element
pub fn feFlood(attrs : Array[Attribute], child? : Element) -> Element {
  term("feFlood", attrs, child?)
}

///|
/// feFuncA element
pub fn feFuncA(attrs : Array[Attribute], child? : Element) -> Element {
  term("feFuncA", attrs, child?)
}

///|
/// feFuncB element
pub fn feFuncB(attrs : Array[Attribute], child? : Element) -> Element {
  term("feFuncB", attrs, child?)
}

///|
/// feFuncG element
pub fn feFuncG(attrs : Array[Attribute], child? : Element) -> Element {
  term("feFuncG", attrs, child?)
}

///|
/// feFuncR element
pub fn feFuncR(attrs : Array[Attribute], child? : Element) -> Element {
  term("feFuncR", attrs, child?)
}

///|
/// feGaussianBlur element
pub fn feGaussianBlur(attrs : Array[Attribute], child? : Element) -> Element {
  term("feGaussianBlur", attrs, child?)
}

///|
/// feImage element
pub fn feImage(attrs : Array[Attribute], child? : Element) -> Element {
  term("feImage", attrs, child?)
}

///|
/// feMerge element
pub fn feMerge(attrs : Array[Attribute], child? : Element) -> Element {
  term("feMerge", attrs, child?)
}

///|
/// feMergeNode element
pub fn feMergeNode(attrs : Array[Attribute], child? : Element) -> Element {
  term("feMergeNode", attrs, child?)
}

///|
/// feMorphology element
pub fn feMorphology(attrs : Array[Attribute], child? : Element) -> Element {
  term("feMorphology", attrs, child?)
}

///|
/// feOffset element
pub fn feOffset(attrs : Array[Attribute], child? : Element) -> Element {
  term("feOffset", attrs, child?)
}

///|
/// fePointLight element
pub fn fePointLight(attrs : Array[Attribute], child? : Element) -> Element {
  term("fePointLight", attrs, child?)
}

///|
/// feSpecularLighting element
pub fn feSpecularLighting(
  attrs : Array[Attribute],
  child? : Element,
) -> Element {
  term("feSpecularLighting", attrs, child?)
}

///|
/// feSpotLight element
pub fn feSpotLight(attrs : Array[Attribute], child? : Element) -> Element {
  term("feSpotLight", attrs, child?)
}

///|
/// feTile element
pub fn feTile(attrs : Array[Attribute], child? : Element) -> Element {
  term("feTile", attrs, child?)
}

///|
/// feTurbulence element
pub fn feTurbulence(attrs : Array[Attribute], child? : Element) -> Element {
  term("feTurbulence", attrs, child?)
}

///|
/// filter element
pub fn filter(attrs : Array[Attribute], child? : Element) -> Element {
  term("filter", attrs, child?)
}

///|
/// font element
pub fn font(attrs : Array[Attribute], child? : Element) -> Element {
  term("font", attrs, child?)
}

///|
/// font-face element
pub fn fontFace(attrs : Array[Attribute], child? : Element) -> Element {
  term("font-face", attrs, child?)
}

///|
/// font-face-src element
pub fn fontFaceSrc(attrs : Array[Attribute], child? : Element) -> Element {
  term("font-face-src", attrs, child?)
}

///|
/// font-face-uri element
pub fn fontFaceUri(attrs : Array[Attribute], child? : Element) -> Element {
  term("font-face-uri", attrs, child?)
}

///|
/// foreignObject element
pub fn foreignObject(attrs : Array[Attribute], child? : Element) -> Element {
  term("foreignObject", attrs, child?)
}

///|
/// g element
pub fn g(attrs : Array[Attribute], child? : Element) -> Element {
  term("g", attrs, child?)
}

///|
/// glyph element or attribute
pub fn glyph(attrs : Array[Attribute], child? : Element) -> Element {
  term("glyph", attrs, child?)
}

///|
/// image element
pub fn image(attrs : Array[Attribute], child? : Element) -> Element {
  term("image", attrs, child?)
}

///|
/// line element
pub fn line(attrs : Array[Attribute], child? : Element) -> Element {
  term("line", attrs, child?)
}

///|
/// linearGradient element
pub fn linearGradient(attrs : Array[Attribute], child? : Element) -> Element {
  term("linearGradient", attrs, child?)
}

///|
/// marker element
pub fn marker(attrs : Array[Attribute], child? : Element) -> Element {
  term("marker", attrs, child?)
}

///|
/// mask element or attribute
pub fn mask(attrs : Array[Attribute], child? : Element) -> Element {
  term("mask", attrs, child?)
}

///|
/// metadata element
pub fn metadata(attrs : Array[Attribute], child? : Element) -> Element {
  term("metadata", attrs, child?)
}

///|
/// missing-glyph element
pub fn missingGlyph(attrs : Array[Attribute], child? : Element) -> Element {
  term("missing-glyph", attrs, child?)
}

///|
/// mpath element
pub fn mpath(attrs : Array[Attribute], child? : Element) -> Element {
  term("mpath", attrs, child?)
}

///|
/// path element
pub fn path(attrs : Array[Attribute], child? : Element) -> Element {
  term("path", attrs, child?)
}

///|
/// pattern element
pub fn pattern(attrs : Array[Attribute], child? : Element) -> Element {
  term("pattern", attrs, child?)
}

///|
/// polygon element
pub fn polygon(attrs : Array[Attribute], child? : Element) -> Element {
  term("polygon", attrs, child?)
}

///|
/// polyline element
pub fn polyline(attrs : Array[Attribute], child? : Element) -> Element {
  term("polyline", attrs, child?)
}

///|
/// radialGradient element
pub fn radialGradient(attrs : Array[Attribute], child? : Element) -> Element {
  term("radialGradient", attrs, child?)
}

///|
/// rect element
pub fn rect(attrs : Array[Attribute], child? : Element) -> Element {
  term("rect", attrs, child?)
}

///|
/// script element
pub fn script(attrs : Array[Attribute], child? : Element) -> Element {
  term("script", attrs, child?)
}

///|
/// set element
pub fn set(attrs : Array[Attribute], child? : Element) -> Element {
  term("set", attrs, child?)
}

///|
/// stop element
pub fn stop(attrs : Array[Attribute], child? : Element) -> Element {
  term("stop", attrs, child?)
}

///|
/// style element
pub fn style(attrs : Array[Attribute], child? : Element) -> Element {
  term("style", attrs, child?)
}

///|
/// svg element
pub fn svg(attrs : Array[Attribute], child? : Element) -> Element {
  term("svg", attrs, child?)
}

///|
/// switch element
pub fn switch(attrs : Array[Attribute], child? : Element) -> Element {
  term("switch", attrs, child?)
}

///|
/// symbol element
pub fn symbol(attrs : Array[Attribute], child? : Element) -> Element {
  term("symbol", attrs, child?)
}

///|
/// text element
pub fn text(attrs : Array[Attribute], child? : Element) -> Element {
  term("text", attrs, child?)
}

///|
/// textPath element
pub fn textPath(attrs : Array[Attribute], child? : Element) -> Element {
  term("textPath", attrs, child?)
}

///|
/// title element
pub fn title(attrs : Array[Attribute], child? : Element) -> Element {
  term("title", attrs, child?)
}

///|
/// tref element
pub fn tref(attrs : Array[Attribute], child? : Element) -> Element {
  term("tref", attrs, child?)
}

///|
/// tspan element
pub fn tspan(attrs : Array[Attribute], child? : Element) -> Element {
  term("tspan", attrs, child?)
}

///|
/// use element
pub fn use_(attrs : Array[Attribute], child? : Element) -> Element {
  term("use", attrs, child?)
}

///|
/// view element
pub fn view(attrs : Array[Attribute], child? : Element) -> Element {
  term("view", attrs, child?)
}

///|
/// animate element
pub fn animate(attrs : Array[Attribute], child? : Element) -> Element {
  term("animate", attrs, child?)
}

///|
/// animateMotion element
pub fn animateMotion(attrs : Array[Attribute], child? : Element) -> Element {
  term("animateMotion", attrs, child?)
}

///|
/// animateTransform element
pub fn animateTransform(attrs : Array[Attribute], child? : Element) -> Element {
  term("animateTransform", attrs, child?)
}

///|
/// Create element without closing tag
fn element_no_end(name : String, attrs : Array[Attribute]) -> Element {
  Element::with_attr(Element::make_empty(name), attrs)
}

///|
/// font-face-format element
pub fn fontFaceFormat(attrs : Array[Attribute]) -> Element {
  element_no_end("font-face-format", attrs)
}

///|
/// font-face-name element
pub fn fontFaceName(attrs : Array[Attribute]) -> Element {
  element_no_end("font-face-name", attrs)
}

///|
/// glyphRef element
pub fn glyphRef(attrs : Array[Attribute]) -> Element {
  element_no_end("glyphRef", attrs)
}

///|
/// hkern element
pub fn hkern(attrs : Array[Attribute]) -> Element {
  element_no_end("hkern", attrs)
}

///|
/// vkern element
pub fn vkern(attrs : Array[Attribute]) -> Element {
  element_no_end("vkern", attrs)
}