-- lua script for L Systems scripts
-- some basic constants
local abort = false
local w_mid = width / 2
local h_mid = height / 2
local radius = math.sqrt((w_mid * w_mid) + (h_mid * h_mid)) / 2
local xMin, xMax, yMin, yMax = 0,0,0,0
local autoXOffset, autoYOffset  = 0,0
local red,green,blue = 0,0,0

--@TBCONFIG SLIDER: Scale, 25
--@TBCONFIG SLIDER: X Offset left -- right, 50
--@TBCONFIG SLIDER: Y Offset up -- down, 50
--@TBCONFIG FLAG: Paint, false
--@TBCONFIG FLAG: AutoScale, true
--@TBCONFIG STYLE: L System, 1, Roger Penrose's rhombuses,PentaPlexity,kock 1,koch 2,koch 3,koch 6,Dragon,Peano,FlowSnake,Quartet,SnowFlake 3,Pentagram,Dekking1Improved,Alfonseca ,Color Penrose,circle,Double cesaro,Hanmer Tiles
--@TBCONFIG STYLE: Iterations, 1, 0,1,2,3,4,5,6,7,8,9,10,11,12,13,15,16,17,18

local uIterations =  styleVal2 - 1
local uScale = sliderVal1 
local uXOffset = (sliderVal2 - 0.5) * width * 3
local uYOffset = (sliderVal3 - 0.5) * height * 3
local uPaint = flagVal1
local uAutoScale = flagVal2

function ToRGB1(rm1, rm2, Hue)
	if (Hue > 360) then  Hue = Hue - 360 end
	if (Hue < 0) then Hue = Hue + 360 end
	if (Hue < 60) then rm1 = rm1 + ((rm2 - rm1) * Hue ) / 60 
	elseif (Hue < 180) then rm1 = rm2
	elseif (Hue < 240) then rm1 = rm1 + ((rm2 - rm1) * (240 - Hue) ) / 60
	end
	return rm1 
end

function ToRGB(hue,luminance,saturation)
local red,green,blue, rm1, rm2
	if (saturation == 0.0) then --  Grauton, einfacher Fall
		red = luminance * 255; green = red; blue = red
	else
		if (luminance <= 0.5) then 
			rm2 = luminance + luminance * saturation
		else
			rm2 = luminance + saturation - luminance * saturation
		end
		rm1 = 2 * luminance - rm2
		red = ToRGB1(rm1, rm2, hue + 120)
		green = ToRGB1(rm1, rm2, hue)
		blue = ToRGB1(rm1, rm2, hue - 120)
	end
	return red, green , blue 
end
-- ===========================================================
function plot(pointx,pointy)
     if (pointx > 0) and (pointy > 0) and (pointx < width) and (pointy < height) then  
        set_rgb(pointx, pointy, red,green,blue )
    end 
end
function round(x)
    local xf = math.floor(x)
    if (x - xf) <= 0.5 then 
        return xf
    else
        return math.ceil(x)
    end 
end
function Liang_Barsky_clipTest(p, q, u1, u2)  -- Line Clipping Algorithm -- Liang-Barsky
local r
   if (p < 0) then 
        r = q / p
        if (r > u2) then 
            return 0, u1, u2
        elseif (r > u1) then 
            return 1, r, u2
        end
   elseif (p > 0) then 
      r = q / p
      if (r < u1) then 
         return 0, u1, u2
      elseif (r < u2) then 
            return 1, u1, r
      end
   elseif (q < 0) then 
      return 0, u1, u2
   end
   return 1, u1, u2
end
function Liang_Barsky_clipping(x0, y0, x1, y1)
local XS_MIN = 0
local XS_MAX = width
local YS_MIN = 0
local YS_MAX = height
local dx = x1 - x0
local dy = y1 - y0
local u1 = 0
local u2 = 1
local t1, t2, t3, t4 

