-- pac freqgraph.lua
--[[
an interactive histogram-ish frequency graph 
used in "spectral filter" and "pitch distort"
usage : 
	local Freqgraph = require "include/gui-extras/freqgraph"
	local fg = Freqgraph {
		-- required paramters :
			-- an array (Lua or C) containing the values to be read and modified by the interactive graph
			data = ;
			-- number of elements in the array
			dataSize = ;
		-- optional paramters :
			-- the position and size of the graph
			bounds = juce.Rectangle_int{15,0,480,330};
			title = "";
			yAxis = {
				name = "amplitude (%)";
				values = {
					[0] = "0";
					[0.5] = "50";
					[1] = "100";
				}
			}
		-- colours : pageBack, pageFore, graphBack, graphFore
	}
	-- public methods :
		-- paint the graph onto a pGraphics ctype
		fg:paint(g)
		-- change the graph's position
		fg:setPos(x, y)

--]]

local J = require "include/protojuce"

-- class
local M = {
	-- default values
	bounds = J.Rectangle_int{15,0,480,330};
	pageBack = J.Colour.black;
	pageFore = J.Colour.green;
	graphBack = J.Colour{r=0, g=0, b=160};
	graphFore = J.Colour.green;
	yAxis = {
		name = "amplitude (%)";
		values = {
			[0] = "0";
			[0.25] = "50";
			[0.5] = "100";
			[0.75] = "150";
			[1] = "200";
		}
	}
}
M.__index = M

setmetatable(M, {
	-- constructor
	__call = function (_, arg)
		local self = setmetatable(arg, M)
		self.frame = J.Rectangle_int(4, 30, self.bounds.w-50, self.bounds.h-65)
		self.outFrame = J.Rectangle_int(2, 28, self.frame.w+4, self.frame.h+4)
		self.yAmp = 1/self.frame.h
		-- display-coord quarters in data coordinates
		self.q1 = self:x2bar(self.frame.w/4)
		self.q2 = self:x2bar(self.frame.w/2)
		self.bgFill = J.FillType(self.pageBack)
		self.bgFill2 = J.FillType(self.graphBack)
		self.fgFill = J.FillType(self.graphFore)
		self:InitBackBuffer()
		self.dirtyLeft, self.dirtyRight = 0, self.dataSize-1
		gui.addHandler("mouseDrag", function (event)
			self:mouseDrag(event)
		end)
		gui.addHandler("mouseUp", function (event)
			self:mouseUp(event)
		end)
		plugin.addHandler("prepareToPlay", function ()
			self:InitBackBuffer()
		end)
		return self
	end;
})

-- Coordinate system conversion between displayed bars <-> data
-- cheap log-like scale made of 3 linear functions
function M:x2bar(x)
	if x<self.frame.w/4 then
		return math.floor(x*(self.dataSize/(4*self.frame.w)))
	elseif x<self.frame.w/2 then
		return math.floor(x*((3*self.dataSize)/(4*self.frame.w))-(self.dataSize/8))
	else
		return math.floor(x*((3*self.dataSize)/(2*self.frame.w))-(self.dataSize/2))
	end
end

function M:bar2x(s)
	if s<self.q1 then
		return math.floor((4*self.frame.w*s)/self.dataSize)
	elseif s<self.q2 then
		return math.floor((self.frame.w*(self.dataSize+8*s))/(6*self.dataSize))
	else
		return math.floor((self.frame.w*(self.dataSize+2*s))/(3*self.dataSize))
	end
end

function M:y2amp(y) return 1 - y*self.yAmp end
function M:amp2y(a) return math.floor(self.frame.h - a*self.frame.h) end

-- on mouse drag, interpolate the movement, update the data, and mark the dirty graph area
function M:mouseDrag(event)
	if not self.bounds:contains(event.mouseDownPos) then
		return
	end
	if self.bar ~= nil then
		self.oldbar = self.bar
		self.oldamp = self.amp
	end
	self.bar = self:x2bar(event.x-self.frame.x-self.bounds.x)
	self.amp   = self:y2amp(event.y-self.frame.y-self.bounds.y)
	
	-- limit to bounds
	if self.bar<0 then self.bar=0 end
	if self.bar>self.dataSize-1 then self.bar=self.dataSize-1 end
	if self.amp<0 then self.amp=0 end
	if self.amp>1 then self.amp=1 end
	
	-- interpolate and set
	local x1, x2, y1, y2 = self.oldbar,self.bar,self.oldamp,self.amp
	if x1 == nil or x1==x2 then
		x1,y1 = x2,y2
		self.data[x2] = y2
	else
		if x1>x2 then
			x1, x2, y1, y2 = x2, x1, y2, y1
		end
		for i=x1,x2 do
			local x = (x2-i)/(x2-x1)
			self.data[i] = y1*x + y2*(1-x)
		end
	end
	-- create or enlarge the currently dirty area (ie. needing a repaint)
	if self.dirtyLeft then
		if x1<self.dirtyLeft then self.dirtyLeft = x1 end
		if x2>self.dirtyRight then self.dirtyRight = x2 end
	else
		self.dirtyLeft, self.dirtyRight = x1, x2
	end
	event.originalComponent:repaint()
