import ./deps/jquery.js
import type {AppletObject} from ./deps/geotypes/api.ts
import {AppletDescription, AdapParams, params, contains3d} from ./adapptypes.ts
colorsea from ./deps/colorsea.js

joyceApplets: AppletDescription[] := []
$('applet[code="Geometry"]').before (i, html) ->
   id := `joyceApplet${i}`
   joyceApplets.push { html, params(this.children), id,
      width: parseInt(this.getAttribute('width') ?? '200'),
      height: parseInt(this.getAttribute('height') ?? '200') }
   `<div id="${id}"></div>`

type Split<S extends string>
   S extends `${infer W} ${infer R}` ? (W | Split<R>) : S

classes := 'point line circle polygon sector plane sphere polyhedron'
type JoyceClass = Split<typeof classes>
function assertJoyceClass(s: string): asserts s is JoyceClass
   unless classes.includes s then throw new Error `Oops ${s} slipped through`

type JoyceName = string  // we use this to indicate where the names
   // from the Joyce commands (which are used as captions in the GeoGebra
   // applet) go.
type GeoName = string  // and this to indicate where GeoGebra identifiers go
type AnyName = GeoName | JoyceName  // and this for slots that can be either

type Description
   otherName: AnyName
   usesCaptions: JoyceName[]
   klass: JoyceClass
   ends?: [GeoName, GeoName]

// We put both JoyceNames and GeoNames in here, pointing to each other
// with the otherName property:
type JoyceElements = Record<AnyName, Description>

adapptScript := findAdappt() as HTMLScriptElement

function findAdappt()
  scripts := document.querySelectorAll 'script'
  for scrip of scripts
     src := scrip.getAttribute 'src'
     if src and src.includes 'adapptlet'
        return scrip

adapParams: AdapParams :=
   typeof GGBApplet is 'undefined'
      ? {loader: 'https://www.geogebra.org/apps/deployggb.js', joyceApplets: []}
      : JSON.parse(adapptScript.dataset.params ?? '') as AdapParams

// For accessing/setting the y coordinate of a set of coordinates:
Y := 1
Z := 2
function vertFlipped(coords: number[], cdata: ConstructionData): XYZ
  coords = coords.slice()
  if cdata.is3d
     if coords[Z] then coords[Z] = -coords[Z]
  else coords[Y] = cdata.height - coords[Y]
  return coords as XYZ

type ConstructionData
  id: string
  bg: RGB
  is3d: boolean
  width: number
  height: number
  elements: JoyceElements
  pivot: string

// Global data setup for pivoting (ugh, but necessary because the api
// is not passed to the callback, so we have to look up the slider in
// a global list):

type PivotData
  api: AppletObject
  pivot: string
  lastAngle: number
  rotatable: string[]
  maybeRotatable: Record<string, string>
  fixed: Record<string, boolean>

pivotData: Record<string, PivotData> := {}

function postApplets(jApplets: AppletDescription[], codebase = '')
   for each jApp of jApplets
      params := {
         appName: 'classic',
         -enableRightClick,
         // +showMenuBar,
         jApp.width,
         jApp.height,
         appletOnLoad: (api: AppletObject) =>
            is3d := contains3d jApp.params
            if is3d
               api.enable3D true
               api.setPerspective 'T'
               // Get rid of the xy-plane indicator
               xml .= api.getXML()
               xml = xml.replace /plate.show="\w+"/, 'plate show="false"'
               api.setXML xml
            else if codebase.includes 'web3d'
               api.setPerspective 'G'
            if adapParams.config?.algebra
               api.setPerspective '+A'
            elements := {}
            pivot .= ''
            if 'pivot' in jApp.params
               if is3d
                  console.warn('Geometry Applet "pivot" only supported for '
                     + '2D constrcutions. Ignoring.')
               else
                  pivot = geoname(jApp.id, elements, 'point') + 'Pivot'
                  pivotData[pivot] = {
                     api, jApp.params.pivot,
                     lastAngle: 0, rotatable: [], maybeRotatable: {}, fixed: {}}
            cdata: ConstructionData := {
               bg: ([255, 255, 255] as RGB),
               is3d, jApp.id, jApp.width, jApp.height, elements, pivot}
            for name, value in jApp.params
               dispatchJcommand
                  api, name, value, cdata
            if pivot
               pd := pivotData[pivot]
               for entity, circ in pd.maybeRotatable
                  if cdata.elements[circ].ends?[0] is pd.pivot
                     pd.rotatable.push entity
            if is3d
               depth .= jApp.width
               if jApp.height > depth then depth = jApp.height
               api.setCoordSystem
                  -10, 10 + jApp.width,
                  -10, 10 + jApp.height,
                  -depth - 10, depth + 10,
                  false
               api.setAxesVisible 3, false, false, false
               api.setGridVisible 3, false
            else
               api.setCoordSystem -10, 10 + jApp.width, -10, 10 + jApp.height
               api.setAxesVisible false, false
               api.setGridVisible false
         } as const
      geoApp := new GGBApplet params
      if codebase then geoApp.setHTML5Codebase codebase
      geoApp.inject jApp.id

// Always use the final joyceApplets if there are any:
if joyceApplets.length
   adapParams.joyceApplets = joyceApplets

