'OHRRPGCE CUSTOM - Animation editor
'(C) Copyright 1997-2025 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 "udts.bi"
#include "custom_udts.bi"
#include "string.bi"
#include "allmodex.bi"
#include "reload.bi"
#include "sliceedit.bi"


' Export a certain animation as a looping gif
' (Would be nice if this didn't require a slice...)
sub export_gif(sl as Slice ptr, fname as string, anim as Animation ptr, transparent as bool = NO)
  BUG_IF(sl->SliceType <> slSprite, "require Sprite")
  LoadSpriteSliceImage sl

  dim writer as GifWriter
  dim gifpal as GifPalette
  GifPalette_from_pal gifpal, master(), sl->SpriteData->img.pal

  if GifBegin(@writer, fopen(fname, "wb"), sl->Size.w, sl->Size.h, 1, transparent, @gifpal) = NO then
    debug "GifWriter(" & fname & ") failed"
    safekill fname
    exit sub
  end if

  dim ms_remainder as integer = 0  'Left over wait ms due to rounding

  dim sprst as AnimationState ptr = sl->GetAnimState()
  sprst->start_animation(anim, 1)  'Loop once

  ' Run until the first wait
  sprst->animate()

  ' FIXME: if animations contain loops other than repeating, this could loop forever
  while sprst->anim
    dim frameidx as integer = sl->SpriteData->frame
    dim waitms as integer = sprst->skip_wait()
    'print "export op " & NodeName(sprst->curop) & " frame " & frameidx & " wait " & waitms
    if waitms < 0 then exit while  'Error

    if waitms > 0 then
      dim frametime as integer  'In hundredths of a second
      waitms += ms_remainder
      frametime = waitms \ 10
      ms_remainder = waitms MOD 10
      'print "export frame " & frameidx
      with sl->SpriteData->original_img[frameidx]  'Ignore .scaled
        if GifWriteFrame8(@writer, .image, .w, .h, frametime, NULL) = NO then
          debug "GifWriteFrame8 failed"
          safekill fname
        end if
      end with
    end if
  wend

  if GifEnd(@writer) = NO then
    debug "GifEnd failed"
  end if
end sub


'==========================================================================================
'                                     Animation Editor
'==========================================================================================

' Popup menus used for picking animation ops and slice properties
type AnimOpsMenuStack extends MenuStack
  sl as Slice ptr
  prop_info_list as SlicePropInfo vector

  ' If the user picks an animation, controls() returns YES and sets these
  ret_animop as integer
  ret_prop as SlicePropInfo ptr

  declare function controls() as bool
  declare sub start(itemrect as RectType, title as string)

 private:
  declare function cur_prop_info() as SlicePropInfo ptr
  declare sub open_prop_section_list()
  declare sub open_prop_section(get_section as string)

  ' MenuDefItem.t values
  enum
    tAnimOp = 1
    tPropSection
    tProperty
  end enum
end type

type AnimationEditor
  top_animset as AnimationSet ptr
  sl as Slice ptr
  original_sl as Slice ptr         'A copy of sl before we started changing it
  animstate as AnimationState ptr  'Always equal to sl->AnimState, just a convenience
  anim_context as AnimationContext
  default_export_filename as string

  declare constructor(sl as Slice ptr, top_animset as AnimationSet ptr, anim_context as AnimationContext)
  declare destructor()

  ' Top-level menu
  topstate as MenuState
  topmenu as SimpleMenuItem vector

  ' Animation editing menu
  prop_info_list as SlicePropInfo vector
  animops_menus as AnimOpsMenuStack
  using_strgrabber as bool
  debug_props as bool       'Show raw slice property keys and values

  declare sub reset_slice()
  declare sub toplevel()
  declare sub rebuild_toplevel_menu()
  declare sub rebuild_toplevel_menu_recurse(animset as AnimationSet ptr)
  declare function cur_animset() as AnimationSet ptr
  declare function cur_anim() as Animation ptr
  declare function cur_animname() as string
  declare function cur_anim_idx() as integer
  declare function animation_info(anim as Animation ptr) as string
  declare sub new_animation()
  declare sub delete_animation()
  declare sub export_menu(anim as Animation ptr, anim_name as string)

  ' Individual animation editor
  declare sub edit_animation(byref anim as Animation, anim_name as string)
  declare sub rebuild_animation_menu(byref menu as SimpleMenuItem vector, byref state as MenuState, byref anim as Animation, curnode as NodePtr = NULL)
  declare sub update_prop_info_list()
  declare function prop_info(propname as string) as SlicePropInfo ptr
  declare function prop_display_name(propname as string) as string
  declare function prop_display_value(propname as string, value_node as NodePtr) as string
  declare sub open_animops_menu(title as string, itemrect as RectType)
  declare function edit_property(propname as string, value_node as NodePtr, state as MenuState) as bool
  declare sub edit_animop_head(byref anim as Animation, curop as NodePtr, state as MenuState)
  declare sub edit_animop_arg(byref anim as Animation, curop as NodePtr, curoparg as NodePtr, state as MenuState)

  declare function create_animop_node(byref anim as Animation, animop as AnimOpType, prop as SlicePropInfo ptr = NULL) as NodePtr
  declare function add_default_keyframe(to_op as NodePtr, prop as SlicePropInfo ptr) as NodePtr
