'OHRRPGCE - Classes for additional slice types
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"
#include "allmodex.bi"
#include "common.bi"
#include "loading.bi"
#include "const.bi"
#include "uiconst.bi"
#include "slices.bi"
#include "sliceedit.bi"

#include "specialslices.bi"


function highlight_color(col as integer, highlight as bool) as integer
 if highlight = NO then return col
 with master(col)
  return findrgb(.r + 100, .g + 100, .b + 100)
 end with
end function


'==============================================================================
'                                   GraphSlice
'==============================================================================


constructor GraphSlice()
 axiscol = uilook(uiMenuItem)
 linecol = findrgb(255,255,255)
 gridcol = uilook(uiHighlight)
 labelcol = uilook(uiMenuItem)
'findrgb(0,0,160)
 default_maxy = 1
end constructor

sub GraphSlice.Initialize(sl as Slice ptr)
 this.sl = sl
 'This padding tries to estimate the amount of additional space needed
 'for the axes, but it's not accurate
 sl->PaddingLeft = 52
 sl->PaddingRight = 12
 sl->PaddingTop = 5
 sl->PaddingBottom = 14
 field_sl = NewSliceOfType(slContainer, sl)
 field_sl->Fill = YES
 field_sl->Protect = YES
end sub

local sub set_min_and_max(byref minv as double, byref maxv as double, vals() as double, defaultmax as double)
 minv = 1e99
 maxv = -1e99
 for i as integer = 0 to ubound(vals)
  if vals(i) < minv then minv = vals(i)
  if vals(i) > maxv then maxv = vals(i)
 next
 'Decide whether to use 0 at the origin, or minv
 if minv > 0 andalso minv < 0.8 * maxv then minv = 0
 if minv = maxv then
  if minv < 0 then
   maxv = 0
  elseif minv = 0 then
   maxv = defaultmax
  else
   minv = 0
  end if
 end if
end sub

'Updates x/yscale and min/maxx/y
sub GraphSlice.update_bounds()
 set_min_and_max minx, maxx, x(), 1.
 set_min_and_max miny, maxy, y(), default_maxy

 RefreshSliceTreeScreenPos(sl)
 xscale = (field_sl->Width - 1) / (maxx - minx)
 yscale = (field_sl->Height - 1) / (maxy - miny)
end sub

'Screen position of bottom-left corner of field
function GraphSlice.origin() as XYPair
 return XY(field_sl->ScreenX, field_sl->ScreenY + field_sl->Height - 1)
end function

function GraphSlice.point_to_screen(pointx as double, pointy as double) as XYPair
 return origin() + XY((pointx - minx) * xscale, -(pointy - miny) * yscale)
end function

local sub draw_label(label as string, pos as XYPair, col as integer, highlightcol as integer = -1, page as integer)
 if highlightcol > -1 then
  'Unfortunately, just drawing a normal background behind the text doesn't provide any margin.
  'Maybe should add another setting to render_text or Font to tweak that
  dim size as XYPair = textsize(label) + XY(3, 3)
  pos.x = relative_pos(pos.x, 0, size.w)
  pos.y = relative_pos(pos.y, 0, size.h)
  rectangle pos.x, pos.y, size.w, size.h, highlightcol, page
  pos += XY(1, 2)  'Center text
 end if
 edgeprint label, pos.x, pos.y, col, page
end sub

function GraphSlice.draw_x_tick(value as double, page as integer, highlight as bool = NO, showlabel as bool = YES) as integer
 dim pos as XYPair = point_to_screen(value, miny)
 rectangle pos.x, pos.y, 1, 5, axiscol, page
 rectangle pos.x, pos.y, 1, -field_sl->Height, highlight_color(gridcol, highlight), page

 if showlabel = NO then return 0
 dim label as string = format_float(value, 3)
 'if int_x_labels then label = str(int(value))

 pos += XY(ancCenter, 5)
 draw_label label, pos, iif(highlight, linecol, labelcol), iif(highlight, gridcol, -1), page
 'edgeprint label, pos.x + ancCenter, pos.y + 5, labelcol, page
 return textwidth(label)
end function

function GraphSlice.draw_y_tick(value as double, page as integer, highlight as bool = NO, showlabel as bool = YES) as integer
 dim pos as XYPair = point_to_screen(minx, value)
 rectangle pos.x, pos.y, -5, 1, axiscol, page
 rectangle pos.x, pos.y, field_sl->Width, 1, highlight_color(gridcol, highlight), page

 if showlabel = NO then return 0
 dim label as string = format_float(value, 3)
 'if int_y_labels then label = str(int(value))

 pos += XY(ancRight - 5, ancCenter)
 draw_label label, pos, iif(highlight, linecol, labelcol), iif(highlight, gridcol, -1), page
 return 10  'Label height
end function