if adapParams.joyceApplets.length
   if adapParams.loader
      jQuery.getScript adapParams.loader, =>
         postApplets adapParams.joyceApplets
   else
      postApplets adapParams.joyceApplets, adapParams.codebase

/* That's all of the actions of this script. All of the remainder is
 * implementation.
 */

type DimParts = [string[], string[], string[]] // Gives GeoNames
      // or expressions for 0-, 1-, and 2-dimensional parts for coloring

// need to pass the parts into the callbacks because sometimes the parts
// are not generated until callback time
type GeogebraCallback = (api: AppletObject, parts: DimParts) => void
type Commander
   commands: string[]
   callbacks: GeogebraCallback[]
   parts: DimParts
   auxiliaries: GeoName[]  // extra entities needed in GeoGebra
   ends?: [GeoName, GeoName]

function freshCommander(): Commander
  commands: []
  callbacks: []
  parts: [[], [], []]
  auxiliaries: []

type JoyceArguments =
   Partial<Record<JoyceClass|'subpoints', GeoName[]> & {scalar: number[]}>
type ClassHandler = (
   name: GeoName,
   method: string,
   args: JoyceArguments,
   index: number,
   cdata: ConstructionData) => Commander
type RGB = [number, number, number]
type XYZ = RGB

// Executes the command corresponding to param against the GeoGebra applet
// api, consulting and extending by side effect the elements that are
// present in that applet
function dispatchJcommand(
      api: AppletObject,
      name: string,
      value: string,
      cdata: ConstructionData): void
   switch name
      'background'
         cdata.bg = joyce2rgb value, cdata.bg
         if adapParams.config?.commands
            console.log 'Setting background to', value, 'interpreted as',
               cdata.bg
         api.setGraphicsOptions 1, bgColor: colorsea(cdata.bg).hex()
      'title'
         if adapParams.config?.commands
            console.log 'Setting title to', value
         corner := cdata.is3d ? 'Corner(-1,1)' : 'Corner(1,1)'
         api.evalCommand `TitlePoint = ${corner}
            Text("${value}", TitlePoint + (2,5))`
      'pivot'
         return  // already handled in postApplets
      'align'
         console.warn
            'Label alignment is not available in GeoGebra'
            'translation, as there is no facility for automatically'
            'positioning labels. However, they can be dragged manually.'
         return
      /e\[\d+\]/
         num := parseInt(name.slice(2))
         {commands, callbacks, parts} :=
            jToG value, num, cdata
         if commands.length
            lastTried .= 0
            if commands.filter((&)).every (cmd) =>
                  if adapParams.config?.commands
                     console.log 'Translated to:', cmd
                  api.evalCommand(cmd) and ++lastTried
               callbacks.forEach &(api, parts)
            else console.warn
               `Geogebra command '${commands[lastTried]}'
                (part of translation of '${value}')
                failed.`
         else console.warn `Could not parse command '${value}'`
      else console.warn `Unknown param ${name} = ${value}`

