--[[----------------------------------------------------------------------------

WFFiniteStateMachineUtils.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]
--
local KSTableUtils = require "KSTableUtils"
local WFArguments = require 'WFArguments'
local WFArguments_check = assert( WFArguments.check, 
										"missing WFArguments.check" )

local WFFunctionUtils = require "WFFunctionUtils"

local WFFiniteStateMachine = require "WFFiniteStateMachine"

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

local function addValidState( validStates, stateName, allValidStates, errMsg )

	if not allValidStates[ stateName ] then

		error( string.format( "%s: %s", errMsg, stateName ) )

	end

	validStates[ stateName ] = stateName

	return validStates

end

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

local function wrapInReturnEmptyStrForEmptyArray( f )

	return function( array, ... )

		return #array == 0 and "" or f( array, ... )

	end

end

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

local function makeColoredRankedNode( stateName, stateColorMap )

	return string.format( [[ 
					node[ peripheries=2  shape = circle style="filled" color=black fillcolor="%s"];
					"%s";]], stateColorMap[ stateName ], 
					stateName )

end

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

local function makeRankedNodes( stateNames, rankStr, stateColorMap )

	local coloredNodes = WFFunctionUtils.map( makeColoredRankedNode, 
												stateNames, stateColorMap )

	return string.format( [[ 
				{ rank = %s; 
					%s }; ]], rankStr, table.concat( coloredNodes, '\n' ) )

end

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

makeRankedNodes = wrapInReturnEmptyStrForEmptyArray( makeRankedNodes )

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

local function makeStartStatesNodesStr( startStates, stateColorMap )

	return makeRankedNodes( startStates, "min", stateColorMap )

end

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

local function makeEndStatesNodesStr( endStates, stateColorMap )

	return makeRankedNodes( endStates, "max", stateColorMap )

end

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

local function interpolate( x, y, i, n )

	return x +  ( i - 1 ) * ( y - x ) / ( n - 1 )

end

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

local function rampUpAndDown( x, y, i, n )

	local mid = math.floor( n / 2 )

	if i <= mid then

		return interpolate( x, y, i, mid )

	end

	return interpolate( y, x, i - mid + 1, n - mid )

end

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

local function rampUpAndDown2( x, y, i, n )

	local mid = math.floor( n / 2 )

	if i <= mid then

		return rampUpAndDown( x, y, i, mid )

	end

	return rampUpAndDown( y, x, i - mid + 1, n - mid )

end

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

local function getStateGroupColors( stateGroupNames )

	local startR, startG, startB = 255, 255, 192

	local endR, endG, endB = 192, 192, 255

	local stateGroupColors = {}

	for i, stateGroupName in ipairs( stateGroupNames ) do

		stateGroupColors[ stateGroupName ] = 
			string.format( "#%02x%02x%02x",
							interpolate( startR, endR, i, #stateGroupNames ),
							rampUpAndDown2( startG, endG, i, #stateGroupNames ),
							interpolate( startB, endB, i, #stateGroupNames ) )

	end

	return stateGroupColors

end

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

local function makeStateGroupNode( stateGroupName, stateGroupColors )

	return string.format( 'node[ style=filled color="%s"];\n\t"%s"',
						stateGroupColors[ stateGroupName ], stateGroupName )

end

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

local function makeStateGroupColorKey( stateGroupNames )

	return string.format( [[
				subgraph clusterColorKey { 
					"%s";
					label = "state groups";
				};]], table.concat( stateGroupNames, '"; "' ) )

end

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

local function getNextStateUnlessSeen( transition, fsm, stateName, seenStates )

	local nextStateName = WFFiniteStateMachine.getNextState( fsm, stateName,
																transition )

	if not seenStates[ nextStateName ] then

		return nextStateName

	end
	
end

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

local function getReachableStates( fsm, startState, endStatesMap,
										reachableStates )

	reachableStates = reachableStates or {}

	local transitions = 
			WFFiniteStateMachine.getStateTransitions( fsm, startState )

	local nextStates = WFFunctionUtils.filter( 
						getNextStateUnlessSeen, 
							transitions, fsm, startState, reachableStates )

	reachableStates[ startState ] = startState

	for _, nextState in ipairs( nextStates ) do

		if not endStatesMap[ nextState ] then

			reachableStates = getReachableStates( fsm, nextState, endStatesMap,
													reachableStates )

		else

			reachableStates[ nextState ] = true

		end

	end

	return reachableStates

end

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

makeStateGroupColorKey = 
	wrapInReturnEmptyStrForEmptyArray( makeStateGroupColorKey )

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

local function isMiddleState( stateName, middleStateMap )

	return middleStateMap[ stateName ] and stateName or nil

end

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

local function getStateGroupMiddleStates( fsm, stateGroupName, middleStateMap )

	local stateGroupStates =
				WFFiniteStateMachine.getStateGroupStates( fsm, stateGroupName )

	return WFFunctionUtils.filter( isMiddleState, stateGroupStates,
										middleStateMap )

end

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

local function makeStateGroupStateNodesStr( stateGroupColor, 
												stateGroupMiddleStates )

	return string.format( 
				[[node[ style=filled color="%s"];"%s";]],
				stateGroupColor,
				table.concat( stateGroupMiddleStates, '"; "' ) )

end

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

makeStateGroupStateNodesStr = 
	wrapInReturnEmptyStrForEmptyArray( makeStateGroupStateNodesStr )

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

local function makeStateGroupStateNodes( stateGroupName, fsm, stateGroupColors,
											middleStateMap )

	local stateGroupMiddleStates =
			getStateGroupMiddleStates( fsm, stateGroupName, middleStateMap )
	
	return #stateGroupMiddleStates > 0 and 
				makeStateGroupStateNodesStr( 
					stateGroupColors[ stateGroupName ],
					stateGroupMiddleStates ) or
				nil

end

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

local function addStateName( map, stateName )

	map[ stateName ] = stateName

	return map

end

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

local function addStateGroupStates( statesInStateGroupMap, stateGroupName, fsm )

	return WFFunctionUtils.reduce( 
			statesInStateGroupMap,
			addStateName,
			WFFiniteStateMachine.getStateGroupStates( fsm, stateGroupName ) )

end

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

local function isStateUndeclared( stateName, statesInStateGroupMap, 
												middleStateMap )

	return not statesInStateGroupMap[ stateName ] and 
			not middleStateMap[ stateName ] and 
				stateName or nil

end

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

local function stringifyAndConcat( strs )

	return string.format( [["%s";]], table.concat( strs, '"; "' ) )

end

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

stringifyAndConcat = wrapInReturnEmptyStrForEmptyArray( stringifyAndConcat )

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

local makeUndeclaredStatesStr = stringifyAndConcat

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

local function addEdgeForStateTransition( edges, transition, stateName, fsm, 
											omitNextStateMap )

	local nextStateName = WFFiniteStateMachine.getNextState( fsm, stateName,
																transition )

	if stateName == nextStateName or omitNextStateMap[ nextStateName ] then

		return edges

	end

	edges[ #edges + 1 ] = string.format( '"%s" -> "%s"[label = "%s"]', 
											stateName, nextStateName,
											transition )

	return edges

end

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

local function addEdgesForState( edges, stateName, fsm, omitNextStateMap )

	local stateTransitions = 
			WFFiniteStateMachine.getStateTransitions( fsm, stateName )

	return WFFunctionUtils.reduce( edges,
				addEdgeForStateTransition,
					stateTransitions, stateName, fsm, omitNextStateMap )

end

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

local function getEdgesStr( fsm, startStatesMap, middleStateMap, 
								reachableEndStatesMap, endStatesMap )

	local edges = {}

	for _, stateMap in pairs( { startStatesMap, middleStateMap, 
									reachableEndStatesMap } ) do

		for stateName in pairs( stateMap ) do

			local omitNextStateMap = 
					stateMap == reachableEndStatesMap and endStatesMap or {}	

			edges = addEdgesForState( edges, stateName, fsm, omitNextStateMap )

		end

	end

	table.sort( edges )

	return #edges == 0 and "" or table.concat( edges, ";\n\t\t" )

end

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

local function addMiddleStates( middleStateMap, startState, fsm, endStatesMap )

	-- make a map of all reachable states that aren't start or end states
	middleStateMap = 
		getReachableStates( fsm, startState, endStatesMap, middleStateMap )

	middleStateMap[ startState ] = nil

	return middleStateMap

end

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

local function addStateGroupStateToColorMap( stateColorMap, stateName, color )

	stateColorMap[ stateName ] = color

	return stateColorMap

end

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

local function addStateGroupStatesToColorMap( stateColorMap, stateGroupName, 
												fsm, stateGroupColors )

	return WFFunctionUtils.reduce( stateColorMap, 
			addStateGroupStateToColorMap, 
				WFFiniteStateMachine.getStateGroupStates( 
					fsm, stateGroupName ), 
				stateGroupColors[ stateGroupName ] )

end

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

local function getAsDotDiagramStrImp( fsm, wantKey, title, size, startStates,
										endStates )

	local stateMap = WFFiniteStateMachine.getStateMap( fsm )

	-- group states by state groups assigining a unique color to each state
	-- group.
	local stateGroupNames = WFFiniteStateMachine.getStateGroupNames( fsm )

	local stateGroupColors = getStateGroupColors( stateGroupNames )

	local stateColorMap = WFFunctionUtils.reduce( {}, 
							addStateGroupStatesToColorMap, 
								stateGroupNames, fsm, stateGroupColors )

	local startStatesNodesStr = makeStartStatesNodesStr( 
									startStates, stateColorMap )

	local endStatesMap = WFFunctionUtils.reduce( {}, 
								addValidState, 
									endStates, stateMap, 
									"endState not valid" )

	local stateGroupNodes = WFFunctionUtils.map( makeStateGroupNode,
													stateGroupNames,
													stateGroupColors )

	local stateGroupNodesStr = table.concat( stateGroupNodes, "\n" )

	-- create a "key" section of the diagram which explain which colors
	-- correspond to which state group
	local stateGroupColorKey = makeStateGroupColorKey( stateGroupNames )

	local middleStateMap = WFFunctionUtils.reduce( {}, 
								addMiddleStates, 
									startStates, fsm, endStatesMap )

	local startStatesMap = KSTableUtils.mapArrayToSet( startStates )

	local reachableEndStatesMap = {}

	local reachableEndStates = {}

	for endState in pairs( endStatesMap ) do

		if middleStateMap[ endState ] then

			middleStateMap[ endState ] = nil

			reachableEndStatesMap[ endState ] = endState

			reachableEndStates[ #reachableEndStates + 1 ] = endState

		end

	end

	table.sort( reachableEndStates )

	local endStateNodesStr = makeEndStatesNodesStr( 
								reachableEndStates, stateColorMap )

	-- write out all of the states for each state group except if the state
	-- group consists solely of start and end states.
	local stateGroupStateNodes = WFFunctionUtils.filter( 
										makeStateGroupStateNodes, 
											stateGroupNames, 
												fsm, stateGroupColors,
												middleStateMap )

	table.sort( stateGroupStateNodes )

	local stateGroupStateNodesStr = table.concat( stateGroupStateNodes, "\n" )

	-- get a map of all states that are in a state group 
	local statesInStateGroupMap = WFFunctionUtils.reduce( {},
										addStateGroupStates,
											stateGroupNames,
												fsm )

	local undeclaredStates = WFFunctionUtils.filter(
						isStateUndeclared,
						WFFiniteStateMachine.getStates( fsm ),
						statesInStateGroupMap, middleStateMap )

	table.sort( undeclaredStates )

	local undeclaredStatesStr = makeUndeclaredStatesStr( undeclaredStates )

	local edgesStr = getEdgesStr( fsm, startStatesMap, middleStateMap,
									reachableEndStatesMap, endStatesMap )

	return string.format( [[
		digraph finite_state_machine {

			dpi=144;
			label = "%s"; /* (title) */
			size="%s"; /* (size) */
			fontname="Helvetica";
			fontsize=24;
	
			labelloc=t;

			node[fontname="Helvetica"; fontsize=16; sep="+24,24";];

			node [shape = square;];

			/* define state groups (stateGroupNodesStr) */
			%s

			/* define state group color key (stateGroupColorKey) */
			%s

			/* starting nodes (startStatesNodesStr) */
			%s

			/* state group state nodes (stateGroupStateNodesStr) */
			%s

			/* ending nodes (endStateNodesStr) */
			%s
			
			/* other states (undeclaredStatesStr) */
			%s

			/* edges (edgesStr) */
			edge[fontsize = 9.0; fontname="Helvetica";];

			%s;
		}]], 
		title,
		size,
		wantKey and stateGroupNodesStr .. ";" or "",
		wantKey and stateGroupColorKey or "",
		startStatesNodesStr,
		stateGroupStateNodesStr,
		endStateNodesStr,
		undeclaredStatesStr,
		edgesStr )

end

--------------------------------------------------------------------------------
--[[!
-- \brief Serializes a FSM to a string that is in the dot language format.
			See http://graphviz.org for more info.  You can generate a diagram
			from a dot language file, by running it through the dot command
			line program.

			To install that on the mac, use homebrew or Macports

			% # homebrew instructions

			% brew install graphviz

			Once installed, use 

			% dot -Tpng 20150513previewServerThreadGraph.dot > prettyGraph.png

			to generate a png file for the graph

	\param (WFFiniteStateMachine object) An exising FSM object

	\param (string) a title for the graph emitted

	\param (number) a size for the graph emitted

	\param (array) An array of state name strings that are start states. The
					resultant diagram will have these at the top.

	\param (array) An array of state name strings that are end states. The
	resultant diagram will have these at the bottom.
	
	\return (string) A string representing the FSM in dot language format. Save
				it to a file and use the dot program to generate a png.
*/
--]]
local function getAsDotDiagramStr( fsm, title, size, startStates, endStates )

	WFArguments_check( "WFFiniteStateMachine", "string", "number", "table", "table" )

	return getAsDotDiagramStrImp( fsm, false, title, size, startStates,
										endStates )

end

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

local function getAsDotDiagramStrWithKey( fsm, title, size, startStates, 
											endStates )

	WFArguments_check( "WFFiniteStateMachine", "string", "number", "table", "table" )

	return getAsDotDiagramStrImp( fsm, true, title, size, startStates,
										endStates )

end

--==============================================================================

local function getOrCreateSrcDestPairForState( stateToSrcDestMaps, state )

	stateToSrcDestMaps[ state ] = stateToSrcDestMaps[ state ] or { {}, {} }

	return stateToSrcDestMaps[ state ] 

end

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

local function addToStateToSrcDestMapsForTransition( 
				stateToSrcDestMaps, transition, fsm, srcState )

	local destState = WFFiniteStateMachine.getNextState( 
						fsm, srcState, transition )

	-- update the destStateMap for the srcState
	local srcDestMapPairForSrcState =
		getOrCreateSrcDestPairForState( stateToSrcDestMaps, srcState )

	local destMapForSrcState = srcDestMapPairForSrcState[ 2 ]

	destMapForSrcState[ transition ] = destState

	srcDestMapPairForSrcState[ 2 ] = destMapForSrcState

	stateToSrcDestMaps[ srcState ] = srcDestMapPairForSrcState
		
	-- update the srcStateMap for the destState
	local srcDestMapPairForDestState =
		getOrCreateSrcDestPairForState( stateToSrcDestMaps, destState )

	local srcMapForDestState = srcDestMapPairForDestState[ 1 ]

	srcMapForDestState[ srcState ] = transition

	srcDestMapPairForDestState[ 1 ] = srcMapForDestState

	stateToSrcDestMaps[ destState ] = srcDestMapPairForDestState

	return stateToSrcDestMaps 

end

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

local function addSrcDestMapsForState( stateToSrcDestMaps, state, fsm )

	local transitions = WFFiniteStateMachine.getStateTransitions( fsm, state )
	
	return WFFunctionUtils.reduce(
				stateToSrcDestMaps,
					addToStateToSrcDestMapsForTransition,
						transitions, fsm, state )

end

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

local function reduceT( acc, f, t, ... )

	for k, v in pairs( t ) do

		acc = f( acc, k, v, ... )

	end

	return acc

end

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

local function keyValPairs( t )

	return reduceT( {}, 
			function( kvPairs, k, v )

				kvPairs[ #kvPairs + 1 ] = { k, v  }

				return kvPairs

			end, t )

end

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

local function wrapInTag( tag, str )

	return string.format( "<%s>%s</%s>", tag, str, tag )

end

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

local function wrapInTagWithAttr( tag, attrStr, str )

	return string.format( "<%s %s>%s</%s>", tag, attrStr, str, tag )

end

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

local function formatState( state, startStatesSet, finishStatesSet )

	local className = 
			( startStatesSet[ state ] and "startState" ) or
			( finishStatesSet[ state ] and "finishState" ) or
			"intermediateState"

	return wrapInTagWithAttr( "span", string.format( "class='%s'", className ),
								state )

end

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

local function makeStateLinkHref( state )

	return string.format( 'href="#%s"', state )

end

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

local function makeTableHeaderRow( headers )

	local headerCells =
				WFFunctionUtils.reduce( "",
					function( tableHeadCells, headerText )

						return tableHeadCells .. wrapInTag( "th", headerText )

					end, headers )

	return wrapInTag( "thead", 
			wrapInTag(  "tr", headerCells ) )

end

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

local function formatTransition( transition )

	return wrapInTagWithAttr( "span", "class='transition'", transition )

end

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

local function addSrcStateRow( srcStateRows, srcStateTransitionPair, state, 
								startStatesSet, finishStatesSet )

	local srcState, transition = unpack( srcStateTransitionPair )

	local formattedSrcState = 
			formatState( srcState, startStatesSet, finishStatesSet )

	local formattedState = 
			formatState( state, startStatesSet, finishStatesSet )

	local formattedTransition = formatTransition( transition )

	return srcStateRows ..
			wrapInTag( "tr",
				wrapInTag( "td",
					wrapInTag( "a " .. makeStateLinkHref( srcState ),
								formattedSrcState ) ) ..
				wrapInTag( "td", "+" ) ..
				wrapInTag( "td", formattedTransition ) ..
				wrapInTag( "td", "=&gt;" ) ..
				wrapInTag( "td", 
							wrapInTagWithAttr( 
								"a", makeStateLinkHref( state ), 
								formattedState ) ) )

end

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

local function addDestStateRow( destStateRows, transitionDestStatePair, state,
									startStatesSet, finishStatesSet )

	local transition, destState = unpack( transitionDestStatePair )

	local formattedState = 
			formatState( state, startStatesSet, finishStatesSet )

	local formattedDestState = 
			formatState( destState, startStatesSet, finishStatesSet )

	local formattedTransition = formatTransition( transition )

	return destStateRows ..
			wrapInTag( "tr",
				wrapInTag( "td", 
							wrapInTagWithAttr( 
								"a", makeStateLinkHref( state ), 
								formattedState ) ) ..
				wrapInTag( "td", "+" ) ..
				wrapInTag( "td", formattedTransition ) ..
				wrapInTag( "td", "=&gt;" ) ..
				wrapInTag( "td",
					wrapInTagWithAttr( 
						"a", makeStateLinkHref( destState ), 
						formattedDestState ) ) )

end

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

local function makeHTMLTable( headerRow, dataRows )

	return wrapInTag( "table", headerRow .. wrapInTag( "tbody", dataRows ) )

end

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

local function addHtmlTableForState( stateHTMLTables, state, srcDestMapPair,
										startStatesSet, finishStatesSet )

	local srcStateMap, destStateMap = unpack( srcDestMapPair )

	local srcStateTransitionPairs = keyValPairs( srcStateMap )

	local srcStatesHeaderRow = 
			makeTableHeaderRow( { "source state", "", "transition", "", "" } )

	local srcStatesDataRows = WFFunctionUtils.reduce( "",
								addSrcStateRow,
									srcStateTransitionPairs,
										state, startStatesSet, 
										finishStatesSet )

	local destStatesHeaderRow = 
			makeTableHeaderRow( { "", "", "transition", "", "dest state" } )

	local transitionDestStatePairs = keyValPairs( destStateMap )

	local destStatesDataRows = WFFunctionUtils.reduce( "",
								addDestStateRow,
									transitionDestStatePairs,
										state, startStatesSet, 
										finishStatesSet )

	local formattedState = 
			formatState( state, startStatesSet, finishStatesSet )

	return stateHTMLTables ..
			wrapInTagWithAttr( "div", string.format( "id='%s'", state ), 
				wrapInTag( "h3", string.format( "Source states leading to %s",
												formattedState ) ) ..
				makeHTMLTable( srcStatesHeaderRow, srcStatesDataRows ) ..
				wrapInTag( "h3", string.format( "Dest states starting from %s",
												formattedState ) ) ..
				makeHTMLTable( destStatesHeaderRow, destStatesDataRows ) )

end

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

local function addStateToStateToStateGroupNameMap(
				stateToStateGroupNameMap, state, stateGroupName )

	stateToStateGroupNameMap[ state ] = stateGroupName

	return stateToStateGroupNameMap

end

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

local function addStateGroupNameToStateToStateGroupNameMap( 
				stateToStateGroupNameMap, stateGroupName, fsm )

	local stateGroupStates =
			WFFiniteStateMachine.getStateGroupStates( fsm, stateGroupName )

	return WFFunctionUtils.reduce( 
			stateToStateGroupNameMap, 
				addStateToStateToStateGroupNameMap, 
					stateGroupStates, stateGroupName )

end

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

local function addStateToAllStatesGrouped( 
				allStatesGrouped, state, stateToStateGroupNameMap )

	local stateGroupName = stateToStateGroupNameMap[ state ] or "Other"

	local statesForGroup = allStatesGrouped[ stateGroupName ] or {}

	statesForGroup[ #statesForGroup + 1 ] = state

	allStatesGrouped[ stateGroupName ] = statesForGroup

	return allStatesGrouped

end

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

local function addStateAsListItem( listItems, state, startStatesSet, 
									finishStatesSet )

	local formattedState = 
			formatState( state, startStatesSet, finishStatesSet )

	return listItems .. 
			wrapInTag( "li", 
				wrapInTagWithAttr( "a", makeStateLinkHref( state ), 
									formattedState ) )

end

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

local function addStateGroupStatesToTableOfContentsHtml( 
				tocHtml, stateGroupName, stateGroupStates, 
											startStatesSet, finishStatesSet )

	return tocHtml .. 
			wrapInTag( "li", 
						stateGroupName .. 
							wrapInTag( "ul", 
								WFFunctionUtils.reduce( 
									"", 
									addStateAsListItem, 
										stateGroupStates, 
										startStatesSet, finishStatesSet ) ) )

end

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

local function getTocHtmlForStateGroupStates( 
				stateGroupStates, startStatesSet, finishStatesSet )

	return reduceT( "",
			addStateGroupStatesToTableOfContentsHtml, 
				stateGroupStates, startStatesSet, finishStatesSet )

end

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

local function getAllStatesTocHtml( fsm, startStatesSet, finishStatesSet )

	local states = WFFiniteStateMachine.getStates( fsm )
	
	local stateToStateGroupNameMap =
			WFFunctionUtils.reduce( {}, 
				addStateGroupNameToStateToStateGroupNameMap, 
					WFFiniteStateMachine.getStateGroupNames( fsm ), fsm )

	local allStatesGrouped =
			WFFunctionUtils.reduce( {}, 
				addStateToAllStatesGrouped, 
					states, stateToStateGroupNameMap )

	return getTocHtmlForStateGroupStates( 
			allStatesGrouped, startStatesSet, finishStatesSet )

end

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

local function getStateHtmlTables( fsm, startStatesSet, finishStatesSet )

	local states = WFFiniteStateMachine.getStates( fsm )

	local stateToSrcDestMaps = 
			WFFunctionUtils.reduce( {}, addSrcDestMapsForState, states, fsm )

	return reduceT( "", 
			addHtmlTableForState, 
				stateToSrcDestMaps, startStatesSet, finishStatesSet )

end

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

local function assembleHTML( title, description, tocSectionsHtml, stateHtmlTables )

	local tableOfContentsHtml = 
			wrapInTag( "h2", "Table of contents" ) ..
			wrapInTagWithAttr( "ul", 'id="default"', tocSectionsHtml )

	local descriptionHtml =
			#description > 0 and
				wrapInTag( "h2", "Description" ) ..
					wrapInTagWithAttr( "p", "class='description'", description ) or
				""

	return wrapInTag( "html",
			wrapInTag( "head",
				wrapInTag( "title", title ) ..
				wrapInTagWithAttr( "style", 'type="text/css"',
					[[
					  div.stateTables div:not(:target) {display: none}
					  div.stateTables div:target {display: block; outline: none}
					  div.stateTables th { background-color: #04AA6D; color: white; }
					  div.stateTables th, td { padding: 15px; text-align: left; }				  
					  div.stateTables tr:nth-child(even) {background-color: #f2f2f2;}
					  div.stateTables span.startState{ color: #88CC88; }
					  div.stateTables span.transition { color: black; font-style: italic; }
					  div.stateTables span.intermediateState{ color: #555555; }
					  div.stateTables span.finishState{ color: #CC2222; }
					]] )  ) ..
			wrapInTag( "body",
				wrapInTagWithAttr( "div", 'class="stateTables"', 
									stateHtmlTables .. descriptionHtml .. 
									tableOfContentsHtml ) ) )

end

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

local function getAsHTMLFile( fsm, title )

	local tocSectionsHtml = getAllStatesTocHtml( fsm, {}, {} )

	local stateHtmlTables = getStateHtmlTables( fsm, {}, {} )

	return assembleHTML( title, "", tocSectionsHtml, stateHtmlTables )

end

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

local function getStartFinishTocHtmlSections( startStatesSet, finishStatesSet )

	local startStates = KSTableUtils.extractKeys( startStatesSet )

	-- add the special states at the start of the TOC
	local tocHtmlStartSection = 
			getTocHtmlForStateGroupStates( 
				{ [ "Start States" ] = startStates }, 
				startStatesSet, finishStatesSet )

	local finishStates = KSTableUtils.extractKeys( finishStatesSet )

	local tocHtmlFinishSection = 
			getTocHtmlForStateGroupStates( 
				{ [ "Finish States" ] = finishStates }, 
				startStatesSet, finishStatesSet )

	return tocHtmlStartSection .. tocHtmlFinishSection

end

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

local function getAsHTMLFileWithExtraStartFinishTOCSection( 
				fsm, title, startStatesSet, finishStatesSet )

	local tocSectionsHtml =
			getStartFinishTocHtmlSections( startStatesSet, finishStatesSet ) ..
			getAllStatesTocHtml( fsm, startStatesSet, finishStatesSet )

	local stateHtmlTables = getStateHtmlTables( fsm, startStatesSet, 
												finishStatesSet )

	return assembleHTML( title, "", tocSectionsHtml, stateHtmlTables )

end

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

local function getAsHTMLFileWithJustStartFinishTOCSection( 
				fsm, title, description, startStatesSet, finishStatesSet )

	local tocSectionsHtml = getStartFinishTocHtmlSections( startStatesSet,
															finishStatesSet )

	local stateHtmlTables = getStateHtmlTables( fsm, startStatesSet, 
												finishStatesSet )

	return assembleHTML( title, description, tocSectionsHtml, stateHtmlTables )

end

--==============================================================================

return {

	getAsDotDiagramStr = getAsDotDiagramStr,

	getAsDotDiagramStrWithKey = getAsDotDiagramStrWithKey,

	getAsHTMLFile = getAsHTMLFile,

	getAsHTMLFileWithExtraStartFinishTOCSection = 
		getAsHTMLFileWithExtraStartFinishTOCSection,

	getAsHTMLFileWithJustStartFinishTOCSection = 
		getAsHTMLFileWithJustStartFinishTOCSection,

}
