/*
Copyright (C) 2015 Runtime Revolution Ltd.

This file is part of LiveCode.

LiveCode is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License v3 as published by the Free
Software Foundation.

LiveCode is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with LiveCode.  If not see <http://www.gnu.org/licenses/>.  */

/*
This widget displays information as a configurable line graph.

Name: graphData

Type: property

Syntax:
set the graphData of <widget> to <pData>
get the graphData of <widget>

Summary: The data displayed by the graph.

Parameters:
pData(string): A line delimited list of the data for each point on the x-axis.

Example:
    local tVar
    put "mon,1000,300,0\ntue,720,340,123\nweb,889,400,80\nthu,800,360,10\nfri,1000,301,234\nsat,964,400,200\nsun,1000,500,0" into tVar
    replace "\n" with return in tVar
    set the graphData of widget "My Graph" to tVar

Description:
The <graphData> is the data to be displayed by the graph widget.
Each line of <pData> consists of a comma delimited list whose first element is the label for that point on the x-axis,
and whose subsequent items give the values corresponding to that label for each line to be displayed on the graph.

Name: graphColors

Type: property

Syntax:
set the graphColors of <widget> to <pColors>
get the graphColors of <widget>

Summary: The colors of the lines on the graph.

Parameters:
pColors(string): A line-delimited list of the colors for each line on the graph

Example:
local tVar
put "255,0,155,255\n155,100,255,255\n100,255,100,255" into tVar
replace "\n" with return in tVar
set the graphColors of widget "My Graph" to tVar

Description:
The <graphColors> are the colors of each line in the graph widget.

Name: graphXLines

Type: property

Summary: Controls whether the horizontal lines on the graph are shown.

Syntax:
set the graphXLines of <widget> to <pLines>
get the graphXLines of <widget>

Parameters:
pLines(boolean): True or false.

Description:
Controls whether the horizontal lines on the graph are shown.

Name: graphYLines

Type: property

Summary: Controls whether the vertical lines on the graph are shown.

Syntax:
set the graphYLines of <widget> to <pLines>
get the graphYLines of <widget>

Parameters:
pLines(boolean): True or false.

Description:
Controls whether the vertical lines on the graph are shown.

*/

widget com.livecode.widget.lineGraph
--

-- dependency declarations
use com.livecode.canvas
use com.livecode.widget
use com.livecode.engine
--

-- adding metadata to ensure the extension displays correctly in LiveCode
metadata author is "LiveCode"
metadata version is "1.0.1"
metadata title is "Line Graph"
metadata preferredSize is "300,175"
metadata svgicon is "M50.6,27.2h-2.2v-3.3l0.8-0.5l-0.2-0.3h2.9v-1h-3.5v-5h3.5v-1h-3.5v-5h3.5v-1h-3.5v-5h3.5v-1h-3.5V1.8h-1v2.4h-10V1.8h-1v2.4h-10V1.8h-1v2.4h-10V1.8h-1v2.4H5V2.5C5,1.1,3.9,0,2.5,0S0,1.1,0,2.5v29.7h50.6c1.4,0,2.5-1.1,2.5-2.5C53.1,28.4,52,27.2,50.6,27.2z M47.4,27.2h-10v-4.1h8l1.2,1.9l0.8-0.5V27.2z M15.4,15.2l1.4,0.9h-1.4V15.2z M14.4,17.1v5h-4.2l3.6-5H14.4z M15.4,17.1h2.9l7.1,4.6v0.4h-10V17.1z M26.4,17.1h0.1l-0.1,0.2V17.1z M25.4,18.1l-1.6-1h1.6V18.1z M26.4,22l4-4.9h6v5h-10V22z M37.4,17.1h4.2l3.2,5h-7.4V17.1z M37.4,16.1v-5h0.4l3.2,5H37.4z M36.4,10.1h-0.1l0.1-0.2V10.1z M36.4,11.1v5h-5.1l4.2-5H36.4z M27.4,16.1h-1v-5h5.1L27.4,16.1z M25.4,16.1h-3.1l-6.9-4.5v-0.5h10V16.1z M14.4,11.3l-3.5,4.9H5v-5h9.4V11.3z M10.1,17.1l-3.6,5H5v-5H10.1z M5.7,23.1l-0.7,1v-1H5.7z M9.5,23.1h4.9v4.1H6.5L9.5,23.1z M15.4,23.1h10v4.1h-10V23.1z M26.4,23.1h10v4.1h-10V23.1z M47.4,20.7l-2.3-3.6h2.3V20.7z M47.4,16.1h-2.9l-3.2-5h6.1V16.1z M47.4,10.1h-6.7l-3.2-5h9.9V10.1z M36.4,5.1v0.1l-4,4.9h-6v-5H36.4z M25.4,5.1v5h-10v-5H25.4z M14.4,5.1v5H5v-5H14.4z"
--