t1, u1, u2 = Liang_Barsky_clipTest(-dx, x0-XS_MIN, u1, u2)
t2, u1, u2 = Liang_Barsky_clipTest( dx, XS_MAX-x0, u1, u2)
t3, u1, u2 = Liang_Barsky_clipTest(-dy, y0-YS_MIN, u1, u2)
t4, u1, u2 = Liang_Barsky_clipTest( dy, YS_MAX-y0, u1, u2)
    
   if (t1 == 1) and (t2 == 1) and (t3 == 1) and (t4 == 1)  then 
        if (u2 < 1) then 
            x1 = x0 + round(u2 * dx)
            y1 = y0 + round(u2 * dy)
        end
        if (u1 > 0) then 
            x0 = x0 + round(u1 * dx)
            y0 = y0 + round(u1 * dy)
        end
    end
    return x0, y0, x1, y1
end
function line_bres(x0, y0, x1, y1)  -- Line Generating Algorithm using Bresenham's technique
   x0, y0, x1, y1 = Liang_Barsky_clipping(x0, y0, x1, y1)
    local steep, t
    if (math.abs(y1 - y0) > math.abs(x1 - x0)) then 
        steep = 1 
    else 
        steep = 0 
    end 

    if steep == 1 then
        t = x0; x0 = y0; y0 = t
        t = x1; x1 = y1; y1 = t
    end
    if x0 > x1 then
        t = x0; x0 = x1; x1 = t
        t = y0; y0 = y1; y1 = t
    end 

    local deltax = x1 - x0
    local deltay = math.abs(y1 - y0)
    local error = round(deltax / 2)
    local ystep
    local y = y0
    if y0 < y1 then ystep = 1 else ystep = -1 end
    
    for x = x0, x1 do
         if (steep == 1) then plot(y,x) else plot(x,y) end 
         error = error - deltay
         if error < 0 then
             y = y + ystep
             error = error + deltax
         end
    end
end
function RotateLineDegrees(x0, y0, length, angleinDegrees, draw)
local xx = autoXOffset + uXOffset
local yy = autoYOffset + uYOffset
	if length < 0.5 then
		return x0,y0
	else
		local angle = angleinDegrees / 180 * math.pi
		local x1 = length * math.cos(angle) + x0
		local y1 = length * math.sin(angle) + y0
		if draw then 
			if uPaint == true then  
				paint_line(xx + x0, yy + y0, xx + x1, yy + y1)
			else
				line_bres(xx + x0, yy + y0, xx + x1, yy + y1)
			end
		end
		return x1, y1
	end
end
-- ==========================================================
-- end of line drawing code 
-- ===========================================================

-- ============================================================
-- L System formulae
-- ============================================================
local angle =  10 -- dont alter this section -- alter the next section
local CornerDegrees = 90 
local axiom = ''
local definition = {}
local j
for j = 1,10 do definition[j] = "" end
--  ===================== definitions start here
function LSystemUpdateFormula(StartAxiom)
local axiomLength = string.len(StartAxiom)
local at = {}
local i, newAxiom, s, j, c, replaced
	
	for i = 1, axiomLength do
		if (is_escape() > 0) then
			abort = true; break
		end
		s = string.sub(StartAxiom,i,i)  -- get one character
		-- replace it with its definition if it exists
		replaced = false
		for j = 1,10 do
			if string.len(definition[j]) > 0 then 
				c = string.sub(definition[j],1,1)
				if s == c then -- replacement needed
					table.insert(at, string.sub(definition[j],3,-1))
					replaced = true
					break
				end
			end
		end
		if replaced == false then table.insert(at,  s) end
	end
	newAxiom = table.concat(at) 
	return newAxiom
end