end type

sub animations_editor(sl as Slice ptr, animset as AnimationSet ptr, anim_context as AnimationContext, default_export_filename as string = "")
  var editor = AnimationEditor(sl, animset, anim_context)
  editor.default_export_filename = default_export_filename
  editor.toplevel()
end sub

constructor AnimationEditor(sl as Slice ptr, top_animset as AnimationSet ptr, anim_context as AnimationContext)
  ' Clone the slice because we're going to be causing changes to it, especially to AnimState
  this.original_sl = sl
  reset_slice
  this.top_animset = top_animset->reference()
  this.anim_context = anim_context
end constructor

destructor AnimationEditor()
  v_free prop_info_list
  animset_unload @top_animset
  DeleteSlice @sl
end destructor

' We can throw away and recreate the slice because a clone shares its AnimationSet
sub AnimationEditor.reset_slice()
  DeleteSlice @sl
  sl = CloneSliceTree(original_sl)
  animstate = sl->GetAnimState()
end sub


'==========================================================================================
'                              Toplevel animation picker menu
'==========================================================================================

const mdatExit = 1
const mdatNewAnim = 2
const mdatAnim = 3

sub AnimationEditor.rebuild_toplevel_menu()
  v_new topmenu
  append_simplemenu_item topmenu, "Previous Menu", , , mdatExit
  rebuild_toplevel_menu_recurse top_animset
  init_menu_state topstate, topmenu
end sub

sub AnimationEditor.rebuild_toplevel_menu_recurse(animset as AnimationSet ptr)
  if animset = NULL then exit sub

  append_simplemenu_item topmenu, "", YES  'Unselectable
  append_simplemenu_item topmenu, animset->name, YES, uilook(eduiHeading)  'Unselectable

  for idx as integer = 0 to v_len(animset->animations) - 1
    with *animset->animations[idx]
      append_simplemenu_item topmenu, rtrim(.name + " " + .variant), , , mdatAnim
      v_last(topmenu).datptr = animset
    end with
  next
  append_simplemenu_item topmenu, "Add new animation...", , , mdatNewAnim
  v_last(topmenu).datptr = animset

  rebuild_toplevel_menu_recurse(animset->shared_set)
  rebuild_toplevel_menu_recurse(animset->fallback_set)
end sub

' Also returns the AnimationSet on the "Add new animation" line
function AnimationEditor.cur_animset() as AnimationSet ptr
  return topmenu[topstate.pt].datptr
end function

' Name of selected animation
function AnimationEditor.cur_animname() as string
  if topmenu[topstate.pt].dat = mdatAnim then
    return topmenu[topstate.pt].text
  end if
end function

function AnimationEditor.cur_anim() as Animation ptr
  if topmenu[topstate.pt].dat = mdatAnim then
    return cur_animset->get_animation(topmenu[topstate.pt].text)
  end if
end function

' Get animset->animations index of selected animation, or -1
function AnimationEditor.cur_anim_idx() as integer
  dim anim as Animation ptr = cur_anim
  if anim then
    dim animations as Animation ptr vector = cur_animset->animations
    for idx as integer = 0 to v_len(animations) - 1
      if anim = animations[idx] then return idx
    next
    showbug("Couldn't find animation from get_animation")
  end if
  return -1
end function

function AnimationEditor.animation_info(anim as Animation ptr) as string
  if anim = NULL then return ""

  dim ret as string

  for idx as integer = 0 to 9999
    with builtin_animations(idx)
      if .name = NULL then exit for
      if *.name = anim->name then
        ret &= "Animation " & fgtag(uilook(uiText), anim->name) & ": " & *.description & !"\n"
        exit for
      end if
    end with
  next

  for idx as integer = 0 to 9999
    with builtin_anim_variants(idx)
      if .name = NULL then exit for
      if *.name = anim->variant orelse (idx = 0 and anim->variant = "") then
        ret &= "Variant " & fgtag(uilook(uiText), *.name) & ": " & *.description & !"\n"
        exit for
      end if
    end with
  next

  return ret
end function

