################################################################################
#                    Poly - the not so primitive, primitive                    #
################################################################################

deref(x) = x
deref(x::Base.RefValue) = x[]

"""
Special method for polys so we don't fall back to atomic meshes, which are much more
complex and slower to draw than standard paths with single color.
"""
function draw_plot(scene::Scene, screen::Screen, poly::Poly)
    # dispatch on input arguments to poly to use smarter drawing methods than
    # meshes if possible.
    # however, since recipes exist, we can't explicitly handle all cases here
    # so, we should also take a look at converted
    # First, we check whether a `draw_poly` method exists for the input arguments
    # before conversion:
    if Base.hasmethod(draw_poly, Tuple{Scene, Screen, typeof(poly), typeof.(deref(poly.args[]))...})
        return draw_poly(scene, screen, poly, deref(poly.args[])...)
        # If not, we check whether a `draw_poly` method exists for the arguments after conversion
        # (`plot.converted`).  This allows anything which decomposes to be checked for.
    elseif Base.hasmethod(draw_poly, Tuple{Scene, Screen, typeof(poly), typeof.(deref(poly.converted[]))...})
        return draw_poly(scene, screen, poly, deref(poly.converted[])...)
        # In the worst case, we return to drawing the polygon as a mesh + lines.
    else
        return draw_poly_as_mesh(scene, screen, poly)
    end
end

# Override `is_cairomakie_atomic_plot` to allow `poly` to remain a unit,
# instead of auto-decomposing in lines and mesh.
is_cairomakie_atomic_plot(plot::Poly) = true


function draw_poly_as_mesh(scene, screen, poly)
    for i in eachindex(poly.plots)
        draw_plot(scene, screen, poly.plots[i])
    end
    return
end

########################################
### outline methods (::Vector{<:VecTypes{2}})
########################################

function draw_poly(scene::Scene, screen::Screen, poly, points::Vector{<:Point2})
    color = to_cairo_color(poly.color[], poly)
    strokecolor = to_cairo_color(poly.strokecolor[], poly)
    strokestyle = Makie.convert_attribute(poly.linestyle[], key"linestyle"())

    miter_limit = to_cairo_miter_limit(poly.miter_limit[])
    joinstyle = to_cairo_joinstyle(poly.joinstyle[])
    linecap = to_cairo_linecap(poly.linecap[])

    draw_poly(
        scene, screen, poly, points, color,
        poly.model[], strokecolor, strokestyle, poly.strokewidth[], miter_limit, joinstyle, linecap
    )
    if color isa Cairo.CairoPattern
        pattern_set_matrix(color, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0))
    end
    return
end

function draw_poly(scene::Scene, screen::Screen, poly, points_list::Vector{<:Vector{<:Point2}})
    color = to_cairo_color(poly.color[], poly)
    strokecolor = to_cairo_color(poly.strokecolor[], poly)
    strokestyle = Makie.convert_attribute(poly.linestyle[], key"linestyle"())

    miter_limit = to_cairo_miter_limit(poly.miter_limit[])
    joinstyle = to_cairo_joinstyle(poly.joinstyle[])
    linecap = to_cairo_linecap(poly.linecap[])

    broadcast_foreach(
        points_list, color,
        strokecolor, strokestyle, poly.strokewidth[], Ref(poly.model[])
    ) do points, color, strokecolor, strokestyle, strokewidth, model
        draw_poly(scene, screen, poly, points, color, model, strokecolor, strokestyle, strokewidth, miter_limit, joinstyle, linecap)
    end
    if color isa Cairo.CairoPattern
        pattern_set_matrix(color, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0))
    end
    return
end

draw_poly(scene::Scene, screen::Screen, poly, circle::Circle) = draw_poly(scene, screen, poly, decompose(Point2f, circle))