function GetLsystem()
	local LSystemType = styleVal1
	if LSystemType == 0 then 
		-- template - copy what you need from here and add the angle, axiom and definitions as required
		-- remember to add the title into the combo list (max 20 items though)
		angle = 6
		axiom = ''
		definition[1] = ''
		definition[2] = ''
		definition[3] = ''
		definition[4] = ''
		definition[5] = '' 
		definition[6] = '' 
	elseif LSystemType == 1 then	
		-- Roger Penrose's rhombuses
		angle = 10
		axiom = '[Y]++[Y]++[Y]++[Y]++[Y]'
		definition[1] = 'W=YF++ZF----XF[-YF----WF]++'
		definition[2] = 'X=+YF--ZF[---WF--XF]+'
		definition[3] = 'Y=-WF++XF[+++YF++ZF]-'
		definition[4] = 'Z=--YF++++WF[+ZF++++XF]--XF'
		definition[5] = 'F=' 
	elseif LSystemType == 2 then 
		-- PentaPlexity
		angle = 10
		axiom = 'F++F++F++F++F'
		definition[1] = 'F=F++F++F|F-F++F'
	elseif LSystemType == 3 then 
		-- kock1
		angle = 6
		axiom = 'F--F--F'
		definition[1] = 'F=F+F--F+F'
	elseif LSystemType == 4 then 
		-- kock2
		angle = 12
		axiom = 'F---F---F---F'
		definition[1] = 'F=-F+++F---F+'
	elseif LSystemType == 5 then 
		-- kock4
		angle = 4
		axiom = 'F-F-F-F'
		definition[1] = 'F=F-F+F+FF-F-F+F'
	elseif LSystemType == 6 then 
		-- kock6
		angle = 4
		axiom = 'f+f+f+f'
		definition[1] = 'f=f-ff+ff+f+f-f-ff+f+f-f-ff-ff+f'
	elseif LSystemType == 7 then 
		-- dragon
		angle = 8
		axiom = 'fx'
		definition[1] = 'f='
		definition[2] = 'y=+FX--FY+'
		definition[3] = 'x=-FX++FY-'
	elseif LSystemType == 8 then 
		-- peano
		angle = 4
		axiom = 'F-F-F-F'
		definition[1] = 'F=F-F+F+F+F-F-F-F+F'
	elseif LSystemType == 9 then 
		-- FlowSnake
		angle = 6
		axiom = 'FL'
		definition[1] = 'L=FL-FR--FR+FL++FLFL+FR-'
		definition[2] = 'R=+FL-FRFR--FR-FL++FL+FR'
		definition[3] = 'F='
	elseif LSystemType == 10 then 
		-- Quartet
		angle = 4
		axiom = 'fb'
		definition[1] = 'A=FBFA+HFA+FB-FA'
		definition[2] = 'B=FB+FA-FB-JFBFA'
		definition[3] = 'F='
		definition[4] = 'H=-'
		definition[5] = 'J=+'
	elseif LSystemType == 11 then 
		-- SnowFlake 3
		angle = 12
		axiom = 'fx'
		definition[1] = 'x=++f!x!fy--fx--fy|+@q0.33333fyf!x!++f!y!++f!y!fx@q3+++f!y!fx'
		definition[2] = 'y=fyf!x!+++@q0.33333fyf!x!++f!x!++f!y!fx@q3|+fx--fy--fxf!y!++'
		definition[3] = 'F='
	elseif LSystemType == 12 then 
		-- SnowFlake 3
		angle = 10
		axiom = 'fx++fx++fx++fx++fx'
		definition[1] = 'x=[++++@i1.618033989f@0.618033989f!x!@i1.618033989f]'
	elseif LSystemType == 13 then 
		-- Dekking1Improved
		angle = 3
		axiom = 'g+g+g'
		definition[1] = 'f=g+f-g'
		definition[2] = 'g=f+g-f'
	elseif LSystemType == 14 then 
		-- Alfonseca 
		angle = 16
		axiom = 'a'
		definition[1] = 'A=f-[A+A]+f[+fA]-A'
		definition[2] = 'f=ff'
	elseif LSystemType == 15 then 	
		-- Color Penrose
		angle = 10
		axiom = '+WC00F--XC180F---YC180F--ZC00F'
		definition[1] = 'W=YC180F++ZC00F----XC180F[-YC180F----WC00F]++'
		definition[2] = 'X=+YC180F--ZC00F[---WC00F--XC180F]+'
		definition[3] = 'Y=-WC00F++XC180F[+++YC180F++ZC00F]-'
		definition[4] = 'Z=--YC180F++++WC00F[+ZC00F++++XC180F]--XC180F'
		definition[5] = 'F=' 
		definition[6] = '' 
	elseif LSystemType == 16 then 
		-- circle
		angle = 24
		axiom = 'X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X+X'
		definition[1] = 'x=[F+F+F+F[---X-Y]+++++F++++++++F-F-F-F]'
		definition[2] = 'y=[F+F+F+F[---Y]+++++F++++++++F-F-F-F]'
	elseif LSystemType == 17 then 
		-- Double cesaro
		angle = 4
		axiom = 'D\90D\90D\90D\90'
		definition[1] = 'D=\42!D!/84!D!\42'
	elseif LSystemType == 18 then 
		-- Hanmer Tiles
		angle = 4
		axiom = '+c53f+g+c12f'
		definition[1] = 'f=GGGGG|FFFFF|GGGGG+G+ffggf|fggff+G+fgfgg|ggfgf+G+ffgfg|gfgff+G+gffgg|ggffg+G+GGGGG+GGGGG+GGGGG'
		definition[2] = 'g=ggggg'
	end
	axiom = string.lower(axiom)
	for j = 1,10 do definition[j] = string.lower(definition[j] ) end
	CornerDegrees = 360 / angle
	local depth 
	for depth = 0, uIterations do
		axiom = LSystemUpdateFormula(axiom)
	end
	