// Parses a Joyce element-creating command, extending the elements
// by side effect:
function jToG(
      jCom: string,
      index: number,
      cdata: ConstructionData): Commander
   [jname, klass, method, data, ...colors] := jCom.split ';'
   if adapParams.config?.commands
      console.log 'Defining', jname, 'as a', klass, 'constructed by',
         method, 'from', data, 'colored as', colors
   cmdr .= freshCommander()
   unless klass in classHandler
      console.warn `Unknown entity class ${klass}`
      return cmdr
   assertJoyceClass klass  // shouldn't need to do that :-/
   isPivot := !!cdata.pivot and jname is pivotData[cdata.pivot].pivot
   name := geoname jname, cdata.elements, klass
   if isPivot then pivotData[cdata.pivot].pivot = name
   args: JoyceArguments := {}
   usesCaptions := []
   for each jdep of data.split ','
      scalar := parseFloat jdep
      if scalar is scalar // not NaN
         (args.scalar ?= []).push scalar
         continue
      unless jdep in cdata.elements
         console.warn `Reference to unknown geometric entity ${jdep} in $jCom}`
         return cmdr
      usesCaptions.push jdep
      {klass: depKlass, otherName: depGeo, ends} := cdata.elements[jdep]
      (args[depKlass] ?= []).push depGeo
      if depKlass is 'point'
         (args.subpoints ?= []).push depGeo
      else if depKlass is 'line'
         (args.subpoints ?= []).push ...ends ?? []
   cmdr = classHandler[klass] name, method, args, index, cdata
   unless name is jname then cmdr.callbacks.push (api: AppletObject) =>
      api.setCaption name, jname
      api.setLabelStyle name, 3  // style CAPTION = 3
   if cmdr.auxiliaries.length and not adapParams.config?.showaux
      cmdr.callbacks.push (api: AppletObject) =>
         for each aux of cmdr.auxiliaries
            api.setAuxiliary aux, true
            api.setLabelVisible aux, false
            api.setVisible aux,false

   // set up the pivot if there is one
   if isPivot
      unless klass is 'point'
         console.warn(`Can only pivot around a point, not the ${klass}`
            + `named ${jname}. Ignoring.`)
         cdata.pivot = ''
      cmdr.commands.push(`${cdata.pivot} = `
         + `Slider(0°,360°,1°,1,${cdata.width/3},true,true,false,false)`)
      cmdr.callbacks.push (api: AppletObject) =>
         api.setCaption cdata.pivot, 'Rotate Display'
         api.setLabelStyle cdata.pivot, 3
         api.setCoords(cdata.pivot, 2*cdata.width/3, cdata.height-10)
         // Not sure how to let TypeScript deal with putting a new function
         // on the global window object, so punting at least for now:
         // @ts-ignore
         window.pivotListener = pivotListener
         api.registerObjectUpdateListener(cdata.pivot, 'pivotListener')

   // Create callback to assign colors
   traceC := adapParams.config?.color
   console.log 'Considering coloring', name, 'with', colors if traceC
   if colors.length is 4 and colors.every (color) => invisible color
      cmdr.callbacks.push (api: AppletObject) => api.setVisible name, false
   else // we have to decorate
      dimension .= cmdr.parts.findLastIndex .includes name
      cmdr.callbacks.push (api: AppletObject, parts: DimParts) =>
         // Operate in order faces, lines, point, caption so that
         // we can adjust components after setting overall color, etc.

         // Color the "Faces"; they default to 'brighter':
         if invisible colors[3]
             for each face of parts[2]
                if face is name
                   console.log 'Fading out interior of', face if traceC
                   // hide the interior by making it transparent
                   api.setFilling face, 0
                else if face not in cdata.elements
                   console.log 'Hiding face', face if traceC
                   api.setVisible face, false
         else
            faceRGB := joyce2rgb(colors[3] or 'brighter', cdata.bg)
            deep := ['circle', 'polygon', 'sector']
            filling := deep.includes(klass) ? 0.7 : 0.2
            for each face of parts[2]
               if traceC
                  console.log 'Coloring face', face, 'to',
                     colors[3], '=', faceRGB
               api.setVisible face, true
               api.setFilling face, filling
               api.setColor face, ...faceRGB

         // Lines default to black:
         if invisible colors[2]
            for each line of parts[1]
                if line is name or line not in cdata.elements
                   console.log 'Hiding line', line if traceC
                   api.setVisible line, false
         else
            lineRGB := joyce2rgb(colors[2] or 'black', cdata.bg)
            if traceC
               console.log 'Need to color lines', parts[1], 'with', lineRGB
            for each line of parts[1]
               console.log 'Coloring line', line, 'to', colors[2] if traceC
               api.setVisible line, true
               api.setColor line, ...lineRGB

         // Now color the points:
         if traceC
            console.log
               'Considering point colors for', name, 'of dimension', dimension
         if invisible colors[1]
            // Hide all the dim-0 elements that are not distinct independent
            // items:
            for each point of parts[0]
               if point is name or point not in cdata.elements
                  console.log 'Hiding point', point if traceC
                  api.setVisible point, false
         else if dimension is 0 or colors[1] // Need to color the points
            if not colors[1]
               colors[1] = pointDefaultColorName name, method, isPivot
            ptRGB := joyce2rgb colors[1], cdata.bg
            for each point of parts[0]
               console.log 'Coloring point', point, 'to', colors[1] if traceC
               api.setVisible point, true
               api.setColor point, ...ptRGB

         // Make the caption the correct color
         if invisible colors[0]
            console.log 'Hiding label', name if traceC
            api.setLabelVisible name, false
         else if colors[0]
            if colors[dimension+1] and colors[dimension+1] is not colors[0]
               // Have to make a text to provide the caption, since GeoGebra
               // doesn't allow caption different color from entity.
               textName := 'GeoText' + index
               locationExpr := switch klass
                  when 'point' then `1.02${name}`
                  when 'line'
                     (`Midpoint(${name})`
                      + ` + Rotate(Direction(${name})`
                      +    `*Length(${name})*0.025/Length(Direction(${name})),`
                      +    `pi/2)`)
                  when 'circle'
                     (`Center(${name})`
                      + ` + Radius(${name})*Vector((12/13,5/13))*1.03`)
                  when 'polygon' then `Centroid(${name})`
                  when 'sector'
                     (`(5*Center(${name})`
                      + ` - ${cmdr.ends?[0]} - ${cmdr.ends?[1]})/3`)
                  when 'plane'
                     `Intersect(${name}, PerpendicularLine((0, 0, 0), ${name}))`
                  when 'sphere'
                     (`Center(${name})`
                      + ` + Radius(${name})*Vector((12/13,0,5/13))*1.03`)
                  when 'polyhedron'
                     // The "ends" are faces or vertices roughly opposite
                     // from each other
                     [ex1, ex2] .= cmdr.ends ?? ['', '']
                     unless parts[0].includes ex1 then ex1 = `Centroid(${ex1})`
                     unless parts[0].includes ex2 then ex2 = `Centroid(${ex2})`
                     `(4*${ex1}+${ex2})/5`
               textCmd := `${textName} = Text("${jname}", ${locationExpr})`
               textCol := joyce2rgb colors[0], cdata.bg
               if traceC
                  console.log `Making text '${textCmd}' colored`, textCol
               api.evalCommand textCmd
               api.setColor textName, ...textCol
               // and hide the underlying GeoGebra label
               api.setLabelVisible name, false
            else // specified label color matches the entity color
               // So label gets the correct color from element
               // but we had better make sure it is visible:
               console.log 'Showing label', name if traceC
               api.setLabelVisible name, true
         else // label color is defaulting
            // Make it same as the element for points, invisible otherwise:
            show := klass is 'point'
            console.log 'Setting label vis of', name, 'to', show if traceC
            api.setLabelVisible name, show

   cdata.elements[jname] = {otherName: name, usesCaptions, klass, cmdr.ends}
   cdata.elements[name] = {otherName: jname, usesCaptions, klass, cmdr.ends}
   cmdr