-- property declarations
property graphData get getData set setData
metadata graphData.editor is "com.livecode.pi.text"
metadata graphData.default is "mon,1000,300,0\ntue,720,340,123\nweb,889,400,80\nthu,800,360,10\nfri,1000,301,234\nsat,964,400,200\nsun,1000,500,0"
metadata graphData.label is "Data (CSV)"

property graphColors get getColors set setColors
metadata graphColors.editor is "com.livecode.pi.colorList"
metadata graphColors.default is "255,0,155,255\n155,100,255,255\n100,255,100,255"
metadata graphColors.label is "Line Colors"

property graphXLines get mGridHShow set setXLinesVisibility
metadata graphXLines.default is "true"
metadata graphXLines.label is "Horizontal Grid Lines"

property graphYLines get mGridVShow set setYLinesVisibility
metadata graphYLines.default is "true"
metadata graphYLines.label is "Vertical Grid Lines"
--

-- private instance variables
private variable mData as List

private variable mGridHShow as Boolean
private variable mGridVShow as Boolean
private variable mLegendShow as Boolean
private variable mLabelXShow as Boolean
private variable mLabelYShow as Boolean
private variable mVerticesShow as Boolean

private variable mMax as Number
private variable mMin as Number
private variable mVPointCount as Integer
private variable mHPointCount as Integer
private variable mGridHWidth as Real
private variable mGridVWidth as Real
private variable mLegendWidth as Real
private variable mLabelXWidth as Real
private variable mLabelYHeight as Real
private variable mGridRect as Rectangle
private variable mColors as List
private variable mPadding as Real

// Stores the numerical increment of the steps in the Y axis
private variable mYIncrement as Real

// Paths
private variable mBackgroundGridPath as Path
private variable mBackgroundAxisPath as Path
--

----------
-- this handler is called when the widget is created
public handler OnCreate() returns nothing
	put [["mon","tue","web","thu","fri","sat","sun"],[1000,720,889,800,1000,964,1000],[300,340,400,360,301,400,nothing],[0,123,80,10,234,200,0]] into mData
	put [stringToColor("255,0,155"),stringToColor("155,100,255"),stringToColor("100,255,100")] into mColors
	put true into mGridHShow
	put true into mGridVShow
	put true into mLegendShow
	put true into mLabelXShow
	put true into mLabelYShow
	put true into mVerticesShow
	initialiseConstants()
end handler
----------

----------
-- this handler is called whenever LiveCode needs to redraw the widget
public handler OnPaint() returns nothing
	if mLabelYShow is true then
		variable tLeft as Real
		put drawYLabels() into tLeft
		updateGridRect(rectangle [tLeft+mPadding,the top of mGridRect,the right of mGridRect,the bottom of mGridRect])
	end if

	if mLabelXShow is true then
		variable tBottom as Real
		//put drawXLabels() into tBottom
	end if

	if mLegendShow is true then
		drawLegend()
	end if

	drawGraph()
end handler
----------