# when color is a Makie.AbstractPattern, we don't need to go to Mesh
function draw_poly(
        scene::Scene, screen::Screen, poly, points::Vector{<:Point2}, color::Union{Colorant, Cairo.CairoPattern},
        model, strokecolor, strokestyle, strokewidth, miter_limit, joinstyle, linecap
    )
    space = poly.space[]
    points = apply_transform(transform_func(poly), points)
    points = clip_poly(poly.clip_planes[], points, space, model)
    points = _project_position(scene, space, points, model, true)

    Cairo.move_to(screen.context, points[1]...)
    for p in points[2:end]
        Cairo.line_to(screen.context, p...)
    end
    Cairo.close_path(screen.context)

    set_source(screen.context, color)

    Cairo.fill_preserve(screen.context)
    Cairo.set_source_rgba(screen.context, rgbatuple(to_color(strokecolor))...)
    Cairo.set_line_width(screen.context, strokewidth)
    set_miter_limit(screen.context, miter_limit)
    Cairo.set_line_join(screen.context, joinstyle)
    Cairo.set_line_cap(screen.context, linecap)

    pattern = to_cairo_linestyle(strokestyle, strokewidth)
    if !isnothing(pattern)
        Cairo.set_dash(screen.context, pattern)
    end
    Cairo.stroke(screen.context)
    return
end

# As a general fallback, draw all polys as meshes.
# This also applies for e.g. per-vertex color.
function draw_poly(
        scene::Scene, screen::Screen, poly, points, color,
        model, strokecolor, strokestyle, strokewidth, miter_limit, joinstyle, linecap
    )
    return draw_poly_as_mesh(scene, screen, poly)
end

########################################
### GeometryPrimtive and BezierPath methods
########################################

draw_poly(scene::Scene, screen::Screen, poly, rect::Rect2) = draw_poly(scene, screen, poly, [rect])
draw_poly(scene::Scene, screen::Screen, poly, bezierpath::BezierPath) = draw_poly(scene, screen, poly, [bezierpath])

function draw_poly(scene::Scene, screen::Screen, poly, shapes::Vector{<:Union{Rect2, BezierPath}})
    model = poly.model[]::Mat4d
    space = poly.space[]::Symbol
    planes = poly.clip_planes[]::Vector{Plane3f}

    projected_shapes = map(shapes) do shape
        clipped = clip_shape(planes, shape, space, model)
        return project_shape(poly, space, clipped, model)
    end

    color = to_cairo_color(poly.color[], poly)

    linestyle = Makie.convert_attribute(poly.linestyle[], key"linestyle"())
    if isnothing(linestyle)
        linestyle_diffed = nothing
    elseif linestyle isa AbstractVector{<:Real}
        linestyle_diffed = diff(Float64.(linestyle))
    else
        error("Wrong type for linestyle: $(poly.linestyle[]).")
    end
    strokecolor = to_cairo_color(poly.strokecolor[], poly)

    miter_limit = to_cairo_miter_limit(poly.miter_limit[])
    joinstyle = to_cairo_joinstyle(poly.joinstyle[])
    linecap = to_cairo_linecap(poly.linecap[])

    broadcast_foreach(projected_shapes, color, strokecolor, poly.strokewidth[]) do shape, c, sc, sw
        create_shape_path!(screen.context, shape)
        set_source(screen.context, c)
        Cairo.fill_preserve(screen.context)
        pattern = to_cairo_linestyle(linestyle, sw)
        if !isnothing(pattern)
            Cairo.set_dash(screen.context, pattern)
        end
        set_source(screen.context, sc)
        Cairo.set_line_width(screen.context, sw)
        set_miter_limit(screen.context, miter_limit)
        Cairo.set_line_join(screen.context, joinstyle)
        Cairo.set_line_cap(screen.context, linecap)
        Cairo.stroke(screen.context)
    end
    if color isa Cairo.CairoPattern
        pattern_set_matrix(color, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0))
    end
    return
end