' The toplevel animation editor, which shows the animations for a SpriteSt and
' lets you create/edit them.
' This ought to be much better integrated into the spriteset browser
sub AnimationEditor.toplevel()
  set_animation_framerate gen(genMillisecPerFrame)

  'Temporarily move the slice
  '(This is the same as what DrawSliceAt with ignore_offset=YES does, except it does it every frame)
  DIM remem_pos as XYPair = sl->Pos
  sl->Pos = 0

  rebuild_toplevel_menu
  ' (There may be no animations)
  animstate->start_animation(cur_anim())

  do
    setwait gen(genMillisecPerFrame)
    setkeys

    if keyval(scShift) = 0 andalso usemenu(topstate, topmenu) then
      animstate->start_animation(cur_anim())
    end if

    if keyval(ccCancel) > 1 then exit do
    if keyval(scF1) > 1 then show_help "animation_editor"
    if keyval(scX) > 1 and cur_animname() <> "" then
      export_menu cur_anim(), cur_animname()
    elseif keyval(scInsert) > 1 or keyval(scPlus) > 1 or keyval(scNumpadPlus) > 1 then
      if cur_animset then new_animation()
    elseif keyval(scDelete) > 1 then
      delete_animation()
    elseif keyval(scR) > 1 then
      reset_slice()  'Actually effective
      'animstate->reset()  'Does nothing
    elseif enter_space_click(topstate) then
      select case topmenu[topstate.pt].dat
          case mdatExit
            exit do
          case mdatNewAnim
            new_animation()
          case mdatAnim
            edit_animation(*cur_anim(), cur_animname())
            animstate->reset()
            animstate->start_animation(cur_anim())
      end select
    elseif keyval(scShift) > 0 andalso cur_anim_idx() >= 0 then
      ' Reorder animations
      dim anidx as integer = cur_anim_idx()
      dim shift as integer = 0
      if keyval(ccUp) > 1 and anidx > 0 then
        shift = -1
      elseif keyval(ccDown) > 1 and anidx < v_len(cur_animset->animations) - 1 then
        shift = 1
      end if
      if shift then
        swap cur_animset->animations[anidx], cur_animset->animations[anidx + shift]
        topstate.pt += shift
        rebuild_toplevel_menu
        'animstate->start_animation(cur_anim())
      end if
    end if

    'animstate->animate()
    AdvanceSlice sl

    clearpage vpage

    'Draw command palette
    edgeprint "Del: Delete", pInfoRight, pMenuY + 0, uilook(uiMenuItem), vpage
    edgeprint "Shift-" & CHR(8) & ": Reorder", pInfoRight, pMenuY + 10, uilook(uiMenuItem), vpage
    edgeprint "R: Reset slice", pInfoRight, pMenuY + 20, uilook(uiMenuItem), vpage
    edgeprint "X: Export", pInfoRight, pMenuY + 30, uilook(uiMenuItem), vpage

    wrapprint animation_info(cur_anim()), pInfoX, pInfoY, uilook(uiMenuItem), vpage
    DrawSliceAt sl, pCentered, pBottom - 50, , , vpage
    standardmenu topmenu, topstate, , , vpage
    setvispage vpage
    dowait
  loop

  v_free topmenu

  sl->Pos = remem_pos

  set_animation_framerate 55  'Most of Custom runs at this framerate
end sub

' For exporting a certain animation as a .gif
sub AnimationEditor.export_menu(anim as Animation ptr, anim_name as string)
  dim choices(...) as string = { "Animated .gif", "Transparent animated .gif" }
  dim choice as integer = multichoice("Export `" & anim_name & "' animation how?", choices())
  if choice = -1 then exit sub
  dim filename as string = inputfilename("Filename?", ".gif", "", "", default_export_filename & " " & anim_name)
  if choice = 0 then
    export_gif sl, filename & ".gif", anim  'FIXME
  elseif choice = 1 then
    export_gif sl, filename & ".gif", anim, YES
  end if
  ' Restart, since export_gif stops it
  sl->AnimState->start_animation(cur_anim())
end sub

sub AnimationEditor.delete_animation()
  if cur_anim() = 0 then exit sub
  if yesno("Really delete animation `" & cur_animname() &"'? No undo!", NO) then
    animstate->stop_animation()
    cur_animset->delete_animation(cur_anim())
    rebuild_toplevel_menu  'Updates state.pt
  end if
end sub

sub AnimationEditor.new_animation()
  dim anim as string = prompt_animation_name("Animation to define?", anim_context)
  if anim = "" then exit sub

  dim variant as string = prompt_variant_name("Animation variant to define?", anim_context)
  if variant = "" then exit sub
  if variant = "(none)" then variant = ""
  dim animvariant as string = rtrim(anim & " " & variant)

  if cur_animset->get_animation(animvariant) then
    notification "An animation named `" & animvariant  & "' already exists"
    exit sub
  end if

  dim newanim as Animation ptr
  newanim = cur_animset->new_animation(anim, variant)

  ' Go to the new animation
  rebuild_toplevel_menu
  correct_menu_state topstate
  animstate->start_animation(newanim)
end sub


'==========================================================================================
'                                Animation op menu stack
'==========================================================================================


function AnimOpsMenuStack.cur_prop_info() as SlicePropInfo ptr
  dim item as MenuDefItem ptr = cur_item
  if item = NULL then return NULL
  if item->t = tProperty then
    return @prop_info_list[item->sub_t]
  end if
  return NULL
