if not modules then modules = { } end modules ['mlib-fnc'] = {
    version   = 1.001,
    comment   = "companion to mlib-ctx.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files",
}

-- The code below is based on an article
--
--     Efficient plotting the functions with discontinuities
--     Tomáš Bayer, Charles University in Prague, Faculty of Science
--     Geoinformatics FCE CTU 17(2), 2018, doi:10.14311/gi.17.2.2
--
-- with code titles "Adaptive sampling class" available at:
--
--    https://github.com/bayertom/sampling
--
-- author   : Tomáš Bayer
-- license  : GNU Lesser General Public License version 3 or later
-- copyright: 2017-2018.
--
-- We (Mikael Sundqvist and Hans Hagen) translated part to Lua and added
-- a few options as well as (we think) fixed begin/end point artifacts. So,
-- any errors are ours.

local abs, acos, sqrt, random = math.abs, math.acos, math.sqrt, math.random
local load, type = load, type
local find = string.find

local pi  = math.pi
local NaN = xmath.nan
local Inf = xmath.inf

local getparameterset = metapost.getparameterset

do

    local function curvature(x1, y1, x2, y2, x3, y3)
        -- angle between 2 line segments of the sampled line
        local dx1 = x3 - x2
        local dx2 = x1 - x2
        local dy1 = y3 - y2
        local dy2 = y1 - y2
        local cross = dx1*dy2 - dx2*dy1
        return sqrt(cross*cross)
    end

    -- Detect discontinuity using LR criterion

    local function discontinuity(f, xi, eps, fmin, fmax)
        local xi1  = xi   + eps
        local xi2  = xi1  + eps
        local x_i1 = xi   - eps
        local x_i2 = x_i1 - eps
        --
        local fxi = f(xi)
        if fxi == NaN or fxi == Inf or fxi < fmin or fxi > fmax then
            return true -- singularity at xi
        end
        local fxi1 = f(xi1)
        if fxi1 == NaN or fxi1 == Inf or fxi1 < fmin or fxi1 > fmax then
            return true -- singularity at xi1
        end
        local fxi2 = f(xi2)
        if fxi2 == NaN or fxi2 == Inf or fxi2 < fmin or fxi2 > fmax then
            return true -- singularity at xi2
        end
        local fx_i1 = f(x_i1)
        if fx_i1 == NaN or fx_i1 == Inf or fx_i1 < fmin or fx_i1 > fmax then
            return true -- singularity at x_i1
        end
        local fx_i2 = f(x_i2)
        if fx_i2 == NaN or fx_i2 == Inf or fx_i2 < fmin or fx_i2 > fmax then
            return true -- singularity at x_i2
        end
        -- LR criterion
        local fr = 3 * fxi - 4 * fxi1  + fxi2
        local fl = 3 * fxi - 4 * fx_i1 + fx_i2
        local rr = fr * fr
        local ll = fl * fl
        local lr = abs((rr - ll) / (rr + ll + 0.0001))
        --
        return lr > 0.8  -- possible discontinuity when true
    end

    -- Adaptive sampling, recursive implementation, method M3

    local function adaptivesampling(points, f, a, b, ya, yb, d, dmin, dmax, fmin, fmax, eps, ff)
        -- 3 recursive calls are used, split to 4 subintervals
        if d >= dmax then
          return -- we're done
        end
        local delta = b - a
        if delta < eps then
            return -- interval is too narrow
        end
        -- take 3 random points inside the interval (u,v)
        local r1 = 0.45 + 0.1*random()
        local r2 = 0.45 + 0.1*random()
        local r3 = 0.45 + 0.1*random()
        --
        local x1 = a + r1 * delta * 0.5
        local x2 = a + r2 * delta
        local x3 = a + r3 * delta * 1.5
        -- function vaules at x1, x2, x3
        local y1 = f(x1)
        local y2 = f(x2)
        local y3 = f(x3)
        -- singular cases
        local heps = 0.5 * eps
        if discontinuity(f, x1, heps, fmin, fmax) then
            return x1
        end
        if discontinuity(f, x2, heps, fmin, fmax) then
            return x2
        end
        if discontinuity(f, x3, heps, fmin, fmax) then
            return x3
        end
        -- check if another recursive step is required
        local alpha1 = curvature( a, ya, x1, y1, x2, y2)
        local alpha2 = curvature(x1, y1, x2, y2, x3, y3)
        local alpha3 = curvature(x2, y2, x3, y3,  b, yb)
        -- we're not flat, recursion depth < dmax
        if alpha1 > ff or alpha2 > ff or alpha3 > ff or d < dmin then
            -- first sub interval
            if alpha1 > ff or d < dmin then
                local r = adaptivesampling(points, f, a, x1, ya, y1, d+1, dmin, dmax, fmin, fmax, eps, ff)
                if r then
                    return r
                end
            end
            points[#points+1] = { x1, y1 }
            -- second sub interval
            if alpha1 > ff or alpha2 > ff or d < dmin then
                local r = adaptivesampling(points, f, x1, x2, y1, y2, d+1, dmin, dmax, fmin, fmax, eps, ff)
                if r then
                    return r
                end
            end
            points[#points+1] = { x2, y2 }
            -- third sub interval
            if alpha2 > ff or alpha3 > ff or d < dmin then
                local r = adaptivesampling(points, f, x2, x3, y2, y3, d+1, dmin, dmax, fmin, fmax, eps, ff)
                if r then
                    return r
                end
            end
            points[#points+1] = { x3, y3 }
            -- fourth sub interval
            if alpha3 > ff or d < dmin then
                local r = adaptivesampling(points, f, x3, b, y3, yb, d+1, dmin, dmax, fmin, fmax, eps, ff)
                if r then
                    return r
                end
            end
            -- no point
        end
    end

    -- adaptive sampling with discontinuities: recursive version

    local function samplefunction(fragments, f, a, b, fmin, fmax, s, smax, dmin, dmax, eps, ff)
        -- check for too many splits, a suspected function
        if s > smax then
            return fragments -- { }
        end
        -- skip incorrect interval
        if a > b then
            return fragments
        end
        -- skip empty interval
        if abs(b - a) < 2 * eps then
            return fragments
        end
        -- sub list
        local points = { }
        -- function values at a, b
        local fa = f(a)
        local fb = f(b)
        local c
        -- singular case at a
        local heps = 0.5 * eps
        if discontinuity(f, a, heps, fmin, fmax) then
            c = a
            goto singularity
        end
        -- singular case at b
        if discontinuity(f, b, heps, fmin, fmax) then
            c = b
            goto singularity
        end
        -- first point a
        points[#points+1] = { a, fa }
        -- adaptive sampling (recursive)
        c = adaptivesampling(points, f, a, b, fa, fb, 1, dmin, dmax, fmin, fmax, eps, ff)
        if c then
            goto singularity
        end
        -- last point b
        points[#points+1] = { b, fb }
        --
        fragments[#fragments+1] = points
        goto done
      ::singularity::
        if a <= c and abs(c - a) < eps then
            -- c is lower bound: shift lower bound
            a = a + eps
            fragments = samplefunction(fragments, f, a, b, fmin, fmax, s + 1, smax, dmin, dmax, eps, ff)
        elseif b >= c and abs(c - b) < eps then
            -- c is upper bound: shift upper bound
            b = b - eps
            fragments = samplefunction(fragments, f, a, b, fmin, fmax, s + 1, smax, dmin, dmax, eps, ff)
        elseif a < c and b > c and abs(c - a) > eps and abs(c - b) > eps then
            -- c inside interval (a, b): split interval into 2 subintervals
            local bl = c - eps
            local ar = c + eps
            -- first subinterval
            local teps = 2 * eps
            if a < bl and abs(bl - a) > teps then
                fragments = samplefunction(fragments, f, a, bl, fmin, fmax, s + 1, smax, dmin, dmax, eps, ff)
            end
            -- second subinterval
            if ar < b and abs(b - ar) > teps then
                fragments = samplefunction(fragments, f, ar, b, fmin, fmax, s + 1, smax, dmin, dmax, eps, ff)
            end
        end
       ::done::
        return fragments
    end

    ----------------------------

    local fragments = false

    function mp.lmt_samplefunctions_cleanup()
        fragments = false
    end

    function mp.lmt_samplefunction_do()
        local p = getparameterset("samplefunction")
        --
        local preamble  = p.preamble   or ""
        local code      = p.code       or "return x"
        local xmin      = p.xmin       or   -1 -- a
        local xmax      = p.xmax       or    1 -- b
        local ymin      = p.ymin       or   -1 -- fmin
        local ymax      = p.ymax       or    1 -- fmin
        local dmin      = p.dmin       or    3
        local dmax      = p.dmax       or   50
        local smax      = p.smax       or 5000
        local eps       = p.eps        or    0.001
        local tolerance = p.tolerance  or    0.0001

        if not find(code,"return") then
            code = "return " .. code
        end

        code = preamble .. " " .. "return function(x) " .. code .. " end"

        local action = load(code)

        if type(action) ~= "function" then
            return
        else
            action = action()
        end

        fragments = samplefunction(
            { }, action,
            xmin, xmax,
            ymin, ymax,
            0, smax,
            dmin, dmax,
            eps, tolerance
        )

        if fragments then
            fragments.curled = p.shape == "curve" or nil
            fragments.tense  = p.tight            or nil
        end
    end

    -- Timestamp: shape and tight support added (at the library end) early October
    -- 2025, celebrated by attending the dual-set RPWL concert in Zoetermeer NL.

    function mp.lmt_samplefunction_flush()
        if fragments then
            for i=1,#fragments do
                fragments[i] = {
                    path   = fragments[i],
                    append = true,
                }
            end
            if #fragments == 0 then
                fragments = { { 0, 0 } }
            end
-- fragments.trace = true
-- inspect(fragments)
            mp.inject.path(fragments)
            fragments = false
        end
    end

end

-- https://www.realtimerendering.com/resources/GraphicsGems/gemsv/ch4-4/aspc.c

do

    local function flat(x,y,t1,t2,tm,tolerance)
        local xp = x(t2) - x(tm)
        local yp = y(t2) - y(tm)
        local xq = x(t1) - x(tm)
        local yq = y(t1) - y(tm)
        local cross = xp*yq - xq*yp
        return cross*cross < tolerance*tolerance
    end

    local function sample(points,x,y,t1,t2,nesting,tolerance)
        local tm = t1 + (0.45 + 0.1*random()) * (t2 - t1)
        if flat(x,y,t1,t2,tm,tolerance) and nesting > 0 then
            points[#points+1] = { x(t1), y(t1) }
            points[#points+1] = { x(t2), y(t2) }
        else
            nesting = nesting + 1
            sample(points,x,y,t1,tm,nesting,tolerance)
            sample(points,x,y,tm,t2,nesting,tolerance)
        end
        return points
    end

    local function parametricplot(x,y,tmin,tmax,tolerance)
        return sample({},x,y,tmin,tmax,0,tolerance)
    end

    ----------------------------

    local points = false

    function mp.lmt_parametricplot_cleanup()
        points = false
    end

    function mp.lmt_parametricplot_do()
        local p = getparameterset("parametricplot")
        --
        local preamble  = p.preamble   or ""
        local xcode     = p.xcode      or "return t"
        local ycode     = p.ycode      or "return t"
        local rcode     = p.rcode      or nil
        local tmin      = p.tmin       or   -1
        local tmax      = p.tmax       or    1
        local tolerance = p.tolerance  or    0.0001
        if not find(xcode,"return") then
            xcode = "return " .. xcode
        end
        if not find(ycode,"return") then
            ycode = "return " .. ycode
        end

        if rcode then
            xcode = preamble .. " " .. "return function(t) return (" .. rcode .. ") * cos(t) end"
            ycode = preamble .. " " .. "return function(t) return (" .. rcode .. ") * sin(t) end"
        else
            xcode = preamble .. " " .. "return function(t) " .. xcode .. " end"
            ycode = preamble .. " " .. "return function(t) " .. ycode .. " end"
        end

        local xaction = load(xcode)
        local yaction = load(ycode)

        if type(xaction) ~= "function" then
            return
        elseif type(yaction) ~= "function" then
            return
        else
            xaction = xaction()
            yaction = yaction()
        end
        --
        points = parametricplot(
            xaction, yaction,
            tmin, tmax,
            tolerance
        )
        -- todo: flush whole or flush segments
    end

    function mp.lmt_parametricplot_flush()
        if points then
            mp.inject.path(points)
        end
    end

end