'This draws all the ticks on an axis except for the min and max ones
sub GraphSlice.draw_regular_ticks(axis as integer, labelsize as integer, page as integer)
 dim as double tickstep, tickfirst, ticklast, scale

 if axis = 0 then
  tickfirst = minx
  ticklast = maxx
  scale = xscale
  'Horizontal spacing is very important
  labelsize += 3
 else
  tickfirst = miny
  ticklast = maxy
  scale = yscale
 end if

 dim as double maxrange, max_next_pow10
 maxrange = large(abs(tickfirst), abs(ticklast))
 if maxrange = 0 then maxrange = 1
 max_next_pow10 = 10 ^ (1 + INT(LOG(maxrange) / LOG(10)))

 'Search through a sequence of increasing ticksteps which are round numbers
 'until we find one which puts the labels far enough apart
 static steps(...) as double = {1, 2, 2.5, 5}
 dim pix_per_label as integer
 for decade as integer = -3 to 0
  for stepi as integer = 0 to ubound(steps)
   tickstep = steps(stepi) * 10 ^ decade * max_next_pow10
   if ABS(tickstep - CINT(tickstep)) > 1e-4 then  'Not an integer value
    if axis = 0 andalso int_x_labels then continue for
    if axis = 1 andalso int_y_labels then continue for
   end if
   pix_per_label = tickstep * scale
   if pix_per_label > labelsize then exit for, for
  next stepi
 next decade

 'Forward from the min to the first regular tick
 dim ticknext as double
 ticknext = (INT(tickfirst / tickstep) + 1) * tickstep

 while ticknext < ticklast
  dim showlabel as bool = YES
  'Skip the label if it's too close to the start or end
  if (ticknext - tickfirst) * scale < labelsize then showlabel = NO
  if (ticklast - ticknext)  * scale < labelsize then showlabel = NO

  if axis = 0 then
   draw_x_tick ticknext, page, , showlabel
  else
   draw_y_tick ticknext, page, , showlabel
  end if
  ticknext += tickstep
 wend
end sub

sub GraphSlice.Draw(sl as Slice ptr, page as integer)
 update_bounds()

 dim as integer labelwidth, labelheight = 10
 ' Draw first and last ticks
 labelwidth = large(draw_x_tick(minx, page), _
                    draw_x_tick(maxx, page))
 sl->PaddingRight = large(12, labelwidth \ 2 + 1)
 draw_y_tick(miny, page)
 draw_y_tick(maxy, page)
 'If not sticking to integer labels, intermediate labels might be
 'longer than the first/last due to decimal places
 '(TODO: this is a poor solution)
 if int_x_labels = NO then labelwidth = large(32, labelwidth)

 ' Draw the other ticks
 draw_regular_ticks(0, labelwidth, page)  'X axis
 draw_regular_ticks(1, labelheight, page)  'Y axis

 if showpoint then
  draw_x_tick showx, page, YES
  draw_y_tick showy, page, YES
 end if

 'Draw the border lines along the axes
 dim orn as XYPair = origin()
 rectangle orn.x, orn.y, 1, -field_sl->Height, axiscol, page
 rectangle orn.x, orn.y, field_sl->Width, 1, axiscol, page

 'Draw the graph
 for i as integer = 1 to ubound(x)
  dim p1 as XYPair = point_to_screen(x(i-1), y(i-1))
  dim p2 as XYPair = point_to_screen(x(i), y(i))
  drawline p1.x, p1.y, p2.x, p2.y, linecol, page
 next
end sub

sub GraphSlice.mouse_over()
 update_bounds()
 dim byref mouse as MouseInfo = readmouse
 if SliceCollidePoint(sl, mouse.pos) = NO then
  showpoint = NO
  exit sub
 end if

 dim besti as integer
 dim bestdist as double = INT_MAX

 if mouse.x >= field_sl->ScreenX then   ' Mouse not over Y axis
  ' Find the nearest point on the graph
  for i as integer = 0 to ubound(x)
   dim dist as double = xypair_distance_squared(mouse.pos, point_to_screen(x(i), y(i)))
   if dist < bestdist then
    bestdist = dist
    besti = i
   end if
  next
 end if

 if bestdist > 10 ^ 2 then
  ' The mouse isn't close to the line, so use the mouse to pick X or Y position instead
  dim mpos as XYPair = mouse.pos - origin()
  mpos.y *= -1
  bestdist = INT_MAX
  for i as integer = 0 to ubound(x)
   dim dist as double
   if mouse.x < field_sl->ScreenX then
    ' Mouse over Y axis
    dist = abs(mpos.y - y(i) * yscale)
   else
    dist = abs(mpos.x - x(i) * xscale)
   end if
   if dist < bestdist then
    bestdist = dist
    besti = i
   end if
  next
 end if

 showpoint = YES
 showx = x(besti)
 showy = y(besti)
end sub

'==============================================================================
'                                   MenuDefSlice
'==============================================================================

'This rather unambitious MenuDefSlice class simply draws a MenuDef with a
'MenuState at the correct depth in a slice tree. It doesn't attempt to
'handle proper slice positioning yet, but there is no special reason why it
'could not in the future

'Note that the calling code is responsible for managing the lifetime of the
'MenuDef and MenuState objects. This class only attempts to hold rather unsafe
'pointers to them *hamstershrug*

constructor MenuDefSlice()
 'Pointers should start null
 mdef = 0
 st = 0
end constructor

sub MenuDefSlice.Initialize(sl as Slice ptr)
 this.sl = sl
end sub

sub MenuDefSlice.Draw(sl as Slice ptr, page as integer)
 if mdef = 0 orelse st = 0 then
  'No MenuDef is currently assigned to this MenuDefSlice
  exit sub
 end if
 
 'If the mdef and sl pointed to instances that had already been freed, that would be bad, and this would crash
 draw_menu *mdef, *st, page

end sub