----------
-- this handler sets the graphData property
public handler setData(in pData as String) returns nothing
	//log "trying to set data"
	//log pData
	variable tData as List
	variable tNewData as List
	put the empty list into tNewData

	split pData by newline into tData

	variable tCount as Number
	variable tGraphLineData as String
	variable tSplitGraphLineData as List

	// Count the longer data points
	variable tMax as Integer
	put 0 into tMax
	repeat for each element tGraphLineData in tData
		split tGraphLineData by "," into tSplitGraphLineData
		if the number of elements in tSplitGraphLineData > tMax then
			put the number of elements in tSplitGraphLineData into tMax
		end if
	end repeat

	repeat with tCount from 1 up to tMax
		push the empty list onto tNewData
	end repeat

	repeat for each element tGraphLineData in tData
		split tGraphLineData by "," into tSplitGraphLineData
		repeat with tCount from 1 up to tMax
			if the number of elements in tSplitGraphLineData >= tCount then
				//log element tCount of tSplitGraphLineData
				if tCount is 1 then
					push element tCount of tSplitGraphLineData onto element 1 of tNewData
				else
					parse element tCount of tSplitGraphLineData as number

					if the result is not nothing then
						push the result onto element tCount of tNewData
					else
						push 0 onto element tCount of tNewData
					end if
				end if
			else
				//log "undefined"
				push nothing onto element tCount of tNewData
				//log "undefined pushed"
			end if
		end repeat
	end repeat

	put tNewData into mData
	initialiseConstants()
	//log "redrawing"
	redraw all
end handler
----------

----------
-- this handler sets the graphColors property
public handler setColors(in tColors as String) returns nothing
	if tColors is empty then
		put the empty list into mColors
	else
		variable tStringColorList as List
		variable tColorList as List
		put the empty list into tColorList
		split tColors by newline into tStringColorList

		variable tColor as String
		repeat for each element tColor in tStringColorList
			push stringToColor(tColor) onto back of tColorList
		end repeat
		put tColorList into mColors
	end if
	redraw all
end handler
----------

----------
-- the following handlers set whether the horizontal and vertical grid lines are shown
public handler setShowHGrid(in pState as Boolean) returns nothing
	//put pState into mShowHGrid
end handler

public handler setShowVGrid(in pState as Boolean) returns nothing
	//put pState into mShowVGrid
end handler
----------

----------
-- this handler sets whether the x gird lines are visible
public handler setXLinesVisibility(in pShowXGridLines as Boolean) returns nothing
	put pShowXGridLines into mGridHShow
	updateConstants()
	redraw all
end handler

public handler setYLinesVisibility(in pShowYGridLines as Boolean) returns nothing
	put pShowYGridLines into mGridVShow
	updateConstants()
	redraw all
end handler
----------

----------
-- this handler gets the graphColors property, returning the graphColors as a list of colors
public handler getColors() returns String
	// Work out how many colors there should be
	log "Trying to return colors"
	variable tMax as Integer
	put the number of elements in mData - 1 into tMax

	variable tColors as String
	variable tColor as Color
	variable tColorStringList as List
	variable tCount as Integer
	put the empty list into tColorStringList
	repeat with tCount from 1 up to tMax
		log tCount
		if the number of elements in mColors >= tCount then
			put element tCount of mColors into tColor
			push colorToString(tColor) onto back of tColorStringList
		else
			push "0,0,0,0" onto back of tColorStringList
			push stringToColor("0,0,0,0") onto back of mColors
		end if
	end repeat
	combine tColorStringList with newline into tColors
	return tColors
end handler
----------

----------
-- this handler gets the graphData property, returning a string
public handler getData() returns String
	// Convert mData in a form that can be combined for output
	variable tData as List
	put the empty list into tData

	variable tCount as Integer
	variable tCount2 as Integer
	variable tMax as Integer
	variable tLineData as List
	put the empty list into tLineData
	put the number of elements in element 1 of mData into tMax

	//log "outputting data"

	repeat with tCount from 1 up to the number of elements in mData
		put element tCount of mData into tLineData
		repeat with tCount2 from 1 up to tMax
			if tCount is 1 then
				push the empty list onto back of tData
				push element tCount2 of tLineData onto element tCount2 of tData
				//log (the number of elements in tData) formatted as string
			else
				if the number of elements in tLineData >= tCount2 then
					if element tCount2 of tLineData is not nothing then
						//log (element tCount2 of tLineData) formatted as string
						variable tString as String
						put stripZeros((element tCount2 of tLineData) formatted as string) into tString
						//log tString
						push tString onto back of element tCount2 of tData
					else
						//log "empty element"
						push "" onto back of element tCount2 of tData
					end if
				else
					//log "blank element"
					push "" onto back of element tCount2 of tData
				end if
			end if
		end repeat
	end repeat

	// Flatten second level list
	variable tFinalList as List
	variable tFlatList as String
	put the empty list into tFinalList
	put the empty string into tFlatList

	repeat with tCount from 1 up to the number of elements in tData
		combine element tCount of tData with "," into tFlatList
		push tFlatList onto tFinalList
	end repeat

	// Combine the list for final output
	variable tReturnString as String
	combine tFinalList with newline into tReturnString
	//log tReturnString
	return tReturnString
