---- luapstricks-plugin-pstmarble.lua
-- Copyright 2021--2023 Marcel Krüger <tex@2krueger.de>
-- Converted from PostScript in pst-marble.pro version 1.6
-- pst-marble.pro: Copyright 2018--2019 Aubrey Jaffer
--
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License, either version 1.3
-- of this license or (at your option) any later version.
-- The latest version of this license is in
--   http://www.latex-project.org/lppl.txt
-- and version 1.3 or later is part of all distributions of LaTeX
-- version 2005/12/01 or later.
--
-- This work has the LPPL maintenance status `maintained'.
-- 
-- The Current Maintainer of this work is M. Krüger
--
-- This work consists of the files luapstricks.lua and luapstricks-plugin-pstmarble.lua

local loader, version, plugininterface = ...
assert(loader == 'luapstricks' and version == 0)

local push = plugininterface.push
local pop = plugininterface.pop
local pop_array = plugininterface.pop_array
local pop_num = plugininterface.pop_num
local pop_proc = plugininterface.pop_proc
local exec = plugininterface.exec

local newtable = lua.newtable
local abs = math.abs
local exp = math.exp
local cos = math.cos
local sin = math.sin
local rad = math.rad
local max = math.max
local deg = math.deg
local atan = math.atan

local function spread(px, py, cx, cy, rad2)
  local pc2 = (px - cx)^2 + (py - cy)^2 -- (p-c)^2
  local a = (rad2 / pc2 + 1)^0.5
  return (px - cx) * a + cx, (py - cy) * a + cy
end

local e_inv = math.exp(-1)
local function rake_deformation(px, py, dx, dy, rs, V, tU, Linv)
  local a = 0
  for i = 1, #rs do
    local r = rs[i]
    local bx, by = dy * r, -dx * r
    a = a + exp(-abs((px - bx) * dy - (py - by) * dx) * Linv) * tU
  end
  return px + dx * a, py + dy * a
end

local function stir_deformation(px, py, cx, cy, rs, th, Linv, oversample)
  local dx, dy = (px - cx), (py - cy)
  local dist = (dx^2 + dy^2)^.5
  if dist <= 1e-6 then return px, py end

  local a = 0
  for i = 1, #rs do
    local r = rs[i]
    local positive = r > 0
    if not positive then r = -r end
    local delta = exp(-abs(dist - r) * (Linv / r)) * th
    if positive then
      a = a - delta
    else
      a = a + delta
    end
  end
  if oversample > 0 then
    a = -a
  end
  a = rad(a)
  local cos_a, sin_a = cos(a), sin(a)
  return cos_a * dx - sin_a * dy + cx, sin_a * dx + cos_a * dy + cy
end

local function symmod(x, m)
  local x = x % m
  if 2 * x >= m then
    x = x - m
  end
  return x
end

-- Common code to compute inverse of non-linear deformation
local function g1(mdls, a, mf, af, major, pw, freq)
  local tmp = mdls / 2
  if a < 0 then tmp = -tmp end

  local tmp2
  if mf > 0 then
    tmp2 = 1 - max(1 - abs(af / 180), 0)^pw
  else
    tmp2 = abs(af / 180)^pw
  end
  local g0 = tmp * tmp2
--[[
    %% one iteration of Newton-Raphson
    %% g_1=g_0-(g_0-a+(m/2)*sin(g_0*f))/(1+pi*m*f/360*cos(g_0*f))
]]
  local gf = rad(g0 * freq)
  return g0 - (g0 - a + major * sin(gf)) / (1 + mf/2 * cos(gf))
end

local function jiggle_deformation(px, py, dx, dy, freq, ofst, trv, major, minor, mf, mdls, pw)
  local a = symmod(py * dx + px * dy + ofst, mdls)
  local af = a * freq
  local x, y
  if mf ~= 0 then
    --[[
      % find the minor axis displacement from the major axis value
      % where it was moved from.
    ]]
    local g = g1(mdls, a, mf, af, major, pw, freq)
    x, y = g - a, cos(rad(g * freq)) * minor
  else
    local ang = rad(af)
    x, y = sin(ang), -cos(ang)
    -- x, y = x * major, y * minor
  end
  return trv[1] * x + trv[3] * y + px, trv[2] * x + trv[4] * y + py
end

local function wriggle_deformation(px, py, cx, cy, freq, major, minor, mf, mdls, pw)
  local rd = ((px - cx)^2 + (py - cy)^2)^.5
  if rd <= 1e-6 then return px, py end

  local a = symmod(rd, mdls)
  local af = a * freq

  -- x, y are radial and angular displacements from cx,cy
  -- The naming is used to demonstrate the similarity with jiggle.
  local x, y
  if mf ~= 0 then
    local g = g1(mdls, a, mf, af, major, pw, freq)
    x, y = g - a, cos(rad(g * freq)) * minor
  else
    local ang = rad(af)
    x, y = sin(ang) * major, -cos(ang) * minor
  end
  rd = rd + x
  local ang = rad(y) + atan(px - cx, py - cy)

  return sin(ang) * rd + cx, cos(ang) * rd + cy
end

local function stylus_deformation(px, py, bx, by, ex, ey, L, tU, steps, nx, ny, step_x, step_y)
  for _ = 1, steps do
    local dxB, dyB = bx - px, by - py
    local dxE, dyE = ex - px, ey - py
    local r = (dxB^2 + dyB^2)^.5
    local denr = r / L
    if 0 < denr and denr < 6 then
      local s = (dxE^2 + dyE^2)^.5
      local txB = dxB * nx + dyB * ny
      local txE = dxE * nx + dyE * ny
      local ty = dxB * ny - dyB * nx
      denr = 2*L*r * exp(denr)
      local dens = 2*L*s * exp(s / L)
      local ty2 = ty^2
      local inx = (L*r - ty2) * tU / denr
                + (L*s - ty2) * tU / dens
      local iny = txB * ty    * tU / denr
                + txE * ty    * tU / dens
      px = px + inx * nx + iny * ny
      py = py + inx * ny - iny * nx
    end
    bx, by = ex, ey
    ex, ey = ex + step_x, ey + step_y
  end
  return px, py
end

-- An irrotational vortex.  circ is circulation; t is time in seconds
local m4o3 = -4/3
local function vortex_deformation(px, py, cx, cy, circ, t, nuterm)
  local pc2 = (px - cx)^2 + (py - cy)^2
  if pc2 < 1e-6 then return px, py end
  local a = rad((nuterm + (pc2 * t)^.75)^m4o3 * circ)
  px, py = px - cx, py - cy
  local cos_a, sin_a = cos(a), sin(a)
  return cos_a * px - sin_a * py + cx, sin_a * px + cos_a * py + cy
end

-- We don't actually gain much from moving this one to Lua, but it's more consistent
local function offset_deformation(px, py, dx, dy)
  return px + dx, py + dy
end

local function do_turn(px, py, cx, cy, trv)
  px, py = px - cx, py - cy
  return trv[1] * px + trv[3] * py + trv[5], trv[2] * px + trv[4] * py + trv[6]
end

local function ct_handler(handler)
  return function(px, py, args, count)
    push(px)
    push(py)

    for j = 1, count do
      push(args[j])
    end
    exec(handler)
    py = pop_num()
    px = pop_num()
    return px, py
  end
end

local function ct_dispatch_fallback(fallback)
  local handler = ct_handler(fallback)
  return function(ct, px, py, args, count)
    push{kind = 'name', value = 'ct'}
    push{kind = 'name', value = ct}
    exec'def'

    return handler(px, py, args, count)
  end
end

local ct_handlers = {
  offset = function(px, py, args, count)
    assert(count == 2)
    return offset_deformation(px, py, args[1], args[2])
  end,
  -- offset = ct_handler'offset-deformation',
  turn = function(px, py, args, count)
    assert(count == 3)
    return do_turn(px, py, args[1], args[2], args[3].value)
  end,
  -- turn = ct_handler'do-turn',
  jiggle = function(px, py, args, count)
    assert(count == 10)
    return jiggle_deformation(px, py, args[1], args[2], args[3], args[4], args[5].value, args[6], args[7], args[8], args[9], args[10])
  end,
  -- jiggle = ct_handler'jiggle-deformation',
  rake = function(px, py, args, count)
    assert(count == 6)
    return rake_deformation(px, py, args[1], args[2], args[3].value, args[4], args[5], args[6])
  end,
  -- rake = ct_handler'rake-deformation',
  vortex = function(px, py, args, count)
    assert(count == 4)
    exec'nuterm'
    local nuterm = pop_num()
    return vortex_deformation(px, py, args[1], args[2], args[3], args[4], nuterm)
  end,
  -- vortex = ct_handler'vortex-deformation',
  stir = function(px, py, args, count)
    assert(count == 5)
    exec'oversample'
    local oversample = pop_num()
    return stir_deformation(px, py, args[1], args[2], args[3].value, args[4], args[5], oversample)
  end,
  -- stir = ct_handler'stir-deformation',
  wriggle = function(px, py, args, count)
    assert(count == 8)
    return wriggle_deformation(px, py, args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8])
  end,
  -- wriggle = ct_handler'wriggle-deformation',
  stylus = function(px, py, args, count)
    assert(count == 11)
    return stylus_deformation(px, py, args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11])
  end,
  -- stylus = ct_handler'stylus-deformation',
}
local function ct_dispatch(fallback)
  fallback = ct_dispatch_fallback(fallback)
  return function(ct, px, py, args, count)
    local handler = ct_handlers[ct]
    if handler then
      return handler(px, py, args, count)
    else
      return fallback(ct, px, py, args, count)
    end
  end
end

local function sharpen(x)
  x = x - 0.5
  if abs(x) >= 1e-8 then
    x = x / abs(x)^.66 * .63
  end
  return x + 0.5
end

local function Vmap2(v1, v2, func)
  local result = newtable(#v2, 0)
  for i=1, #v1 do
    result[i] = func(v1[i], v2[i])
  end
  return result
end

local function paper_shading(rgb, pwr, paper)
  return Vmap2(rgb, paper, function(c1, c2)
    if c2 >= c1 then
      local a = 1 - c1/c2
      if a >= 1e-30 then
        a = a^pwr
      end
      return (1 - a) * c2
    else
      local a = (1 - c1) / (1 - c2)
      if a >= 1e-30 then 
        a = a^(2-pwr)
      end
      return 1 - a * (1 - c2)
    end
  end)
end

local function actions2rgb(fallback)
  local dispatch = ct_dispatch(fallback)
  return function(px, py, actions, acnt, paper)
    local cdx = acnt
    for cdx = acnt, 1, -1 do
      local action = actions[cdx]
      local kind = action.kind
      if kind == 'executable' then
        action = action.value
        kind = action.kind
      end
      assert(kind == 'array')
      action = action.value
      local count = #action
      local ct = action[count].value
      if ct == 'drop' then
        assert(count == 8)
        local cx, cy = action[1], action[2]
        local rad2 = action[3]
        local bgc, rgb = action[4].value, action[5].value
        local sr2, gc = action[6], action[7]

        local a2 = (px - cx)^2 + (py - cy)^2
        local disc = a2 < 1e-10 and 0 or 1 - rad2 / a2
        if disc <= 0 then
          if gc ~= 0 then
            rgb = paper_shading(rgb, exp(a2 * sr2) * gc, paper)
          end
          if disc > -0.001 then
            local a = sharpen((-disc)^.5)
            rgb = Vmap2(rgb, bgc, function(v1, v2) return v1 * a + v2 * (1-a) end)
          end
          return rgb
        else
          local a = disc^.5
          px, py = (px - cx) * a + cx, (py - cy) * a + cy
        end
      else
        px, py = dispatch(ct, px, py, action, count - 1)
      end
    end
  end
end

-- At this point, fx and fy contain the raster coordinates;
-- [ r g b ] is on top of the stack.
local function do_shadings(rgb, fx, fy, shadings, paper)
  local scnt = #shadings
  if scnt > 0 then
    for idx = 1, scnt do
      local shading = shadings[idx]
      local kind = shading.kind
      if kind == 'executable' then
        shading = shading.value
        kind = shading.kind
      end
      assert(kind == 'array')
      shading = shading.value
      local count = #shading
      local ct = shading[count].value
      if ct == 'jiggle-shade' then
        assert(count == 9)
        local dx, dy = shading[1], shading[2]
        local freq, ofst = shading[3], shading[4]
        local major, mf = shading[5], shading[6]
        local mdls, pw = shading[7], shading[8]

	local a = symmod(fy * dx + fx * dy + ofst, mdls)
        local af = a * freq

        if mf ~= 0 then
          local g = g1(mdls, a, mf, af, major, pw, freq)
	  rgb = paper_shading(rgb, max(cos(rad(g * freq)) * mf + 1, 0), paper)
        end
      elseif ct == 'wriggle-shade' then
        assert(count == 9)
        local cx, cy = shading[1], shading[2]
        local freq, ofst = shading[3], shading[4]
        local major, mf = shading[5], shading[6]
        local mdls, pw = shading[7], shading[8]

        local a = symmod(((fx - cx)^2 + (fy - cy)^2)^.5 + ofst, mdls)
        local af = a * freq

        if mf ~= 0 then
          local g = g1(mdls, a, mf, af, major, pw, freq)
	  rgb = paper_shading(rgb, max(cos(rad(g * freq)) * mf + 1, 0), paper)
        end
      else
	print(string.format('unrecognized shade token %s', ct))
      end
    end
  end

  return rgb
end

local function do_raster(dispatch)
  local actions2rgb = actions2rgb(dispatch)
  return function(lox, hix, loy, hiy, oversample, actions, acnt, shadings, paper, scl, background)
    local sampleover = 1 / oversample
    local width = (hix - lox) // sampleover
    local height = (hiy - loy) // sampleover
    local raster = newtable(width * height, 0)

    local factor = sampleover / scl

    for y = 0, height do
      local fy = loy / scl + y * factor
      for x = 0, width do
        local fx = lox / scl + x * factor

	local rgb = actions2rgb(fx, fy, actions, acnt, paper) or background
        rgb = do_shadings(rgb, fx, fy, shadings, paper)
	raster[1 + y*(width+1) + x] = {kind = 'array', value = rgb}
      end
    end
    return raster, width + 1, height + 1
  end
end

return {
  spread = function()
    local rad2 = pop_num() -- rad^2
    local cy = pop_num()
    local cx = pop_num()
    local py = pop_num()
    local px = pop_num()
    px, py = spread(px, py, cx, cy, rad2)
    push(px)
    push(py)
  end,
  ['.actions2rgb'] = function() -- px py actions acnt paper fallback .composite-map exec
    local _, fallback = pop_proc()
    local actions2rgb = actions2rgb(fallback)
    push(function()
      local paper = pop_array().value
      local acnt = pop_num()
      local actions = pop_array().value

      local py = pop_num()
      local px = pop_num()

      local rgb = actions2rgb(px, py, actions, acnt, paper)
      if rgb then
        push{kind = 'array', value = rgb}
        push(true)
      else
        push(false)
      end
    end)
  end,
  ['.paper-shading'] = function() -- rgb pwr paper
    local paper = pop_array().value
    local pwr = pop_num()
    local rgb = pop_array().value
    rgb = paper_shading(rgb, pwr, paper)
    push{kind = 'array', value = rgb}
  end,
  ['.composite-map'] = function() -- acnt idx actions fallback .composite-map exec
    local _, fallback = pop_proc()
    local dispatch = ct_dispatch(fallback)
    push(function()
      local actions = pop_array().value
      local idx = pop_num()
      local acnt = pop_num()

      local py = pop_num()
      local px = pop_num()

      for i = idx + 1, acnt - 1 do
        local action = actions[i+1]
        local kind = action.kind
        if kind == 'executable' then
          action = action.value
          kind = action.kind
        end
        assert(kind == 'array')
        action = action.value
        local count = #action
        local ct = action[count].value
        if ct == 'drop' then
          assert(count == 8)
          px, py = spread(px, py, action[1], action[2], action[3])
        else
          px, py = dispatch(ct, px, py, action, count - 1)
        end
      end
      push(px)
      push(py)
    end)
  end,
  ['stir-deformation'] = function()
    local Linv = pop_num()
    local th = pop_num()
    local rs = pop_array().value
    local cy = pop_num()
    local cx = pop_num()
    local py = pop_num()
    local px = pop_num()

    exec'oversample'
    local oversample = pop_num()
    px, py = stir_deformation(px, py, cx, cy, rs, th, Linv, oversample)
    push(px)
    push(py)
  end,
  ['rake-deformation'] = function()
    local Linv = pop_num()
    local tU = pop_num()
    local V = pop_num()
    local rs = pop_array().value
    local dy = pop_num()
    local dx = pop_num()
    local py = pop_num()
    local px = pop_num()
    px, py = rake_deformation(px, py, dx, dy, rs, V, tU, Linv)
    push(px)
    push(py)
  end,
  ['jiggle-deformation'] = function()
    local pw = pop_num()
    local mdls = pop_num()
    local mf = pop_num()
    local minor = pop_num()
    local major = pop_num()
    local trv = pop_array().value
    local ofst = pop_num()
    local freq = pop_num()
    local dy = pop_num()
    local dx = pop_num()
    local py = pop_num()
    local px = pop_num()
    px, py = jiggle_deformation(px, py, dx, dy, freq, ofst, trv, major, minor, mf, mdls, pw)
    push(px)
    push(py)
  end,
  ['wriggle-deformation'] = function()
    local pw = pop_num()
    local mdls = pop_num()
    local mf = pop_num()
    local minor = pop_num()
    local major = pop_num()
    local freq = pop_num()
    local cy = pop_num()
    local cx = pop_num()
    local py = pop_num()
    local px = pop_num()
    px, py = wriggle_deformation(px, py, cx, cy, freq, major, minor, mf, mdls, pw)
    push(px)
    push(py)
  end,
  ['stylus-deformation'] = function()
    local step_y = pop_num()
    local step_x = pop_num()
    local ny = pop_num()
    local nx = pop_num()
    local steps = pop_num()
    local tU = pop_num()
    local L = pop_num()
    local ey = pop_num()
    local ex = pop_num()
    local by = pop_num()
    local bx = pop_num()
    local py = pop_num()
    local px = pop_num()
    px, py = stylus_deformation(px, py, bx, by, ex, ey, L, tU, steps, nx, ny, step_x, step_y)
    push(px)
    push(py)
  end,
  ['.vortex-deformation'] = function()
    local nuterm = pop_num()
    local t = pop_num()
    local circ = pop_num()
    local cy = pop_num()
    local cx = pop_num()
    local py = pop_num()
    local px = pop_num()

    px, py = vortex_deformation(px, py, cx, cy, circ, t, nuterm)
    push(px)
    push(py)
  end,
  ['offset-deformation'] = function()
    local dy = pop_num()
    local dx = pop_num()
    local py = pop_num()
    local px = pop_num()

    px, py = offset_deformation(px, py, dx, dy)
    push(px)
    push(py)
  end,
  ['do-turn'] = function()
    local trv = pop_array().value
    local cy = pop_num()
    local cx = pop_num()
    local py = pop_num()
    local px = pop_num()

    px, py = do_turn(px, py, cx, cy, trv)
    push(px)
    push(py)
  end,
  ['.Minsky-circle'] = function()
    local eps = pop_num()
    local y = pop_num()
    local x = pop_num()
    x = x - eps * y
    y = y + eps * x
    push(x)
    push(y)
  end,
  ['.do-shadings'] = function()
    local paper = pop_array().value
    local shadings = pop_array().value
    local fy = pop_num()
    local fx = pop_num()
    local rgb = pop_array().value
    rgb = do_shadings(rgb, fx, fy, shadings, paper)
    push{kind = 'array', value = rgb}
  end,
  ['.do-raster'] = function()
    local _, fallback = pop_proc()
    local do_raster = do_raster(fallback)
    push(function()
      local background = pop_array().value
      local scl = pop_num()
      local paper = pop_array().value
      local shadings = pop_array().value
      local acnt = pop_num()
      local actions = pop_array().value
      local oversample = pop_num()
      local hiy = pop_num()
      local loy = pop_num()
      local hix = pop_num()
      local lox = pop_num()

      local raster, width, height = do_raster(lox, hix, loy, hiy, oversample, actions, acnt, shadings, paper, scl, background)
      push{kind = 'array', value = raster}
      push(width)
      push(height)
    end)
  end,
}, 0
