using FreeTypeAbstraction: hadvance, leftinkbound, inkwidth, get_extent, ascender, descender

one_attribute_per_char(attribute, string) = (attribute for char in string)

function one_attribute_per_char(font::NativeFont, string)
    return (find_font_for_char(char, font) for char in string)
end

function attribute_per_char(string, attribute)
    n_words = 0
    if attribute isa AbstractVector
        if length(attribute) == length(string)
            return attribute
        else
            n_words = length(split(string, r"\s+"))
            if length(attribute) == n_words
                i = 1
                return map(collect(string)) do char
                    f = attribute[i]
                    char == "\n" && (i += 1)
                    return f
                end
            end
        end
    else
        return one_attribute_per_char(attribute, string)
    end
    error("A vector of attributes with $(length(attribute)) elements was given but this fits neither the length of '$string' ($(length(string))) nor the number of words ($(n_words))")
end



"""
    layout_text(
        string::AbstractString, textsize::Union{AbstractVector, Number},
        font, align, rotation, justification, lineheight
    )

Compute a GlyphCollection for a `string` given textsize, font, align, rotation, model, justification, and lineheight.
"""
function layout_text(
        string::AbstractString, textsize::Union{AbstractVector, Number},
        font, align, rotation, justification, lineheight, color, strokecolor, strokewidth
    )

    ft_font = to_font(font)
    rscale = to_textsize(textsize)
    rot = to_rotation(rotation)

    fontperchar = attribute_per_char(string, ft_font)
    textsizeperchar = attribute_per_char(string, rscale)

    glyphcollection = glyph_collection(string, fontperchar, textsizeperchar, align[1],
        align[2], lineheight, justification, rot, color, strokecolor, strokewidth)

    return glyphcollection
end