end


-- ============================================================
-- end of L System formulae
-- ============================================================
local StackLimit = 1000
local StackPos = 1 
local stX = {}
local stY = {}
local stA = {}
local stL = {}
local stH = {}
local stA2 = {}
local stD = {}
function fillStackPush(X, Y, angle,length,thisHue, angle2, direction)
	if StackPos < StackLimit  then
		stX[StackPos] = X
		stY[StackPos] = Y
		stA[StackPos] = angle
		stL[StackPos] = length
		stH[StackPos] = thisHue
		stA2[StackPos] = angle2
		stD[StackPos] = direction
		StackPos = StackPos + 1 
	end
end
function fillStackPop()
local X, Y, angle,length,thisHue, angle2, direction
	StackPos = StackPos - 1
	if StackPos > 0 then 
		X = stX[StackPos]
		Y = stY[StackPos]
		angle = stA[StackPos] 
		length = stL[StackPos]
		thisHue = stH[StackPos]  
		angle2 = stA2[StackPos]  
		direction = stD[StackPos]  
		return X, Y, angle,length,thisHue, angle2, direction
	else
		return 0, 0, 0, 1, 0, 0, 0 ,0
	end
	
end

function ExtractNumber(str,index)
	local a = string.sub(str,index,-1)
	local nstart,nend = string.find(a, "[0-9.]+")
	local n = tonumber(string.sub(a,nstart,nend))
	return n,nend
end
function alterAngle(OrigAngle, DeltaAngle)
	OrigAngle = OrigAngle + DeltaAngle
	while OrigAngle > 360 do OrigAngle = OrigAngle - 360 end 
	while OrigAngle < 0 do OrigAngle = OrigAngle + 360 end 
	return OrigAngle