end function

' Update cursor/hover, open or close menus.
' Returns YES and sets ret_animop when a leaf menu item is selected;
' also sets ret_prop if it's a slice property.
function AnimOpsMenuStack.controls() as bool
  if not is_open then return NO

  if activate then
    select case cur_item->t
        case tAnimOp
          ret_animop = cur_item->sub_t
          if ret_animop = animOpSetProp orelse ret_animop = animOpTween then
            open_prop_section_list
          else
            while is_open
              close_menu
            wend
            return YES
          end if

        case tPropSection
          open_prop_section cur_item->caption

        case tProperty
          ret_prop = cur_item->dataptr
          while is_open
            close_menu
          wend
          return YES
    end select
  else
    if cur_prop_info then
      helpkey = cur_prop_info->helpkey
    else
      helpkey = cur_menu->helpkey
    end if
    base.controls()
  end if
end function

' Open the first menu, to pick an animation op
' itemrect is for the selected item in the exterior menu, telling where to position the menu
sub AnimOpsMenuStack.start(itemrect as RectType, title as string)
  ret_animop = animOpUnknown
  ret_prop = NULL
  open_menu_at(itemrect)
  menus(0).name = title
  menus(0).helpkey = "animation_op"
  for idx as integer = 0 to ubound(anim_op_fullnames)
    add_item(anim_op_fullnames(idx), tAnimOp, idx)
  next
  finish_open()
end sub

' Open menu with list of slice editor sections
sub AnimOpsMenuStack.open_prop_section_list()
  open_menu()
  menus(1).helpkey = "animation_op"
  for idx as integer = 0 to v_len(prop_info_list) - 1
    with prop_info_list[idx]
      if .infotype = SlicePropInfoType.section then
        add_item(.display, tPropSection)
      end if
    end with
  next
  finish_open()
end sub

' Open menu with contents of a slice editor section
sub AnimOpsMenuStack.open_prop_section(get_section as string)
  open_menu()
  dim cur_section as string
  for idx as integer = 0 to v_len(prop_info_list) - 1
    with prop_info_list[idx]
      if .infotype = SlicePropInfoType.section then
        cur_section = .display
      elseif .infotype = SlicePropInfoType.prop then
        if cur_section = get_section then
          ' Set sub_t = idx, dataptr = prop
          add_item(.display, tProperty, idx, @prop_info_list[idx])
        end if
      end if
    end with
  next
  finish_open()
end sub


'================================ Editing an Animation ====================================

const itemdatExit = -1
const itemdatAddStep = -2
const itemdatPlay = -3
const itemdatLoop = -4
const itemdatReset = -5
const itemdatOpAddArg = -6  'datptr is the NodePtr for the op
const itemdatOp = 1  'datptr is the NodePtr for the op
const itemdatOpArg = 2  'datptr is the NodePtr for the op arg

function format_ms(ms as integer) as string
  return ms_to_frames(ms) & " frame(s) (" & FORMAT(0.001 * ms, "0.000") & "sec)"
end function

' prop_info_list will change based on the slice properties that can be edited given the current slice state
' (e.g., width if not filling parent). That might be confusing...
sub AnimationEditor.update_prop_info_list()
  v_new prop_info_list
  var property_editor = SlicePropertiesEditor(sl)
  property_editor.gather_properties(prop_info_list)
end sub

' Lookup info about a slice property.
' TODO: The info might be missing, because prop_info_list depends on current slice data
function AnimationEditor.prop_info(propname as string) as SlicePropInfo ptr
  for idx as integer = 0 to v_len(prop_info_list) - 1
    ' In one case (Rect/Scroll "style") different slice types have different valid ranges, and
    ' more often have different names for the same prop. So match on sltype too.
    dim prop as SlicePropInfo ptr = @prop_info_list[idx]
    if *prop->propname = propname then
      if prop->sltype = slNone orelse prop->sltype = sl->SliceType then
        return prop
      end if
    end if
  next
  debuginfo "Couldn't find prop " & propname
  return NULL
end function

' Get the display string for a slice property name
function AnimationEditor.prop_display_name(propname as string) as string
  dim prop as SlicePropInfo ptr = prop_info(propname)
  if prop = NULL orelse debug_props then
    return propname
  end if
  return prop->display
end function