end

function M:mouseUp(event)
	self.bar = nil
	self.oldbar = nil
end

-- create the backbuffer and draw all permanent parts of the graph
function M:InitBackBuffer()
	self.backBuffer = J.Image(
		J.Image.PixelFormat.RGB, 
		self.bounds.w, 
		self.bounds.h, true)
	local g = J.Graphics(self.backBuffer)
	g:fillAll(self.pageBack)
	g:setColour(self.graphFore)
	g:drawRect(self.outFrame, 1)
	g:setColour(self.pageFore)
	if self.title then
		g:setFont(17)
		g:drawText("== "..self.title.." ==", self.frame.x, 5, self.frame.w, 20, J.Justification.centred)
	end
	
	-- draw the Y axis labels
	g:saveState()
	g:addTransform(J.AffineTransform(0, -1, self.bounds.w, 1, 0, self.frame.y))
	g:setFont(16)
	g:drawText(self.yAxis.name, 0, 0, self.frame.h, 20, J.Justification.centred)
	g:restoreState()
	g:setFont(14)
	for pos, label in pairs(self.yAxis.values) do
		local y = self.frame.y+((self.frame.h-10)*(1-pos))-6
		g:drawText(tostring(label), self.frame:getR()+3, y, 33, 20)
	end
	
	-- draw the X axis labels
	g:setFont(16)
	g:drawText("frequency (kHz)", self.frame.x, self.bounds.h-20, self.frame.w, 20, J.Justification.centred)
	g:setFillType(self.fgFill)
	local sr = 44100
	if plugin.isSampleRateKnown() then
		sr = plugin.getSampleRate()
	else
		-- if the samplerate is unknown, call this again when it becomes known
		plugin.addHandler('prepareToPlay', function() self:InitBackBuffer() end)
	end
	local f,fmax = 0, sr/2
	local function f2x(f)
		return self:bar2x(f/fmax*self.dataSize)+self.frame.x
	end
	g:setFont(14)
	while f<fmax do
		if f%10000==0 then
			g:fillRect(f2x(f), self.frame:getB(), 3, 8)
		elseif f%1000==0 then
			g:fillRect(f2x(f), self.frame:getB(), 2, 4)
		end
		f = f + 100
	end
	g:drawText("0", f2x(0)-2, self.frame:getB()+3, 20, 20)
	g:drawText("2", f2x(2000)-2, self.frame:getB()+3, 20, 20)
	g:drawText("10", f2x(10000)-2, self.frame:getB()+3, 20, 20)
	g:drawText("20", f2x(20000)-2, self.frame:getB()+3, 20, 20)
	g = nil
	
	-- draw the entire graph's contents
	self:DrawBars(0, self.dataSize-1)
end

-- update the graph between left and right
function M:DrawBars(leftBar, rightBar)
	local g = J.Graphics(self.backBuffer)
	g:setFillType(self.bgFill2)
	local leftX, rightX = self:bar2x(leftBar), self:bar2x(rightBar+1)
	g:fillRect(leftX+self.frame.x, self.frame.y, rightX-leftX, self.frame.h)
	g:setFillType(self.fgFill)
	local lastX = -1
	for i = leftBar, rightBar do
		local x = self:bar2x(i)
		if x~=lastX then
			local width = math.max(1,self:bar2x(i+1)-x)
			local y = self:amp2y(self.data[i])
			local height = self.frame.h-y
			g:fillRect(J.Rectangle_int(x+self.frame.x, y+self.frame.y, width, height))
		end
		lastX = x
	end
	
end

-- on paint, update the backbuffer where necessary, and blit it
function M:paint(g)
	if self.dirtyLeft then
		local l, r = self.dirtyLeft, self.dirtyRight
		self.dirtyLeft, self.dirtyRight = nil, nil
		self:DrawBars(l, r)
	end
	g:drawImageAt(self.backBuffer, self.bounds.x, self.bounds.y)
end

function M:setPos(x,y)
	self.bounds.x, self.bounds.y = x, y
	local guiComp = gui.getComponent()
	if guiComp ~= nil then
		guiComp:repaint()
	end
end

return M