function project_shape(scene, space, shape::BezierPath, model)
    commands = Makie.PathCommand[]
    for cmd in shape.commands
        if cmd isa EllipticalArc
            bezier = Makie.elliptical_arc_to_beziers(cmd)
            for b in bezier.commands
                push!(commands, project_command(b, scene, space, model))
            end
        else
            push!(commands, project_command(cmd, scene, space, model))
        end
    end
    return BezierPath(commands)
end

function create_shape_path!(ctx, r::Rect2)
    return Cairo.rectangle(ctx, origin(r)..., widths(r)...)
end

function create_shape_path!(ctx, b::BezierPath)
    isempty(b.commands) && return
    if !isa(first(b.commands), MoveTo)
        error("The first command in a BezierPath must be `MoveTo`, but is $(first(b.commands)).")
    end

    for cmd in b.commands
        path_command(ctx, cmd)
    end

    # match W/GLMakie
    if !isa(last(b.commands), ClosePath)
        Cairo.close_path(ctx)
    end
    return
end

function polypath(ctx, polygon)
    isempty(polygon) && return nothing
    ext = decompose(Point2f, polygon.exterior)
    Cairo.set_fill_type(ctx, Cairo.CAIRO_FILL_RULE_EVEN_ODD)
    Cairo.move_to(ctx, ext[1]...)
    for point in ext[2:end]
        Cairo.line_to(ctx, point...)
    end
    Cairo.close_path(ctx)
    interiors = decompose.(Point2f, polygon.interiors)
    for interior in interiors
        # Cairo needs to have interiors counter clockwise
        n = length(interior)
        Cairo.move_to(ctx, interior[1]...)
        for idx in 2:n
            point = interior[idx]
            Cairo.line_to(ctx, point...)
        end
        Cairo.close_path(ctx)
    end
    return
end

draw_poly(scene::Scene, screen::Screen, poly, polygon::Polygon) = draw_poly(scene, screen, poly, [polygon])
draw_poly(scene::Scene, screen::Screen, poly, multipolygon::MultiPolygon) = draw_poly(scene, screen, poly, multipolygon.polygons)

function draw_poly(scene::Scene, screen::Screen, poly, polygons::AbstractArray{<:Polygon})
    model = poly.model[]
    space = poly.space[]
    projected_polys = map(polygons) do polygon
        return project_polygon(poly, space, polygon, poly.clip_planes[], model)
    end

    color = to_cairo_color(poly.color[], poly)
    strokecolor = to_cairo_color(poly.strokecolor[], poly)
    strokestyle = Makie.convert_attribute(poly.linestyle[], key"linestyle"())

    miter_limit = to_cairo_miter_limit(poly.miter_limit[])
    joinstyle = to_cairo_joinstyle(poly.joinstyle[])
    linecap = to_cairo_linecap(poly.linecap[])

    broadcast_foreach(projected_polys, color, strokecolor, poly.strokewidth[]) do po, c, sc, sw
        polypath(screen.context, po)
        set_source(screen.context, c)
        Cairo.fill_preserve(screen.context)
        set_source(screen.context, sc)
        Cairo.set_line_width(screen.context, sw)
        pattern = to_cairo_linestyle(strokestyle, sw)
        if !isnothing(pattern)
            Cairo.set_dash(screen.context, pattern)
        end
        set_miter_limit(screen.context, miter_limit)
        Cairo.set_line_join(screen.context, joinstyle)
        Cairo.set_line_cap(screen.context, linecap)
        Cairo.stroke(screen.context)
    end

    if color isa Cairo.CairoPattern
        pattern_set_matrix(color, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0))
    end
    return
end