' Get the display string for a slice property value, e.g. "34", or "Left" for an alignment
' Invalidates previous prop_info_list pointers!
function AnimationEditor.prop_display_value(propname as string, value_node as NodePtr) as string
  dim prop as SlicePropInfo ptr = prop_info(propname)
  if prop = NULL orelse debug_props then
    return value_node..str
  end if

  if prop->custom_caption then
    ' Get the caption in an inefficient but very general way: by change the value
    ' of this property on the slice, regenerating the entire slice detail editor menu, extracting
    ' the caption from it, and then resetting the value!
    dim old_value_node as NodePtr = prop->value_node
    prop->value_node = NULL  'Don't delete it
    set_slice_property sl, propname, value_node
    update_prop_info_list
    prop = prop_info(propname)
    ' Reset the value
    set_slice_property sl, propname, old_value_node
    FreeNode old_value_node
    if prop then
      return prop->caption
    else
      ' Could happen
      debug propname & " disappeared..."
      ' prop is now invalid
      return value_node..str
    end if
  end if

  ' Adapted from finish_defitem
  select case prop->dtype
    case dtypeBool:          return iif(value_node..int, "YES", "NO")
    case dtypeInt, dtypeStr: return value_node..str
    case dtypeFloat:         return format_percent(value_node..float)
  end select

end function

' If curnode (an anim or anim argument node) is given, try to update state.pt to point to it
sub AnimationEditor.rebuild_animation_menu(byref menu as SimpleMenuItem vector, byref state as MenuState, byref anim as Animation, curnode as NodePtr = NULL)
  update_prop_info_list

  v_new menu
  append_simplemenu_item menu, "Previous Menu", , , itemdatExit
  append_simplemenu_item menu, "Ops:", YES, uilook(eduiHeading)  'unselectable

  dim op as NodePtr = FirstChild(anim.opsnode)
  while op
    dim caption as string
    select case op..int
      case animOpWait
        caption = format_ms(op."ms")
      case animOpWaitMS
        caption = FORMAT(0.001 * op."ms", "0.000") & "sec (" & ms_to_frames(op."ms") & " frame(s))"
      case animOpRepeat
        'pass
      case animOpFrame
        caption = op."frame".string
      case animOpPlayFrameGroup
        caption = (100 * op."framegroup") & ", " & format_ms(op."ms")
      case animOpSetProp
        '?"setop:  key=", op."key".str, "value=", op."value".str
        dim propname as string = op."key".str
        caption = prop_display_name(propname) & " = " & prop_display_value(propname, op."value".ptr)

      case animOpSwitchAnim
        caption = op."anim".str & " " & op."variant".str

      case animOpTween
        dim propname as string = op."key".str
        caption = prop_display_name(propname) & " between:"

      case else
        caption = "???" 'STR(.arg1)   '& " " & .arg2
    end select
    append_simplemenu_item menu, "- " & anim_op_names(op..int) & " " & caption, , , itemdatOp
    v_last(menu).datptr = op

    ' Add menu items for sub items
    select case op..int
      case animOpTween
        readnode op, ignoreall
          withnode op."keyframe" as keyframe
            caption = FORMAT(0.001 * keyframe..int, "0.000") & "s: "
            caption &= prop_display_value(op."key".str, keyframe."value".ptr)
            append_simplemenu_item menu, "  - " & caption, , , itemdatOpArg
            v_last(menu).datptr = keyframe
          end withnode
        end readnode
        append_simplemenu_item menu, "  - Add keyframe", , , itemdatOpAddArg
        v_last(menu).datptr = op

    end select

    op = NextSibling(op)
  wend

  append_simplemenu_item menu, "- Add step (+)", , uilook(eduiSpecial), itemdatAddStep

  append_simplemenu_item menu, "", YES, uilook(uiDisabledItem)  'unselectable
  'append_simplemenu_item menu, "Reset slice (R)", , , itemdatReset
  append_simplemenu_item menu, "Play looped (L)", , , itemdatLoop
  append_simplemenu_item menu, "Play (P)", , , itemdatPlay

  state.last = v_len(menu) - 1

  if curnode then
    for pt as integer = 0 to state.last
      if menu[pt].datptr = curnode then
        state.pt = pt
        ' There may be multiple menu items with the same pointer, due to itemdatOpAddArg
        exit for
      end if
    next
    correct_menu_state state
  end if
end sub

' Edit a number of milliseconds as number of frames
function intgrabber_ms_as_frames(ms_node as NodePtr, minframes as integer, maxframes as integer) as bool
  dim frames as integer = ms_to_frames(ms_node..int)
  if intgrabber(frames, minframes, maxframes, , , , , , , , NO) then  'wrap = NO
    SetContent(ms_node, frames_to_ms(frames))
    return YES
  end if
end function

' Returns true if edited
function AnimationEditor.edit_property(propname as string, value_node as NodePtr, state as MenuState) as bool
  dim prop as SlicePropInfo ptr = prop_info(propname)
  if prop = NULL then return NO

  dim edited as bool
  '?"Edit prop", propname, "dtype", prop->dtype, "val", value_node..int, "max", prop->range_max
  select case prop->dtype
    case pdtypeBool
      dim value as bool = value_node..bool
      'FIXME: enter clashes with replacing op
      edited = boolgrabber(value, state)
      SetContentBool(value_node, value)
    case pdtypeInt
      dim value as integer = value_node..int
      edited = intgrabber(value, prop->range_min, prop->range_max)
      SetContent(value_node, value)
    case pdtypeFloat
      dim valuefloat as double = value_node..float
      var sigfigs = 3
      var is_percent = YES
      edited = percent_grabber(valuefloat, "", prop->range_min_float, prop->range_max_float, sigfigs, YES, is_percent)
      SetContent(value_node, valuefloat)
    case pdtypeStr
      dim valuestr as string = value_node..str
      if keyval(scAnyEnter) > 1 then
        valuestr = multiline_string_editor(valuestr, "sliceedit_text_multiline", NO)
        edited = YES
      else
        edited or= strgrabber(valuestr)
      end if
      if edited then using_strgrabber = YES
      SetContent(value_node, valuestr)

  end select
  return edited
