local base = require("packages.base")

local package = pl.class(base)
package._name = "linespacing"

local metrics = require("fontmetrics")

local metricscache = {}

local getLineMetrics = function (l)
  local linemetrics = { ascender = 0, descender = 0, lineheight = SILE.length() }
  if not l or not l.nodes then return linemetrics end
  for i = 1, #(l.nodes) do
    local node = l.nodes[i]
    if node.is_nnode then
      local m = metricscache[SILE.font._key(node.options)]
      if not m then
        local face = SILE.font.cache(node.options, SILE.shaper.getFace)
        m = metrics.get_typographic_extents(face)
        m.ascender = m.ascender * node.options.size
        m.descender = m.descender * node.options.size
        metricscache[SILE.font._key(node.options)] = m
      end
      SILE.settings:temporarily(function ()
        SILE.call("font", node.options, {})
        m.lineheight = SU.cast("length", SILE.settings:get("linespacing.css.line-height")):absolute()
      end)
      if m.ascender > linemetrics.ascender then linemetrics.ascender = m.ascender end
      if m.descender > linemetrics.descender then linemetrics.descender = m.descender end
      if m.lineheight > linemetrics.lineheight then linemetrics.lineheight = m.lineheight end
    end
  end
  return linemetrics
end

local linespacingLeading = function (_, vbox, previous)
  local method = SILE.settings:get("linespacing.method")

  local firstline = SILE.settings:get("linespacing.minimumfirstlineposition"):absolute()
  if not previous then
    if firstline.length:tonumber() > 0 then
      local toAdd = SILE.length(firstline.length - vbox.height)
      return SILE.nodefactory.vkern(toAdd)
    else
      return nil
    end
  end

  if method == "tex" then
    return SILE.typesetters.base:leadingFor(vbox, previous)
  end

  if method == "fit-glyph" then
    local extra = SILE.settings:get("linespacing.fit-glyph.extra-space"):absolute()
    local toAdd = SILE.length(extra)
    return SILE.nodefactory.vglue(toAdd)
  end

  if method == "fixed" then
    local btob = SILE.settings:get("linespacing.fixed.baselinedistance"):absolute()
    local toAdd = SILE.length(btob.length - (vbox.height + previous.depth), btob.stretch, btob.shrink)
    return SILE.nodefactory.vglue(toAdd)
  end

  -- For these methods, we need to read the font metrics
  if not metrics then
    SU.error("'"..method.."' line spacing method requires font metrics module, which is not available.")
  end

  local thismetrics = getLineMetrics(vbox)
  local prevmetrics = getLineMetrics(previous)
  if method == "fit-font" then
    -- Distance to next baseline is max(descender) of fonts on previous +
    -- max(ascender) of fonts on next
    local extra = SILE.settings:get("linespacing.fit-font.extra-space"):absolute()
    local btob = prevmetrics.descender + thismetrics.ascender + extra
    local toAdd = btob - (vbox.height + (previous and previous.depth or 0))
    return SILE.nodefactory.vglue(toAdd)
  end

  if method == "css" then
    local lh = prevmetrics.lineheight
    local leading = (lh - (prevmetrics.ascender + prevmetrics.descender))
    if previous then
      previous.height = previous.height + leading / 2
      previous.depth = previous.depth + leading / 2
    end
    return SILE.nodefactory.vglue()

  end

  SU.error("Unknown line spacing method "..method)
end

function package:_init ()
  base._init(self)
  self.class:registerPostinit(function(_)
    SILE.typesetter.leadingFor = linespacingLeading
  end)
end