end handler
----------

----------
-- this handler returns a paint
public handler getPaint(pLocation, pType) returns Paint
	if pLocation is "grid" then
		if pType is "fill" then
			return solid paint with stringToColor("248,248,248")
		else if pType is "stroke" then
			return solid paint with stringToColor("0,0,0,50")
		end if
	else if pLocation is "axis" and pType is "stroke" then
		return solid paint with stringToColor("0,0,0,150")
	end if

	return solid paint with stringToColor("255,100,200")
end handler
----------

----------
-- this handler returns a font name as a string
private handler getFontName() returns String
	return the name of the font of this canvas
end handler
----------

----------
-- this handler returns a font size
private handler getFontSize() returns Number
	return 11
end handler
----------

----------
-- this handler initialises the constants
private handler initialiseConstants() returns nothing

	variable tMax as Number
	variable tMin as Number
	variable tCount as Integer
	put 0 into mMax
	put 1000 into mMin
	repeat with tCount from 2 up to the number of elements in mData
		put listMax(element tCount of mData) into tMax
		if tMax > mMax then
			put tMax into mMax
		end if

		put listMax(element tCount of mData) into tMin
		if tMin < mMin then
			put tMin into mMin
		end if
	end repeat
	put the number of elements in (element 1 of mData) -1 into mHPointCount
	put 5 into mVPointCount

	put mMax / mVPointCount into mYIncrement

	updateConstants()
end handler
----------

----------
-- this handler updates the constants
private handler updateConstants() returns nothing
	put my height * 0.02 into mPadding
	if mPadding < 6 then
		put 6 into mPadding
	end if

	updateGridRect(rectangle [0,mPadding,my width-mPadding,my height-mPadding])
end handler
----------

----------
-- this handler updates the grid rectangle
private handler updateGridRect(in tRect as Rectangle) returns nothing
	put tRect into mGridRect
	put the height of mGridRect / mVPointCount into mGridVWidth
	put the width of mGridRect / mHPointCount into mGridHWidth
end handler
----------

----------
-- this handler is called when the geometry of the widget is changed
public handler onGeometryChanged() returns nothing
	updateConstants()
end handler
----------

----------
-- the following handlers draw the x and y labels on the graph
private handler drawXLabels() returns Real
	save state of this canvas
	translate this canvas by [100,100]
	rotate this canvas by 90

	fill text "hello" at point [0,0] on this canvas

	/*save state of this canvas


	variable tNumber as Real
	variable tString as String
	variable tTextBounds as Rectangle
	variable tMaxWidth as Real
	put 0 into tMaxWidth

	// Work out the max width first

	variable tLabels as List
	put element 1 of mData into tLabels

	variable tLabel as String
	repeat for each element tLabel in tLabels
		measure tLabel on this canvas
		put the result into tTextBounds
		if the width of tTextBounds > tMaxWidth then
			put the width of tTextBounds into tMaxWidth
		end if
	end repeat

	// Draw the text
	variable tCount as Integer
	variable tPos as Real
	variable tRect as Rectangle
	put 1 into tCount
	repeat for each element tLabel in tLabels

		put 0 - (((mHPointCount)-tCount) * mGridHWidth) into tPos
		put rectangle [0-tMaxWidth,tPos+20,0,tPos-20] into tRect
		fill text tLabel at left of tRect on this canvas
		log "-------"
		log (0-tMaxWidth) formatted as string
		log (tPos+20) formatted as string
		log "0"
		log (tPos-20) formatted as string
		add 1 to tCount
	end repeat
	*/

	restore state of this canvas

	return 0