end function

' Controls for modifying curop when it is the selected menu item,
' Also modifies the slice state by applying this op as appropriate
sub AnimationEditor.edit_animop_head(byref anim as Animation, curop as NodePtr, state as MenuState)
  dim sprset as SpriteSet ptr
  if sl->SliceType = slSprite then sprset = spriteset_for_frame(sl->SpriteData->img.sprite)

  using_strgrabber = NO

  select case curop..int
    case animOpWait
      ' Arg is ms, but edit as number of frames
      state.need_update or= intgrabber_ms_as_frames(GetOrCreateChild(curop, "ms"), 0, 1000000)
      if keyval(scTab) > 1 then
        anim.mutate_op(curop, animOpWaitMS)
        state.need_update = YES
      end if
    case animOpWaitMS
      dim ms as integer = curop."ms"
      state.need_update or= intgrabber(ms, 0, 9999, , , , , , , 10, NO)  'moreless_step = 10, wrap = NO
      SetChildNode(curop, "ms", ms)
      if keyval(scTab) > 1 then
        anim.mutate_op(curop, animOpWait)
        state.need_update = YES
      end if
    case animOpFrame
      if sprset = 0 then exit select
      dim frame as integer = curop."frame"
      state.need_update or= intgrabber(frame, 0, sprset->num_frames - 1)
      SetChildNode(curop, "frame", frame)
      sl->SpriteData->set_frame(sl, frame)  'Update visual
    case animOpPlayFrameGroup
      if sprset = 0 then exit select
      dim framegroup as integer = curop."framegroup"
      if keyval(scShift) > 0 then
        state.need_update or= intgrabber_ms_as_frames(GetOrCreateChild(curop, "ms"), 0, 1000000)
      else
        state.need_update or= intgrabber(framegroup, 0, sprset->last_frame_group)
        SetChildNode(curop, "framegroup", framegroup)
      end if
      sl->SpriteData->set_frameid(sl, framegroup * 100)  'Update visual
    case animOpSetProp
      if edit_property(curop."key".str, curop."value".ptr, state) then
        state.need_update = YES
      end if
      set_slice_property(sl, curop."key".str, curop."value".ptr)  'Update visual
    case animOpSwitchAnim
      'No support yet. Need a tileanim-editor-style popup menu.
    case animOpTween
      'TODO: pick curve type

  end select
end sub

' Edit an itemdatOpArg menu item
' Also modifies the slice state by applying this op as appropriate (at this keyframe)
sub AnimationEditor.edit_animop_arg(byref anim as Animation, curop as NodePtr, curoparg as NodePtr, state as MenuState)
  ' TAB state. This is temp
  static whicharg as integer = 0

  select case curop..int
    case animOpTween
      if keyval(scTab) > 1 then whicharg xor= 1
      if whicharg = 0 andalso keyval(scCtrl) = 0 then
        if edit_property(curop."key".str, curoparg."value".ptr, state) then
          state.need_update = YES
        end if
      else
        dim ms as integer = curoparg..int
        state.need_update or= intgrabber(ms, 0, 9999, , , , , , , 10, NO)  'moreless_step = 10, wrap = NO
        SetContent(curoparg, ms)
      end if
      set_slice_property(sl, curop."key".str, curoparg."value".ptr)  'Update visual
  end select
end sub

' Add a keyframe at 0ms which sets the property to its current value
function AnimationEditor.add_default_keyframe(to_op as NodePtr, prop as SlicePropInfo ptr) as NodePtr
  if prop = NULL then return NULL  ' Shouldn't happen
  dim firstframe as NodePtr = AppendChildNode(to_op, "keyframe", 0)
  dim newnode as NodePtr = CloneNodeTree(prop->value_node, get_anim_doc)
  AddChild firstframe, newnode
  return newnode
end function