function pivotListener(slider: string)
   pd := pivotData[slider]
   newval := pd.api.getValue slider
   if newval is pd.lastAngle then return
   rotation := newval - pd.lastAngle
   pd.lastAngle = newval
   pX := pd.api.getXcoord pd.pivot
   pY := pd.api.getYcoord pd.pivot
   relX: Record<string, number> := {}
   relY: Record<string, number> := {}
   for each anchor of pd.rotatable
      relX[anchor] = pd.api.getXcoord(anchor) - pX
      relY[anchor] = pd.api.getYcoord(anchor) - pY
      pd.api.setFixed anchor, false
   ct := Math.cos rotation
   st := Math.sin rotation
   for each anchor of pd.rotatable
      rX := relX[anchor]*ct - relY[anchor]*st
      rY := relY[anchor]*ct + relX[anchor]*st
      pd.api.setCoords(anchor, rX + pX, rY + pY)
      if pd.fixed[anchor]
         pd.api.setFixed anchor, true

function invisible(cname: string): boolean
   if adapParams.config?.showall then return false
   cname is '0' or cname is 'none'

function joyce2rgb(cname: string, backgroundRGB?: RGB): RGB
   whiteRGB: RGB := [255, 255, 255]
   bg: RGB := backgroundRGB or whiteRGB
   switch cname
      // RGB values from Duck Duck Go search on "[COLOR] rgb"
      /black/i
         [0,0,0]
      /blue/i
         [0,0,255]
      /cyan/i
         [0,255,255]
      /darkgray/i
         [128,128,128]
      /^gray/i
         [169,169,169]
      /green/i
         [0,255,0]
      /lightgray/i
         [211,211,211]
      /magenta/i
         [255,0,255]
      /orange/i
         [255,165,0]
      /pink/i
         [255,192,203]
      /red/i
         [255,0,0]
      /white/i
         [255,255,255]
      /yellow/i
         [255,255,0]
      /random/i
         colorsea.random().lighten(40).rgb()
      /background/i
         bg
      /brighter/i
         colorsea(bg).lighten(30).rgb()
      /darker/i
         colorsea(bg).darken(20).rgb()
      /^[0-9A-F]{6}$/i
         colorsea(`#${cname}`).rgb()
      /^\d+,\d+,\d+$/
         // HSB specification
         [H,S,B] := cname.split(',').map (s) => parseInt s
         colorsea.hsv(H, S, B).rgb()
      else
         console.warn 'Could not parse color:', cname
         [128, 128, 128]

function pointDefaultColorName(
            name: string, method: string, isPivot: boolean): string
   if isPivot then return 'green'
   switch method
      'free'
         'red'
      /.*[Ss]lider$/
         'orange'
      else 'black'