end handler

private handler drawYLabels() returns Real
	set the font of this canvas to font getFontName() at size 10
	set the paint of this canvas to getPaint("axis","stroke")

	variable tNumber as Real
	variable tString as String
	variable tTextBounds as Rectangle
	variable tMaxWidth as Real
	put 0 into tMaxWidth

	// Work out the max width first
	variable tCount as Integer
	repeat with tCount from 0 up to mVPointCount
		put mYIncrement * tCount into tNumber
		put tNumber formatted as string into tString
		put char 1 to -8 of tString into tString

		measure tString on this canvas
		put the result into tTextBounds
		if the width of tTextBounds > tMaxWidth then
			put the width of tTextBounds into tMaxWidth
		end if
	end repeat

	// Draw the text
	repeat with tCount from 0 up to mVPointCount
		put mYIncrement * tCount into tNumber
		put tNumber formatted as string into tString
		put char 1 to -8 of tString into tString
		//fill text tString at point [0,the top of mGridRect + (((mVPointCount)-tCount) * mGridVWidth)] on this canvas
		variable tY as Real
		put the top of mGridRect + (((mVPointCount)-tCount) * mGridVWidth) into tY
		fill text tString at right of rectangle [0,tY-20,tMaxWidth,tY+20] on this canvas
	end repeat

	return tMaxWidth
end handler
----------

----------
-- this handler draws the graph legend
private handler drawLegend() returns nothing
end handler
----------

----------
-- this handler draws the graph
private handler drawGraph() returns nothing
	put gridPath() into mBackgroundGridPath
	put axisPath() into mBackgroundAxisPath

	variable tWidth as Real
	variable tHeight as Real
	put the width of mGridRect into tWidth
	put the height of mGridRect into tHeight

	// Draw grid lines
	set the paint of this canvas to getPaint("grid","stroke")
	set the antialias of this canvas to false
	set the stroke width of this canvas to 0
	stroke mBackgroundGridPath on this canvas

	// Draw the graph lines
	set the antialias of this canvas to true
	set the stroke width of this canvas to 1
	variable tDataPoints as List
	variable tCount2 as Integer
	variable tColour as Color

	variable tPath as Path
	variable tPathVertices as Path
	variable tPoint as Point

	variable tCount as Integer
	repeat with tCount from 2 up to the number of elements in mData
		put the empty path into tPath
		put the empty path into tPathVertices
		put element tCount of mData into tDataPoints
		if the number of elements in mColors >= tCount-1 then
			put element tCount-1 of mColors into tColour
		else
			put stringToColor("0,0,0") into tColour
		end if
		set the paint of this canvas to solid paint with tColour
		repeat with tCount2 from 1 up to the number of elements in tDataPoints
			if element tCount2 of tDataPoints is not nothing then
				if tCount2 is 1 then
					put point [the left of mGridRect + ((tCount2-1) * mGridHWidth),the top of mGridRect + (tHeight - (tHeight * (element tCount2 of tDataPoints / mMax)))] into tPoint
					move to tPoint on tPath
				else
					put point [the left of mGridRect + ((tCount2-1) * mGridHWidth),the top of mGridRect + (tHeight - (tHeight * (element tCount2 of tDataPoints / mMax)))] into tPoint
					line to tPoint on tPath
				end if

				// Draw vertices
				if mVerticesShow is true then
					add circle path centered at tPoint with radius 3 to tPathVertices
					//add rectangle path of rectangle [element 1 of tPoint - 3,element 2 of tPoint - 3,element 1 of tPoint + 3,element 2 of tPoint + 3] to tPathVertices
				end if
			end if
		end repeat
		stroke tPath on this canvas
		fill tPathVertices on this canvas
	end repeat

	// Draw Axis lines
	set the antialias of this canvas to false
	set the stroke width of this canvas to 1
	set the paint of this canvas to getPaint("axis","stroke")
	stroke mBackgroundAxisPath on this canvas