function package.declareSettings (_)

  SILE.settings:declare({
    parameter = "linespacing.method",
    default = "tex",
    type = "string",
    help = "How to set the line spacing (tex, fixed, fit-font, fit-glyph, css)"
  })

  SILE.settings:declare({
    parameter = "linespacing.fixed.baselinedistance",
    default = SILE.length("1.2em"),
    type = "length",
    help = "Distance from baseline to baseline in the case of fixed line spacing"
  })

  SILE.settings:declare({
    parameter = "linespacing.minimumfirstlineposition",
    default = SILE.length(0),
    type = "length"
  })

  SILE.settings:declare({
    parameter = "linespacing.fit-glyph.extra-space",
    default = SILE.length(0),
    type = "length"
  })

  SILE.settings:declare({
    parameter = "linespacing.fit-font.extra-space",
    default = SILE.length(0),
    type = "length"
  })

  SILE.settings:declare({
    parameter = "linespacing.css.line-height",
    default = SILE.length("1.2em"),
    type = "length"
  })

end

function package:registerCommands ()

  self:registerCommand("linespacing-on", function ()
    SILE.typesetter.leadingFor = linespacingLeading
  end)

  self:registerCommand("linespacing-off", function ()
    SILE.typesetter.leadingFor = SILE.typesetters.base.leadingFor
  end)

end

package.documentation = [[
\begin{document}
\linespacing-on
SILE’s default method of inserting leading between lines should be familiar to users of TeX, but it is not the most friendly system for book designers.
The \autodoc:package{linespacing} package provides a better choice of leading systems.

After loading the package, you are able to choose the linespacing mode by setting the \autodoc:setting{linespacing.method} parameter.
The following examples have funny sized words in them so that you can see how the different methods interact.

By default, this is set to \code{tex}. The other options available are:

\medskip
\set[parameter=linespacing.method,value=fixed]
\set[parameter=linespacing.fixed.baselinedistance,value=1.5em]
\begin{itemize}
\item{\code{fixed}. This set the lines at a fixed baseline-to-baseline distance, determined by the \autodoc:setting{linespacing.fixed.baselinedistance} parameter.
You can specify this parameter either relative to the type size (\code{1.2em}) or as a absolute distance (\code{15pt}).
This paragraph is set with a fixed 1.5em baseline-to-baseline distance.}
\end{itemize}

\medskip
\set[parameter=linespacing.method,value=fit-glyph]
\begin{itemize}
\item{\code{fit-glyph}. This sets the lines solid; that is, the lowest point on line 1 (either a descender like \font[size=20pt]{q} or, if there are no descenders, the baseline) will touch the \font[size=20pt]{highest} point of line 2, as in this paragraph.
You generally don’t want to use this as-is.}
\end{itemize}

\set[parameter=linespacing.fit-glyph.extra-space,value=5pt]

What you probably want to do is insert a constant (relative or absolute) s\font[size=20pt]{p}ace between the lines by setting the \autodoc:setting{linespacing.fit-glyph.extra-space} parameter.
\font[size=20pt]{T}his paragraph is set with 5 points of space between the descenders and the ascenders.

\medskip
\set[parameter=linespacing.method,value=fit-font]
\begin{itemize}
\item{\code{fit-font}. This inspects each \code{hbox} on the line, and asks the fonts it finds for their bounding boxes—the highest ascender and the lower descender.
It then sets the lines solid.
Essentially each character is treated as if it is the same height, rather like composing a slug of metal type.
If there are things other than text on your line, or the text is buried inside other boxes, this may not work well.}
\end{itemize}

\set[parameter=linespacing.fit-font.extra-space,value=5pt]

As with \code{fit-glyph}, you can insert extra space between the lines with the \autodoc:setting{linespacing.fit-font.extra-space} parameter.

\medskip
\set[parameter=linespacing.method,value=css]
\set[parameter=linespacing.css.line-height,value=2em]
\begin{itemize}
\item{\code{css}. This is similar to the method used in browsers; the baseline distance is set with the \autodoc:setting{linespacing.css.line-height} parameter, and the excess \font[size=20pt]{space} between this parameter and the actual height of the line is distributed between the top and bottom of the line.}
\end{itemize}
\medskip

\linespacing-off
\end{document}
]]

return package