end
-- =======================================================================
-- main logic
-- =======================================================================
function lSystem(Axiom, length, draw)
	local direction = true
	local x,y = 0,0
	local thisHue, thisAngle, Angle2 = 240, 0, 0 
	local depth, axiomLength, i
	red,green,blue = ToRGB(thisHue,0.5,0.5)
	StackPos = 1 
	xMin, xMax, yMin, yMax = 0,0,0,0

	axiomLength = string.len(Axiom)
	-- output_debug_string("Axiom  length = "..axiomLength)
	for i = 1, axiomLength do
		if (is_escape() > 0) then
			abort = true; break
		end
		local cmd = string.sub(Axiom,i,i)  -- get one character
		-- output_debug_string("Axiom# ".. i .." = "..cmd)
		if cmd == 'f' or cmd == 'd' then -- draw foreward
				if cmd == 'f'  then 
					x,y = RotateLineDegrees(x, y, length, thisAngle, draw)
				else
					x,y = RotateLineDegrees(x, y, length, Angle2, draw)
				end
				xMin = math.min(x, xMin) ;  xMax = math.max(x, xMax) 
				yMin = math.min(y, yMin) ;  yMax = math.max(y, yMax) 
		elseif cmd == 'g' or cmd == 'm' then -- move forward
			if cmd == 'g' then 
				x,y = RotateLineDegrees(x, y, length, thisAngle, false)
			else
				x,y = RotateLineDegrees(x, y, length, Angle2, false)
			end
		elseif cmd == '+' then -- increment angle
			if direction == true then 	thisAngle = alterAngle(thisAngle , CornerDegrees)
			else thisAngle = alterAngle(thisAngle, - CornerDegrees)
			end
		elseif cmd == '-' then -- decrement angle
			if direction == true then thisAngle = alterAngle(thisAngle, - CornerDegrees)
			else thisAngle = alterAngle(thisAngle, CornerDegrees)
			end
		elseif cmd == '|' then --  reverse (180 shift)
				thisAngle = alterAngle(thisAngle , 180)
		elseif cmd == '!' then --  reverse direction commands
			if direction == true then direction = false else direction = true end
		elseif cmd == '[' then -- push state
			fillStackPush(x, y, thisAngle, length, thisHue, Angle2, direction)
		elseif cmd == ']' then -- pop state
			x, y, thisAngle, length, thisHue, Angle2, direction = fillStackPop()
		elseif string.find(cmd, "[\\/c<>@]") ~= nil then 
			local nxtChar = string.sub(Axiom,i+1,i+1)  -- test for @q or @i
			local numStart = i+1
			if nxtChar == "q" or nxtChar == "i" then numStart = i + 2 end
			local num, newpos  = ExtractNumber(Axiom,numStart)
			i = newpos   -- update the for loop index variable!! to skip over the number on the next time around
			if cmd == "\\" then -- increase angle by n degrees
				Angle2 = alterAngle(Angle2 , num)
			elseif cmd == "/" then -- decrease angle by n degrees
				Angle2 = alterAngle(Angle2, - num)
			elseif cmd == "c" then -- select color n
				thisHue = num
				red,green,blue = ToRGB(thisHue,0.5,0.5)
			elseif cmd == "<" then -- increment color by n
				thisHue = alterAngle(thisHue , num)
				red,green,blue = ToRGB(thisHue,0.5,0.5)
			elseif cmd == ">" then -- decrease color by n
				thisHue = alterAngle(thisHue, - num)
				red,green,blue = ToRGB(thisHue,0.5,0.5)
			elseif cmd == "@" then -- multiply line length by n
				if nxtChar == "q" then 	length = length * math.sqrt(num)
				elseif nxtChar == "i" then 	length = length / num
				else length = length * num
				end
			end
		end
	end
end

paint_open(1)
GetLsystem()
if uAutoScale then 
	local TrialLength = 100
	-- first we use a trial length to get the bounding box for that length
	-- we then map this bounding box back onto the real screen and adjust the scale factor to suit
	lSystem(axiom, TrialLength, false)  -- trial run to get size of bounding box		
	local xyRatio = math.min(width / (xMax - xMin), height / (yMax - yMin) ) -- calc the amount needed to scale to full size
	local Scalelength, xShift, yShift
	Scalelength = TrialLength * xyRatio * uScale * 4  -- 4 is because Scale defaults to 0.25
	xShift = (width - (xMax - xMin) * Scalelength / TrialLength) / 2 
	yShift = (height - (yMax - yMin) * Scalelength / TrialLength) / 2
	autoXOffset = - (xMin ) * Scalelength / TrialLength + xShift
	autoYOffset = - (yMin ) * Scalelength / TrialLength + yShift
	lSystem( axiom, Scalelength, true )
else
	autoXOffset = width / 2 
	autoYOffset = height / 2 
	lSystem(axiom, uScale * radius  / (uIterations + 1) , true )
end
paint_close()