"""
    glyph_collection(str::AbstractString, font_per_char, fontscale_px, halign, valign, lineheight_factor, justification, rotation, color)

Calculate the positions for each glyph in a string given a certain font, font size, alignment, etc.
This layout in text coordinates, relative to the anchor point [0,0] can then be translated and
rotated to wherever it is needed in the plot.
"""
function glyph_collection(str::AbstractString, font_per_char, fontscale_px, halign, valign,
        lineheight_factor, justification, rotation, color, strokecolor, strokewidth)

    isempty(str) && return GlyphCollection(
        [], [], Point3f[],FreeTypeAbstraction.FontExtent{Float32}[],
        Vec2f[], Float32[], RGBAf[], RGBAf[], Float32[])

    # collect information about every character in the string
    charinfos = broadcast((c for c in str), font_per_char, fontscale_px) do char, font, scale
        unscaled_extent = get_extent(font, char)
        lineheight = Float32(font.height / font.units_per_EM * lineheight_factor * scale)
        unscaled_hi_bb = height_insensitive_boundingbox(unscaled_extent, font)
        hi_bb = Rect2f(
            Makie.origin(unscaled_hi_bb) * scale,
            widths(unscaled_hi_bb) * scale)
        (char = char, font = font, scale = scale, hadvance = hadvance(unscaled_extent) * scale,
            hi_bb = hi_bb, lineheight = lineheight, extent = unscaled_extent)
    end

    # split the character info vector into lines after every \n
    lineinfos = let
        last_line_start = 1
        lineinfos = typeof(view(charinfos, last_line_start:last_line_start))[]
        for (i, ci) in enumerate(charinfos)
            if ci.char == '\n' || i == length(charinfos)
                push!(lineinfos, view(charinfos, last_line_start:i))
                last_line_start = i+1
            end
        end
        lineinfos
    end

    # calculate the x positions of each character in each line
    xs = map(lineinfos) do line
        cumsum([
            0f0;
            [l.hadvance for l in line[1:end-1]]
        ])
    end

    # calculate linewidths as the last origin plus hadvance for each line
    linewidths = map(lineinfos, xs) do line, xx
        nchars = length(line)
        # if the last and not the only character is \n, take the previous one
        # to compute the width
        i = (nchars > 1 && line[end].char == '\n') ? nchars - 1 : nchars
        xx[i] + line[i].hadvance
    end

    # the maximum width is needed for justification
    maxwidth = maximum(linewidths)

    # how much each line differs from the maximum width for justification correction
    width_differences = maxwidth .- linewidths

    # shift all x values by the justification amount needed for each line
    # if justification is automatic it depends on alignment
    float_justification = if justification === automatic
        if halign == :left || halign == 0
            0.0f0
        elseif halign == :right || halign == 1
            1.0f0
        elseif halign == :center || halign == 0.5
            0.5f0
        else
            0.5f0
        end
    elseif justification == :left
        0.0f0
    elseif justification == :right
        1.0f0
    elseif justification == :center
        0.5f0
    else
        Float32(justification)
    end

    xs_justified = map(xs, width_differences) do xsgroup, wd
        xsgroup .+ wd * float_justification
    end

    # each character carries a "lineheight" metric given its font and scale and a lineheight scaling factor
    # make each line's height the maximum of these values in the line
    lineheights = map(lineinfos) do line
        maximum(l -> l.lineheight, line)
    end

    # compute y values by adding up lineheights in negative y direction
    ys = cumsum([0.0; -lineheights[2:end]])

    # compute x values after left/center/right alignment
    halign = if halign isa Number
        Float32(halign)
    elseif halign == :left
        0.0f0
    elseif halign == :center
        0.5f0
    elseif halign == :right
        1.0f0
    else
        error("Invalid halign $halign. Valid values are <:Number, :left, :center and :right.")
    end
    xs_aligned = [xsgroup .- halign * maxwidth for xsgroup in xs_justified]

    # for y alignment, we need the largest ascender of the first line
    # and the largest descender of the last line
    first_line_ascender = maximum(lineinfos[1]) do l
        ascender(l.font) * l.scale
    end

    last_line_descender = minimum(lineinfos[end]) do l
        descender(l.font) * l.scale
    end

    # compute the height of all lines together
    overall_height = first_line_ascender - ys[end] - last_line_descender

    # compute y values after top/center/bottom/baseline alignment
    ys_aligned = if valign == :baseline
        ys .- first_line_ascender .+ overall_height .+ last_line_descender
    else
        va = if valign isa Number
            Float32(valign)
        elseif valign == :top
            1f0
        elseif valign == :bottom
            0f0
        elseif valign == :center
            0.5f0
        else
            error("Invalid valign $valign. Valid values are <:Number, :bottom, :baseline, :top, and :center.")
        end

        ys .- first_line_ascender .+ (1 - va) .* overall_height
    end

    # compute the origins for each character by rotating each character around the common origin
    # which is the alignment anchor and now [0, 0]
    # use 3D coordinates already because later they will be required in that format anyway
    charorigins = [Ref(rotation) .* Point3f.(xsgroup, y, 0) for (xsgroup, y) in zip(xs_aligned, ys_aligned)]

    # return a GlyphCollection, which contains each character's origin, height-insensitive
    # boundingbox and horizontal advance value
    # these values should be enough to draw characters correctly,
    # compute boundingboxes without relayouting and maybe implement
    # interactive features that need to know where characters begin and end
    per_char(attr) = collect(attribute_per_char(str, attr)) # attribute_per_char returns generators
    return GlyphCollection(
        [x.char for x in charinfos],
        [x.font for x in charinfos],
        reduce(vcat, charorigins),
        [x.extent for x in charinfos],
        [Vec2f(x.scale) for x in charinfos],
        per_char(rotation), # rotations is used as one rotation per string above. TODO, allow one rotation per char
        per_char(color),
        per_char(strokecolor),
        per_char(strokewidth)
    )
end

# function to concatenate vectors with a value between every pair
function padded_vcat(arrs::AbstractVector{T}, fillvalue) where T <: AbstractVector{S} where S
    n = sum(length.(arrs))
    arr = fill(convert(S, fillvalue), n + length(arrs) - 1)

    counter = 1
    @inbounds for a in arrs
        for v in a
            arr[counter] = v
            counter += 1
        end
        counter += 1
    end
    arr