' Append a new animop with the return values from AnimOpsMenuStack
function AnimationEditor.create_animop_node(byref anim as Animation, animop as AnimOpType, prop as SlicePropInfo ptr = NULL) as NodePtr
  dim newop as NodePtr = anim.append(animop)
  if animop = animOpSetProp then
    SetChildNode newop, "key", *prop->propname
    AddChild newop, CloneNodeTree(prop->value_node, get_anim_doc)
  elseif animop = animOpTween then
    SetChildNode newop, "key", *prop->propname
    add_default_keyframe newop, prop
  elseif animop = animOpSwitchAnim then
    dim anim as string = prompt_animation_name("Animation to play?", anim_context)
    dim variant as string
    if anim <> "" then
      variant = prompt_variant_name("Variant to play?", anim_context)
    end if
    if anim = "" orelse variant = "" then  'Cancelled
      FreeNode newop
      return NULL
    end if
    SetChildNode newop, "anim", anim
    if variant <> "(none)" then
      SetChildNode newop, "variant", variant
    end if
  end if
  return newop
end function

sub AnimationEditor.open_animops_menu(title as string, itemrect as RectType)
  animops_menus.sl = sl
  update_prop_info_list
  animops_menus.prop_info_list = prop_info_list
  animops_menus.start(itemrect, title)
end sub