end handler
----------

----------
-- this handler returns the graph rectangle
private handler GraphRect() returns Rectangle
	return rectangle [0,0,my width,my height]
end handler
----------

----------
-- this handler converts a string of numbers to an RGBA color
private handler stringToColor(in pString as String) returns Color
	variable tRed as Real
	variable tGreen as Real
	variable tBlue as Real
	variable tAlpha as Real

	variable tComponentList as List
	split pString by "," into tComponentList

	variable tComponentCount
	put the number of elements in tComponentList into tComponentCount
	if tComponentCount is not 3 and tComponentCount is not 4 then
		// Invalid number of components detected
		throw "Invalid color"
	end if

	put (element 1 of tComponentList) parsed as number into tRed
	put (element 2 of tComponentList) parsed as number into tGreen
	put (element 3 of tComponentList) parsed as number into tBlue

	if tComponentCount is 4 then
		put (element 4 of tComponentList) parsed as number into tAlpha
	else
		put 255 into tAlpha
	end if

	return color [ tRed/255, tGreen/255, tBlue/255, tAlpha/255 ]
end handler
----------

----------
-- this handler converts an RGBA color to a string
private handler colorToString(in pColor as Color) returns String
	return stripZeros((the rounded of ((the red of pColor) * 255)) formatted as string) & "," & stripZeros((the rounded of ((the green of pColor) * 255)) formatted as string) & "," & stripZeros((the rounded of ((the blue of pColor) * 255)) formatted as string) & "," & stripZeros((the rounded of ((the alpha of pColor) * 255)) formatted as string)
end handler
----------

----------
-- this handler draws the grid lines on the graph
private handler gridPath() returns Path
	variable tPath
	variable tCount
	put the empty path into tPath

	// Create the path for the horizontal grid
	if mGridHShow is true then
		repeat with tCount from 1 up to mVPointCount
			move to point [the left of mGridRect,the top of mGridRect + (mGridVWidth * tCount-1)] on tPath
			line to point [the right of mGridRect,the top of mGridRect + (mGridVWidth * tCount-1)] on tPath
		end repeat
	end if

	// Create the path for the vertical grid
	if mGridVShow is true then
		repeat with tCount from 1 up to mHPointCount
			move to point [the left of mGridRect + (mGridHWidth * tCount-1),the top of mGridRect] on tPath
			line to point [the left of mGridRect + (mGridHWidth * tCount-1),the bottom of mGridRect] on tPath
		end repeat
	end if

	return tPath
end handler
----------

----------
-- this handler draws the graph axes
private handler axisPath() returns Path
	variable tPath
	put the empty path into tPath

	move to point [the left of mGridRect + 0.5,the top of mGridRect] on tPath
	line to point [the left of mGridRect + 0.5,the bottom of mGridRect] on tPath
	move to point [the left of mGridRect,the bottom of mGridRect-0.5] on tPath
	line to point [the right of mGridRect,the bottom of mGridRect-0.5] on tPath

	return tPath
end handler
----------

// UTILITY FUNCTIONS
----------
-- this handler strips the zeros when a integer is formatted as a string
private handler stripZeros(in tString as String) returns String
	if tString contains "." then
		variable tCount as Integer
		repeat while ((the last char of tString) is in ".0")
			if the last char of tString is "." then
				delete the last char of tString
				exit repeat
			else
				delete the last char of tString
			end if
		end repeat
	end if
	return tString
end handler
----------

----------
-- the following handlers return the maximum/minimum number from a list
private handler listMax(in tList as List) returns Number
	variable tMax as Number
	put 0 into tMax

	variable tElement as optional any
	repeat for each element tElement in tList
		if tElement is a number then
			if tElement > tMax then
				put tElement into tMax
			end if
		end if
	end repeat

	return tMax
end handler

private handler listMin(in tList as List) returns Number
	variable tMin as optional Number
	put nothing into tMin

	variable tElement as optional any
	repeat for each element tElement in tList
		if tElement is a number then
			if tElement < tMin then
				put tElement into tMin
			end if
		end if
	end repeat

	return tMin
end handler
----------

end widget