function draw_poly(scene::Scene, screen::Screen, poly, polygons::AbstractArray{<:MultiPolygon})
    model = poly.model[]
    space = poly.space[]
    projected_polys = map(polygons) do polygon
        project_multipolygon(poly, space, polygon, poly.clip_planes[], model)
    end

    color = to_cairo_color(poly.color[], poly)
    strokecolor = to_cairo_color(poly.strokecolor[], poly)
    strokestyle = Makie.convert_attribute(poly.linestyle[], key"linestyle"())

    miter_limit = to_cairo_miter_limit(poly.miter_limit[])
    joinstyle = to_cairo_joinstyle(poly.joinstyle[])
    linecap = to_cairo_linecap(poly.linecap[])
    broadcast_foreach(projected_polys, color, strokecolor, poly.strokewidth[]) do mpo, c, sc, sw
        for po in mpo.polygons
            polypath(screen.context, po)
            set_source(screen.context, c)
            Cairo.fill_preserve(screen.context)
            set_source(screen.context, sc)
            Cairo.set_line_width(screen.context, sw)
            pattern = to_cairo_linestyle(strokestyle, sw)
            if !isnothing(pattern)
                Cairo.set_dash(screen.context, pattern)
            end
            set_miter_limit(screen.context, miter_limit)
            Cairo.set_line_join(screen.context, joinstyle)
            Cairo.set_line_cap(screen.context, linecap)
            Cairo.stroke(screen.context)
        end
    end

    if color isa Cairo.CairoPattern
        pattern_set_matrix(color, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0))
    end
    return
end


################################################################################
#                                     Band                                     #
#     Override because band is usually a polygon, but because it supports      #
#        gradients as well via `mesh` we have to intercept the poly use        #
################################################################################

function band_segment_ranges(lowerpoints, upperpoints)
    ranges = UnitRange{Int}[]
    start = nothing

    for i in eachindex(lowerpoints, upperpoints)
        if isnan(lowerpoints[i]) || isnan(upperpoints[i])
            if start !== nothing && i - start > 1 # more than one point
                push!(ranges, start:(i - 1))
            end
            start = nothing
        elseif start === nothing
            start = i
        elseif i == lastindex(lowerpoints)
            push!(ranges, start:i)
        end
    end
    return ranges
end

# we can draw an array-colored band using a linear gradient if it's a "normal" band
# in which all upper/lower values with the same index are at the same x value.
# the linear gradient will look much better than falling back to mesh drawing, which
# results in many little triangles when colors are semitransparent and a jagged corner
function is_linear_gradient_compatible(band)
    return all(zip(band[1][], band[2][])) do (p1, p2)
        p1 isa Point2 && p2 isa Point2 && p1[1] == p2[1]
    end
end

function draw_plot(
        scene::Scene, screen::Screen,
        band::Band{<:Tuple{<:AbstractVector{<:Point2}, <:AbstractVector{<:Point2}}}
    )
    if is_linear_gradient_compatible(band) || !(band.color[] isa AbstractArray)
        color = to_cairo_color(band.color[], band)

        model = band.model[]
        space = band.space[]

        xdir::Bool = band.direction[] === :x

        upperpoints = xdir ? band[1][] : reverse.(band[1][])
        lowerpoints = xdir ? band[2][] : reverse.(band[2][])

        for rng in band_segment_ranges(lowerpoints, upperpoints)
            points_segment = vcat(@view(lowerpoints[rng]), reverse(@view(upperpoints[rng])))
            points = clip_poly(band.clip_planes[], points_segment, space, model)
            points = project_position.(Ref(band), space, points, Ref(model))
            Cairo.move_to(screen.context, points[1]...)
            for p in points[2:end]
                Cairo.line_to(screen.context, p...)
            end
            Cairo.close_path(screen.context)
            if color isa AbstractVector
                # for the gradient we use all points, irrespective of clipping
                lower_proj = project_position.(Ref(band), space, points_segment[1:(end ÷ 2)], Ref(model))
                p_first = lower_proj[begin]
                p_last = lower_proj[end]
                P = typeof(p_last)
                # the gradient must be parallel to x or y axis
                p_last = xdir ? P(p_last[1], p_first[2]) : P(p_first[1], p_last[2])
                dist = p_last - p_first
                pat = Cairo.pattern_create_linear(p_first..., p_last...)
                for (p, c) in zip(lower_proj, color)
                    i = xdir ? 1 : 2
                    fraction = (p[i] - p_first[i]) / dist[i]
                    Cairo.pattern_add_color_stop_rgba(pat, fraction, red(c), green(c), blue(c), alpha(c))
                end
                Cairo.set_source(screen.context, pat)
                Cairo.fill(screen.context)
                Cairo.destroy(pat)
            else
                set_source(screen.context, color)
                Cairo.fill(screen.context)
            end
        end

        if color isa Cairo.CairoPattern
            pattern_set_matrix(color, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0))
        end

        for p in band.plots
            p isa Mesh && continue
            draw_plot(scene, screen, p)
        end
    else
        for p in band.plots
            draw_plot(scene, screen, p)
        end
    end

    return nothing