' Edit a single animation
sub AnimationEditor.edit_animation(byref anim as Animation, anim_name as string)

  dim state as MenuState
  dim menu as SimpleMenuItem vector
  rebuild_animation_menu menu, state, anim

  dim menuopts as MenuOptions
  menuopts.wide = 80  ' Minimum width
  menuopts.calc_size = YES
  dim mpos as XYPair = (pMenuX, pMenuY)

  ' Precompute the menu size
  calc_menu_rect state, menuopts, mpos, vpage, cast(BasicMenuItem vector, menu)

  dim animating as bool = NO  'Playing the animation, rather than editing it
  ' What we're going to do with the op returned from animops_menus
  dim animops_action as string

  dim as NodePtr curop, curoparg ', curnode
  dim itemdat as integer = 0

  do
    setwait gen(genMillisecPerFrame)
    setkeys

    if animating then
      ' Menu cursor shows the current op

      AdvanceSlice sl
      if animstate->anim = NULL then
        animating = NO
      end if

      if keyval(scR) > 1 then  'Shortcut
        animating = NO
        'animstate->reset()
        'animstate->stop_animation()
        reset_slice
        state.need_update = YES
      'elseif keyval(ccCancel) > 1 orelse keyval(scP) > 1
      elseif anykeypressed() then
        animating = NO
        animstate->stop_animation()  'Not necessary, since we don't call animate() anyway
      end if

    elseif animops_menus.is_open then

      with animops_menus
        if .controls() then
          ' Picked an anim op from the menu, what do we do with it?
          if animops_action = "add_op" then
            dim newop as NodePtr = create_animop_node(anim, .ret_animop, .ret_prop)
            if newop then
              if curop then
                AddSiblingBefore curop, newop
              end if
              curop = newop
              state.need_update = YES
            end if

          elseif animops_action = "replace_op" then
            assert(curop)
            ' Is the new op different from curop? (Putting this on one line hits a PyPEG bug!)
            dim different as bool
            different = .ret_animop <> curop..int
            different or= .ret_prop andalso *.ret_prop->propname <> curop."key".str
            different or= .ret_animop = animOpSwitchAnim
            if different then
              ' Maybe use anim.mutate_op instead?
              dim newop as NodePtr = create_animop_node(anim, .ret_animop, .ret_prop)
              if newop then
                AddSiblingAfter curop, newop
                FreeNode curop
                curop = newop
                state.need_update = YES
              end if
            end if

          else
            showbug "Bad action " & animops_action
          end if

        end if
      end with

    else

      if keyval(scShift) = 0 then  'Not reordering
        usemenu state, menu
        ' FIXME: if pgup/down/home/end or mouse is used the slice state will be inconsistent
        ' because we jump without applying the intermediate ops
      end if

      curop = NULL
      curoparg = NULL
      'curnode = NULL
      if state.need_update then
        'Pointers would be stale. But shouldn't happen
        itemdat = 0
      else
        itemdat = menu[state.pt].dat
        if itemdat = itemdatOp orelse itemdat = itemdatOpAddArg then
          curop = cast(NodePtr, menu[state.pt].datptr)
          assert(curop)
          'curnode = curop
        elseif itemdat = itemdatOpArg then
          curoparg = cast(NodePtr, menu[state.pt].datptr)
          assert(curoparg)
          curop = NodeParent(curoparg)
          'curnode = curoparg
        end if
      end if

      ' Editing op arguments (these may set using_strgrabber)
      if itemdat = itemdatOp /'curop'/ then
        edit_animop_head anim, curop, state
      elseif itemdat = itemdatOpArg then
        edit_animop_arg anim, curop, curoparg, state
      end if

      dim add_step as bool
      dim add_keyframe as bool
      dim delete_step as bool
      dim play_anim as bool
      dim loop_anim as bool
      dim reset_anim as bool

      if using_strgrabber = NO then
        add_step = keyval(scInsert) orelse keyval(scPlus) > 1 orelse keyval(scNumpadPlus) > 1
        delete_step = keyval(scDelete) > 1 orelse keyval(scNumpadMinus) > 1
        play_anim = keyval(scP) > 1
        loop_anim = keyval(scL) > 1
        reset_anim = keyval(scR) > 1
      end if

      if using_strgrabber then clearkey scSpace
      if enter_space_click(state) then
        select case itemdat
          case itemdatExit
            exit do
          case itemdatAddStep
            add_step = YES
          case itemdatPlay
            play_anim = YES
          case itemdatLoop
            loop_anim = YES
          case itemdatReset
            reset_anim = YES
          case itemdatOp
            ' Replace an op
            animops_action = "replace_op"
            open_animops_menu("Replace with operator?", standardmenu_item_rect(menu, state))
          case itemdatOpArg
            ' Nothing
          case itemdatOpAddArg
            add_keyframe = YES
        end select
      end if

      if keyval(ccCancel) > 1 then exit do
      if keyval(scF1) > 1 then show_help "animation_edit"
      if keyval(scF6) > 1 then debug_props xor= YES

      if add_step then
        ' Add op
        animops_action = "add_op"
        open_animops_menu("Add which operator?", standardmenu_item_rect(menu, state))
      elseif delete_step then
        ' Delete op or keyframe
        if itemdat = itemdatOp then
          dim nextop as NodePtr = NextSibling(curop)
          FreeNode curop
          curop = nextop
          state.need_update = YES
        elseif itemdat = itemdatOpArg then
          dim nextarg as NodePtr = NextSibling(curoparg)
          FreeNode curoparg
          curoparg = nextarg
          state.need_update = YES
        end if
      elseif add_keyframe then
        ' Duplicate the last keyframe, if it has one, else add a keyframe at 0ms
        dim lastarg as NodePtr = LastChild(curop, "keyframe")
        if lastarg then
          curoparg = CloneNodeTree(lastarg)
          AddChild curop, curoparg
        else
          curoparg = add_default_keyframe(curop, prop_info(curop."key".str))
        end if
        state.need_update = YES
      elseif play_anim then
        ' Play (may or may not loop)
        animstate->start_animation(@anim)
        animating = YES
      elseif loop_anim then
        ' Loop
        animstate->start_animation(@anim, -1)
        animating = YES
      elseif reset_anim then
        ' Reset slice
        'animstate->reset()  'Does nothing
        reset_slice  'Reclones the slice
        state.need_update = YES
      elseif keyval(scShift) > 0 andalso itemdat = itemdatOp then
        ' Rearranging ops
        if keyval(ccUp) > 1 andalso PrevSibling(curop) then
          SwapSiblingNodes curop, PrevSibling(curop)
          state.need_update = YES
        elseif keyval(ccDown) > 1 andalso NextSibling(curop) then
          SwapSiblingNodes curop, NextSibling(curop)
          state.need_update = YES
        end if
      end if

    end if

    if state.need_update then
      state.need_update = NO
      dim curnode as NodePtr = iif(curoparg, curoparg, curop)
      rebuild_animation_menu menu, state, anim, curnode
    end if

    ' Draw screen
    clearpage vpage
    draw_background vpages(vpage), bgChequer
    DrawSliceAt sl, pCentered, pBottom - 50, , , vpage

    fuzzyrect vpages(vpage), state.rect, uilook(uiBackground)
    edgeprint anim_name, pCentered, mpos.y, uilook(uiText), vpage
    standardmenu cast(BasicMenuItem vector, menu), state, mpos.x, mpos.y + 12, vpage, menuopts

    ' Draw tooltip
    dim message as string
    if animating then
      message = "P/ESC to Stop"
    else
      message = "" '"(P)lay  (L)oop"
      if itemdat = itemdatOp then
        ' If the op has multiple editable fields
        if curop..int = animOpWait orelse curop..int = animOpWaitMS then message += "  Tab to select arg"
      elseif itemdat = itemdatOpArg then
        if curop..int = animOpTween then message += "  Tab to select arg"
      end if
    end if
    edgeprint message, pInfoX, pInfoY, uilook(uiText), vpage

    ' Draw command palette
    dim cmds() as string
    a_append cmds(), "Ins/+: Insert step"
    a_append cmds(), "Del: Delete step"
    a_append cmds(), "Shift-" & CHR(8) & ": Reorder"
    'a_append cmds(), "</>: Change op"
    a_append cmds(), CHR(27) & "/" & CHR(26) & ": Edit arg1"
    a_append cmds(), "Shift-" & CHR(27) & CHR(26) & ": Edit arg2"
    a_append cmds(), "Enter: Replace step"
    a_append cmds(), "R: Reset slice"

    dim cmdstate as MenuState
    cmdstate.active = NO
    init_menu_state cmdstate, cmds()
    dim menuopts as MenuOptions
    menuopts.edged = YES
    standardmenu cmds(), cmdstate, rRight - 20 * 8 , pMenuY + 12, vpage, menuopts

    ' Draw popup menus
    animops_menus.draw(vpage)

    setvispage vpage
    dowait
  loop
  v_free menu
end sub