end

function alignment2num(x::Symbol)
    (x == :center) && return 0.5f0
    (x in (:left, :bottom)) && return 0.0f0
    (x in (:right, :top)) && return 1.0f0
    return 0.0f0 # 0 default, or better to error?
end


# Backend data

_offset_to_vec(o::VecTypes) = to_ndim(Vec3f, o, 0)
_offset_to_vec(o::Vector) = to_ndim.(Vec3f, o, 0)
_offset_at(o::Vec3f, i) = o
_offset_at(o::Vector, i) = o[i]
Base.getindex(x::ScalarOrVector, i) = x.sv isa Vector ? x.sv[i] : x.sv

function text_quads(position::VecTypes, gc::GlyphCollection, offset, transfunc)
    p = apply_transform(transfunc, position)
    pos = [to_ndim(Point3f, p, 0) for _ in gc.origins]

    atlas = get_texture_atlas()
    pad = GLYPH_PADDING[] / PIXELSIZE_IN_ATLAS[]
    off = _offset_to_vec(offset)

    char_offsets = Vector{Vec3f}(undef, length(pos)) # TODO can this be Vec2f?
    quad_offsets = Vector{Vec2f}(undef, length(pos))
    scales = Vector{Vec2f}(undef, length(pos))
    uvs = Vector{Vec4f}(undef, length(pos))

    for i in eachindex(pos)
        glyph_bb, extent = FreeTypeAbstraction.metrics_bb(
            gc.glyphs[i], gc.fonts[i], gc.scales[i]
        )
        uvs[i] = glyph_uv_width!(atlas, gc.glyphs[i], gc.fonts[i])
        scales[i] = widths(glyph_bb) .+ gc.scales[i] .* 2pad
        char_offsets[i] = gc.origins[i] .+ _offset_at(off, i)
        quad_offsets[i] = minimum(glyph_bb) .- gc.scales[i] .* pad
    end

    # pos is the (space) position given to text (with transfunc applied)
    # char_offsets are 3D offsets in marker space, including:
    #   - offsets passed to text
    #   - character origins (relative to the string origin pos)
    # quad_offsets are 2D offsets of the quad origin to the character origins
    #   (these transform differently than character offsets, also marker space)
    # uvs are the texture coordinates of each character
    # scales is the size of the quad including textsize, character size and texture atlas padding
    return pos, char_offsets, quad_offsets, uvs, scales
end

function text_quads(position::Vector, gcs::Vector{<: GlyphCollection}, offset, transfunc)
    ps = apply_transform(transfunc, position)
    pos = [to_ndim(Point3f, p, 0) for (p, gc) in zip(ps, gcs) for _ in gc.origins]

    atlas = get_texture_atlas()
    pad = GLYPH_PADDING[] / PIXELSIZE_IN_ATLAS[]
    off = _offset_to_vec(offset)

    char_offsets = Vector{Vec3f}(undef, length(pos)) # TODO can this be Vec2f?
    quad_offsets = Vector{Vec2f}(undef, length(pos))
    scales = Vector{Vec2f}(undef, length(pos))
    uvs = Vector{Vec4f}(undef, length(pos))

    k = 1
    for (j, gc) in enumerate(gcs), i in eachindex(gc.origins)
        glyph_bb, extent = FreeTypeAbstraction.metrics_bb(
            gc.glyphs[i], gc.fonts[i], gc.scales[i]
        )
        uvs[k] = glyph_uv_width!(atlas, gc.glyphs[i], gc.fonts[i])
        scales[k] = widths(glyph_bb) .+ gc.scales[i] * 2pad
        char_offsets[k] = gc.origins[i] .+ _offset_at(off, j)
        quad_offsets[k] = minimum(glyph_bb) .- gc.scales[i] .* pad
        k += 1
    end

    return pos, char_offsets, quad_offsets, uvs, scales
end