end

# Override `is_cairomakie_atomic_plot` to allow this dispatch of `band` to remain a unit,
# instead of auto-decomposing in lines and mesh.
function is_cairomakie_atomic_plot(plot::Band{<:Tuple{<:AbstractVector{<:Point2}, <:AbstractVector{<:Point2}}})
    return true
end

#################################################################################
#                                  Tricontourf                                  #
# Tricontourf creates many disjoint polygons that are adjacent and form contour #
#  bands, however, at the gaps we see white antialiasing artifacts. Therefore   #
#               we override behavior and draw each band in one go               #
#################################################################################

function draw_plot(scene::Scene, screen::Screen, tric::Tricontourf)

    pol = only(tric.plots)::Poly
    colornumbers = pol.color[]
    colors = to_cairo_color(colornumbers, pol)
    polygons = pol[1][]
    model = pol.model[]
    space = pol.space[]
    projected_polys = project_polygon.(Ref(tric), space, polygons, Ref(tric.clip_planes[]), Ref(model))

    function draw_tripolys(polys, colornumbers, colors)
        for (i, (pol, colnum, col)) in enumerate(zip(polys, colornumbers, colors))
            polypath(screen.context, pol)
            if i == length(colornumbers) || colnum != colornumbers[i + 1]
                set_source(screen.context, col)
                Cairo.fill(screen.context)
            end
        end
        return
    end

    draw_tripolys(projected_polys, colornumbers, colors)

    if colors isa Cairo.CairoPattern
        pattern_set_matrix(colors, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0))
    end

    return
end

# Override `is_cairomakie_atomic_plot` to allow `Tricontourf` to remain a unit,
# instead of auto-decomposing in lines and mesh.
function is_cairomakie_atomic_plot(plot::Tricontourf)
    return true
end


################################################################################
#                                   Arrows2D                                   #
#   The SVG backend does not support CairoMeshPattern, rasterizing the image   #
#  instead of using vector constructs.  Avoid a simple arrow rasterizing the   #
#  image by intercepting simple (typical) arrows and redirecting them through  #
#             the already special cased 2D polygon rendering path.             #
################################################################################

function draw_plot(scene::Scene, screen::Screen, arrow::Arrows2D)
    poly = only(arrow.plots)
    color = to_cairo_color(poly.color[], poly)
    model = Ref(poly.model[])
    strokecolor = to_cairo_color(poly.strokecolor[], poly)
    strokestyle = Makie.convert_attribute(poly.linestyle[], key"linestyle"())
    strokewidth = poly.strokewidth[]

    miter_limit = to_cairo_miter_limit(poly.miter_limit[])
    joinstyle = to_cairo_joinstyle(poly.joinstyle[])
    linecap = to_cairo_linecap(poly.linecap[])

    broadcast_foreach(
        poly.meshes[], color, model, strokecolor, strokestyle, strokewidth
    ) do mesh, props...
        points = [Point2(c[1], c[2]) for c in coordinates(mesh)]
        draw_poly(scene, screen, poly, points, props..., miter_limit, joinstyle, linecap)
    end
    return nothing
end

function is_cairomakie_atomic_plot(plot::Arrows2D)
    return true
end