function geoname(
      jname: JoyceName, elements: JoyceElements, klass: JoyceClass): GeoName
   unless jname.substring(0,3) is 'Geo' // those might clash
      // Names with word characters starting with a capital are always good:
      if /^[A-Z]['\w]*$/.test jname then return jname
      // If it's not a point, can start with any letter:
      if klass !== 'point' and /^\p{L}['\w]*$/u.test jname then return jname
   // GeoGebra won't deal with this name, so hash it:
   numCode .= 0n
   numCode = numCode*128n + BigInt ch.codePointAt(0) ?? 1 for each ch of jname
   return .= 'Geo' + numCode.toString(36);
   return += '1' while return.value in elements

// Helpers for some corresponding point/line functions:
function cutoffExtend(
      method: string, pt: string[], point?: string[], line?: string[]
      ): [string, string]
   direction .= `UnitVector(Vector(${pt[0]},${pt[1]}))`
   if line and (not point or point[0] !== pt[0])
      direction = `UnitVector(${line[0]})`
   displacement := `Distance(${pt[2]}, ${pt[3]})*${direction}`
   source := method is 'cutoff' ? pt[0] : pt[1]
   [source, displacement]

function proportionSimilar(
      method: string, args: JoyceArguments, cdata: ConstructionData,
      aux: string, commands: string[], auxiliaries: string[]
      ): readonly [string, string]
   bad := ['', ''] as const
   pt .= args.subpoints
   unless pt then return bad
   // reduce the similar case to general proportion
   if method is 'similar'
      unless pt.length is 5 then return bad
      sourcePlane .= ''
      destPlane .= ''
      if cdata.is3d
         unless args.plane then return bad
         destPlane = `, ${args.plane[0]}`
         if args.plane.length > 1
            sourcePlane = `, ${args.plane[1]}`
         else
            sourcePlane = `, Plane(${pt[2]}, ${pt[3]}, ${pt[4]})`
      angle := `Angle(${pt[3]}, ${pt[2]}, ${pt[4]}${sourcePlane})`
      commands.push
         `${aux} = Rotate(${pt[1]}, ${angle}, ${pt[0]}${destPlane})`
      auxiliaries.push aux
      pt = [pt[2], pt[3], pt[2], pt[4], pt[0], pt[1], pt[0], aux]
   len := `Distance(${pt[2]},${pt[3]})*Distance(${pt[4]},${pt[5]})`
      + `/ Distance(${pt[0]},${pt[1]})`
   direction := `UnitVector(Vector(${pt[6]}, ${pt[7]}))`
   return [pt[6], `${len}*${direction}`]

// All of the detailed semantics of each available command lies in this
// function.
classHandler: Record<JoyceClass, ClassHandler> :=
   point: (name, method, args, index, cdata): Commander =>
      return := freshCommander()
      {commands, callbacks, parts, auxiliaries} := return.value
      zeroVector := cdata.is3d ? 'Vector((0,0,0))' : 'Vector((0,0))'
      aux := name + 'aUx'
      pivotable := cdata.pivot and name !== pivotData[cdata.pivot].pivot
      parts[0].push name
      switch method
         /angle(?:Bisector|Divider)/
            {center, foot} :=
               makeAngDiv(method, args, cdata, aux, auxiliaries, commands)
            unless foot return
            commands.push `${name} = ${foot}`
         'center'
            entity := args.circle ? args.circle[0] : args.sphere?[0]
            if entity
               commands.push `${name} = Center(${entity})`
               entityEnds := cdata.elements[entity].ends
               if entityEnds and not entityEnds[0]
                  entityEnds[0] = name // labeled the center
            else
               console.warn 'Nothing to produce center point of in',
                  name, method, args
         'circleSlider'
            unless args.circle then return
            circ := args.circle[0]
            commands.push `${name} = Point(${circ})`
            if pivotable
               maybeCenter := cdata.elements[circ].ends?[0]
               if maybeCenter is pivotData[cdata.pivot].pivot
                  pivotData[cdata.pivot].rotatable.push name
               if not maybeCenter
                  pivotData[cdata.pivot].maybeRotatable[name] = circ
            if args.scalar and args.scalar.length
               callbacks.push (api: AppletObject) =>
                  api.setCoords name, ...vertFlipped(args.scalar or [], cdata)
         /cutoff|extend/
            pt := args.subpoints
            unless pt and pt.length is 4 then return
            [source, displacement] :=
               cutoffExtend method, pt, args.point, args.line
            commands.push `${name} = Translate(${source}, ${displacement})`
         'first'
            unless args.subpoints then return
            // HACK: Special-case correction for Joyce Elements Bk II, prop 14
            index .= 0
            if name === 'H' and args.line and args.line[0] === 'HH2'
               index = 1
            commands.push `${name} = ${args.subpoints[index]}`
         /fixed|free/
            unless args.scalar then return
            coords := vertFlipped(args.scalar, cdata)
            scoord := coords.join ','
            if pivotable then pivotData[cdata.pivot].rotatable.push name
            commands.push `${name} = (${scoord})`
            if method is 'fixed'
               if pivotable then pivotData[cdata.pivot].fixed[name] = true
               callbacks.push (api: AppletObject) => api.setFixed name, true
         'foot'
            pt := args.subpoints
            unless pt then return
            destination := args.plane
               ? args.plane[0]
               : `Line(${pt[1]},${pt[2]})`
            commands.push
               `${name} = ClosestPoint(${destination}, ${pt[0]})`
         'intersection'
            // Checking Joyce source, means intersection of lines, not
            // intersection of line segments
            pt := args.subpoints
            unless pt then return
            l1 := `Line(${pt[0]},${pt[1]})`
            e2 := args.plane ? args.plane[0] : `Line(${pt[2]},${pt[3]})`
            commands.push `${name} = Intersect(${l1},${e2})`
         'last'
            unless args.subpoints then return
            commands.push
               `${name} = ${args.subpoints.at(-1)}`
         'lineSegmentSlider'
            segment .= args.line?[0]
            unless segment
               unless args.point then return
               commands.push `${aux} = Segment(${args.point.join ','})`
               auxiliaries.push aux
               segment = aux
            commands.push `${name} = Point(${segment})`
            if args.scalar and args.scalar.length
               callbacks.push (api: AppletObject) =>
                  api.setCoords name, ...vertFlipped(args.scalar or [], cdata)
         'lineSlider'
            pt := args.subpoints
            unless pt and pt.length is 2 then return
            commands.push `${name} = Point(Line(${pt[0]}, ${pt[1]}))`
            if args.scalar and args.scalar.length
               callbacks.push (api: AppletObject) =>
                  api.setCoords name, ...vertFlipped(args.scalar or [], cdata)
         'meanProportional'
            pt := args.subpoints
            unless pt and pt.length is 6 then return
            direction := `UnitVector(Vector(${pt[4]},${pt[5]}))`
            lensq := `Distance(${pt[0]},${pt[1]})*Distance(${pt[2]},${pt[3]})`
            disp := `sqrt(${lensq})*${direction}`
            commands.push `${name} = Translate(${pt[4]},${disp})`
         'midpoint'
            if args.line
               commands.push `${name} = Midpoint(${args.line[0]})`
            else
               commands.push
                  `${name} = Midpoint(${args.point?[0]},${args.point?[1]})`
         'parallelogram'
            pt := args.subpoints
            unless pt then return
            commands.push `${name} = ${pt[0]} + ${pt[2]} - ${pt[1]}`
         'perpendicular'
            // Note only the two-point option implemented so far
            unless args.subpoints return
            [center, direction] := args.subpoints
            commands.push `${name} = Rotate(${direction}, pi/2, ${center})`
         /proportion|similar/
            [source, displacement] :=
               proportionSimilar method, args, cdata, aux, commands, auxiliaries
            unless source then return
            commands.push `${name} = Translate(${source}, ${displacement})`
         'vertex'
            commands.push
               `${name} = Vertex(${args.polygon?[0]},${args.scalar?[0]})`
         else console.warn 'Unknown point method:', method

   line: (name, method, args, index, cdata) =>
      return := freshCommander()
      return.value.ends = ['', '']
      {commands, callbacks, parts, auxiliaries, ends} := return.value
      aux := name + 'aUx'
      parts[1].push name
      madeSegment .= false
      switch method
         /angle(?:Bisector|Divider)/
            {center, foot} :=
               makeAngDiv(method, args, cdata, aux, auxiliaries, commands)
            unless foot return
            auxiliaries.push aux + 'F'
            commands.push
               `${aux}F = ${foot}`
            ends[0] = center
            ends[1] = aux+'F'
         'bichord'
            // To match Joyce, we need to get the ordering here correct.
            // we want the order so that start -> end sweeping past the
            // center of the other circle is clockwise in the first
            // circle
            cr := args.circle
            unless cr return
            commands.push ...[1..2].map (n) =>
              `${aux}${n} = Intersect(${cr[0]}, ${cr[1]}, ${n})`
            inPlane := cdata.is3d ? `, Plane(${cr[0]})` : ''
            ctr := cr.map (c) => `Center(${c})`
            condition := (`Angle(${aux}1,${ctr[0]},${ctr[1]}${inPlane})`
               + `< Angle(${aux}2,${ctr[0]},${ctr[1]}${inPlane})`)
            commands.push
              `${aux}3 = If(${condition}, ${aux}1, ${aux}2)`
              `${aux}4 = If(${condition}, ${aux}2, ${aux}1)`
            ends[0] = aux + 3
            ends[1] = aux + 4
            auxiliaries.push ...[1..4].map (n) => aux + n
         'chord'
            // To match Joyce, we need to get the ordering here correct.
            // The complicated condition about distances is modeled after
            // Joyce's code, but it boils down to: take the endpoint of the
            // chord closest to the first subpoint, unless that first
            // subpoint is essentially at the midpoint of the chord, in
            // which case start with the endpoint nearest the second subpoint.
            unless args.subpoints and args.circle then return
            // We intersect with the whole line, not just the segment:
            line := `Line(${args.subpoints.join ','})`
            pA := args.subpoints[0]
            pB := args.subpoints[1]
            commands.push ...[1..2].map (n) =>
              `${aux}${n} = Intersect(${args.circle}, ${line}, ${n})`
            s := `Distance(${aux}1,${aux}2)`
            d := `Distance(${aux}1,${pA}) - Distance(${aux}2,${pA})`
            condition := (`If(${s}/10^9 < abs(${d}), ${d} > 0,`
               + `Distance(${aux}2,${pB}) < Distance(${aux}1,${pB}))`)
            commands.push
              `${aux}3 = If(${condition}, ${aux}2, ${aux}1)`
              `${aux}4 = If(${condition}, ${aux}1, ${aux}2)`
            ends[0] = aux + 3
            ends[1] = aux + 4
            auxiliaries.push ...[1..4].map (n) => aux + n
         'connect'
            unless args.subpoints and args.subpoints.length is 2 then return
            ends[0] = args.subpoints[0]
            ends[1] = args.subpoints[1]
         /cutoff|extend/
            pt := args.subpoints
            unless pt and pt.length is 4 then return
            [source, displacement] :=
               cutoffExtend method, pt, args.point, args.line
            ends[0] = source
            commands.push `${aux} = Translate(${source}, ${displacement})`
            auxiliaries.push aux
            ends[1] = aux
         'foot'
            pt := args.subpoints
            unless pt then return
            destination := args.plane
               ? args.plane[0]
               : `Line(${pt[1]},${pt[2]})`
            ends[0] = pt[0]
            commands.push
               `${aux} = ClosestPoint(${destination}, ${pt[0]})`
            auxiliaries.push aux
            ends[1] = aux
         'parallel'
            unless args.subpoints then return
            [newStart, oldStart, oldEnd] := args.subpoints
            commands.push `${aux}1 = Vector(${oldStart}, ${newStart})`
            auxiliaries.push aux + 1, aux + 2
            ends[0] = newStart
            ends[1] = aux + 2
            if args.line?.length is 1 and args.point?[0] is args.subpoints[0]
               // In this case we are translating an existing segment
               commands.push
                  `${name} = Translate(${args.line[0]}, ${aux}1)`
                  `${aux}2 = Vertex(${name}, 2)`
               madeSegment = true
            else
               commands.push `${aux}2 = Translate(${oldEnd}, ${aux}1)`
         'perpendicular'
            pt := args.subpoints
            unless pt then return
            inPlane .= ''
            if cdata.is3d
               unless args.plane?.length is 1 then return
               inPlane = `,${args.plane[0]}`
            switch pt.length
               when 2
                  ends[0] = pt[0]
                  ends[1] = aux
                  auxiliaries.push aux
                  commands.push
                     `${aux} = Rotate(${pt[1]}, pi/2, ${pt[0]}${inPlane})`
               when 3
                  return // TODO: line perpendicular to plane
               when 4
                  ends[0] = pt[0]
                  ends[1] = aux + 2
                  auxiliaries.push aux+1, aux+2
                  commands.push
                     `${aux}1 = Rotate(${pt[1]}, 3*pi/2, ${pt[0]}${inPlane})`
                  unitVec := `UnitVector(Vector(${pt[0]}, ${aux}1))`
                  dist := `Distance(${pt[2]}, ${pt[3]})`
                  commands.push
                     `${aux}2 = Translate(${pt[0]}, ${dist}*${unitVec})`
               else return
         /proportion|similar/
            [source, displacement] :=
               proportionSimilar method, args, cdata, aux, commands, auxiliaries
            unless source then return
            ends[0] = source
            commands.push `${aux}1 = Translate(${source},${displacement})`
            auxiliaries.push aux+1
            ends[1] = aux+1

      unless madeSegment
         commands.push `${name} = Segment(${ends[0]},${ends[1]})`
      callbacks.push (api: AppletObject) => api.setLabelVisible name, true
      parts[0].push ...ends

   circle: (name, method, args) =>
      return := freshCommander()
      return.value.ends = ['', '']
      {commands, callbacks, parts, auxiliaries, ends} := return.value
      aux := name + 'aUx'
      parts[1].push name
      circle .= ''
      switch method
         'circumcircle'
            pt := args.subpoints
            unless pt and pt.length is 3 then return
            circle = `Circle(${pt.join ','})`
         'radius'
            pt := args.subpoints
            unless pt then return
            inPlane := args.plane ? `, ${args.plane[0]}` : ''
            switch pt.length
               when 2
                  [center, point] := pt
                  ends[0] = center
                  circle = `Circle(${center}, ${point}${inPlane})`
               when 3
                  center := pt[0]
                  ends[0] = center
                  radius := `Distance(${pt[1]}, ${pt[2]})`
                  circle = `Circle(${center}, ${radius}${inPlane})`
      commands.push
         `${aux} = ${circle}`  // for the filling
         `${name} = ${circle}` // for the perimeter
      parts[2].push aux
      makeLinesInvisible callbacks, aux
      callbacks.push (api: AppletObject) => api.setLabelVisible name, true

   polygon: (name, method, args, index, cdata) =>
      return := freshCommander()
      {commands, callbacks, parts, auxiliaries} := return.value
      parts[2].push name
      aux := geoname name + 'aUx', cdata.elements, 'point'
      switch method
         /equilateralTriangle|square|regularPolygon/
            pt := args.subpoints
            unless pt then return
            N .= 3
            if method is 'square' then N = 4
            else if method is 'regularPolygon' and args.scalar
               N = args.scalar[0]
            commands.push '' // hack, make sure there is a command
            parts[0].push pt[0], pt[1]
            callbacks.push (api: AppletObject, moreParts: DimParts) =>
               made:= api.evalCommandGetLabels
                  `${name} = Polygon(${pt[0]},${pt[1]}, ${N})`
               if not made return
               for each obj of made.split ','
                  if obj is name continue
                  newObj := 'GeoAux' + index + obj
                  api.renameObject obj, newObj
                  switch api.getObjectType newObj
                     'segment'
                        moreParts[1].push newObj
                     'point'
                        moreParts[0].push newObj
                        api.setVisible newObj, false
         ///triangle|similar|parallelogram|application|quadrilateral
               |octagon|pentagon|hexagon///
            unless args.subpoints then return
            pt .= args.subpoints
            if method is 'parallelogram'
               unless pt.length is 3 then return
               commands.push `${aux} = ${pt[0]} + ${pt[2]} - ${pt[1]}`
               auxiliaries.push aux
               pt = [...pt, aux]
            else if method is 'application'
               unless pt.length is 3 then return
               unless args.polygon?.length is 1 then return
               direction := `UnitVector(${pt[2]} - ${pt[0]})`
               angle := `Angle(${pt[1]},${pt[0]},${pt[2]})`
               length := `Area(${args.polygon})`
                  + `/(Distance(${pt[0]},${pt[1]})*abs(sin(${angle})))`
               commands.push ...[0..1].map (n) =>
                  `${aux}${n} = ${pt[n]} + ${length}*${direction}`
               auxiliaries.push aux+0, aux+1
               pt = [pt[0], pt[1], aux+1, aux+0]
            else if method is 'similar'
               unless pt.length is 5 then return
               if cdata.is3d and not args.plane then return
               inSourcePlane := cdata.is3d
                  ? `, Plane(${pt[2]},${pt[3]},${pt[4]})`
                  : ''
               inDestPlane := cdata.is3d ? (', ' + args.plane?[0]) : ''
               factor :=
                  `Distance(${pt[2]},${pt[4]})/Distance(${pt[2]},${pt[3]})`
               commands.push
                  `${aux}1 = Angle(${pt[3]},${pt[2]},${pt[4]}${inSourcePlane})`
                  `${aux}2 = Rotate(${pt[1]},${aux}1,${pt[0]}${inDestPlane})`
                  `${aux}3 = ${pt[0]} + (${aux}2 - ${pt[0]})*${factor}`
               auxiliaries.push ...[1..3].map (n) => aux + n
               pt = [pt[0], pt[1], aux+3]
            else
               commands.push ''
            parts[0].push ...pt
            callbacks.push (api: AppletObject, moreParts: DimParts) =>
               command := `Polygon(${pt.join ','})`
               made := api.evalCommandGetLabels `${name} = ${command}`
               if adapParams.config?.commands
                  console.log 'Finished constructing', name, 'with', command,
                     'producing', made
               if not made return
               for each obj of made.split ','
                  if obj is name continue
                  newObj := 'GeoAux' + index + obj
                  api.renameObject obj, newObj
                  moreParts[1].push newObj

   sector: (name, method, args, index) =>
      return := freshCommander()
      return.value.ends = ['', '']
      {commands, callbacks, parts, auxiliaries, ends} := return.value
      aux := name + 'aUx'
      parts[2].push name
      switch method
         /arc|sector/
            unless args.subpoints?.length is 3 return
            parts[0].push ...args.subpoints
            [center, start, end] .= args.subpoints
            parms .= center + ', ' + start + ', ' + end
            prefix .= 'Circular'
            if method is 'arc'
               temp := start
               start = center
               center = temp
               parms = start + ', ' + center + ', ' + end
               prefix = 'Circumcircular'
            ends[0] = start
            ends[1] = end
            commands.push
               `${name} = ${prefix}Sector(${parms})`
               `${aux}1 = ${prefix}Arc(${parms})`
            parts[1].push aux + 1
            makeLinesInvisible callbacks, name

   plane: (name, method, args) =>
      return := freshCommander()
      {commands, callbacks, parts, auxiliaries} := return.value
      parts[2].push name
      switch method
         '3points'
            unless args.subpoints?.length is 3 then return
            commands.push `${name} = Plane(${args.subpoints.join ','})`
         'perpendicular'
            unless args.subpoints?.length is 2 then return
            [thru, perp] := args.subpoints
            commands.push
               `${name} = PerpendicularPlane(${thru}, Line(${thru}, ${perp}))`

   sphere: (name, method, args) => freshCommander()

   polyhedron: (name, method, args, index) =>
      return := freshCommander()
      return.value.ends = ['', '']
      {commands, callbacks, parts, auxiliaries, ends} := return.value
      aux := name + 'aUx'
      switch method
         'tetrahedron'
            pt := args.subpoints
            unless pt and pt.length is 4 then return
            commands.push '' // hack, make sure there is a command
            parts[0].push ...pt
            ends[0] = aux + 1
            ends[1] = pt[3]
            callbacks.push (api: AppletObject, moreParts: DimParts) =>
               madeBase := api.evalCommandGetLabels
                  `${ends[0]} = Polygon(${pt[0]},${pt[1]},${pt[2]})`
               if not madeBase return
               for each obj of madeBase.split ','
                  if obj is ends[0] continue
                  newObj := 'GeoAux' + index + obj
                  api.renameObject obj, newObj
                  moreParts[1].push newObj
               made := api.evalCommandGetLabels
                  `${name} = Pyramid(${aux}1, ${pt[3]})`
               if not made return
               for each obj of made.split ','
                  if obj is name continue
                  newObj := 'GeoAux' + index + obj
                  api.renameObject obj, newObj
                  switch api.getObjectType newObj
                     'segment'
                        moreParts[1].push newObj
                     'triangle'
                        moreParts[2].push newObj

// Helper for dividing an angle
function makeAngDiv(
      method:string,
      args: JoyceArguments,
      cdata: ConstructionData,
      aux: string,
      auxiliaries: string[],
      commands: string[])
   // Note we just ignore a possible plane argument; it's irrelevant
   unless args.subpoints return center: '', foot: ''
   [start, center, end] := args.subpoints
   // see if we need to make the destination segment from start to end
   destination .= ''
   unless args.line?.length is 1 and args.point?[0] is center
      destination = aux + '1'
      auxiliaries.push destination
      commands.push `${destination} = Segment(${start}, ${end})`
   else destination = args.line[0]
   n := method is 'angleBisector' ? 2 : args.scalar?[0]
   inPlane := cdata.is3d ? `, Plane(${start}, ${center}, ${end})` : ''
   commands.push
      `${aux}2 = Angle(${start}, ${center}, ${end}${inPlane})`
      `${aux}3 = If(${aux}2 > pi, ${aux}2 - 2*pi, ${aux}2)`
      `${aux}4 = Rotate(${start}, ${aux}3/${n}, ${center}${inPlane})`
   auxiliaries.push ...[2..4].map (i) => `${aux}${i}`
   return {center, foot: `Intersect(${destination}, Ray(${center}, ${aux}4))`}

// helper for separating color of perimeter and interior:
function makeLinesInvisible(callbacks: GeogebraCallback[], name: string)
   callbacks.push (api: AppletObject) =>
      api.setLineThickness name, 1
      // The rest of this function is a weird roundabout way to make
      // the lines of the sector have zero opacity. I got it from
      // https://www.reddit.com/r/geogebra/comments/12cbr85/setlineopacity_command/
      // I don't really understand how/why it works, but it seems to
      // So that's good enough for me
      xml .= api.getXML name
      xml = xml.replace /opacity="\d+"/, 'opacity="0"'
      api.evalXML xml
      // This last step is especially confusing... I think
      // evaluating the modified XML created a sort of second
      // copy of the entity, and so we have to hide the original one
      api.setVisible name, false
