# Implementation of module {move}
# Last edited on 2021-11-06 23:04:46 by stolfi

import move
import move_parms
import hacks
import rn
import pyx 
from math import nan, inf, sqrt
import sys

class Move_IMP:
  # The field {mv.endpts} is a 2-tuple with the two endpoints of the 
  # axis, in arbitrary order.   An oriented move {(mv,dr)} means 
  # that the motion is from {mv.endpts[dr]} to {mv.endpts[1-dr]}.
  #
  # The field {mv.mp} is a {Move_Parms} object that specifies the width 
  # and timing parameters of the move.
  # The field {mv.fabtime} is the time (in seconds) needed to fabricate it, NOT including the 
  # nozzle up/down or filament suck/refeed if it is a jump.

  def __init__(self, p0, p1, mp, tex):
    self.endpts = (p0, p1)
    self.mp = mp
    self.fabtime = tex
    self.name = None
    # ----------------------------------------------------------------------
    # Fields for the {hotpath} heuristic (see {move_hp_IMP.py):
    self.hp_paths = None  # List of input paths that own the move and its indices therein.

def make(p0, p1, mp):
  assert hacks.is_point(p0); p0 = tuple(p0) # Make sure it is immutable.
  assert hacks.is_point(p1); p1 = tuple(p1) # Make sure it is immutable.
  assert mp != None and isinstance(mp, move_parms.Move_Parms)
  dpq = rn.dist(p0, p1)
  tex = move_parms.nozzle_travel_time(dpq, None, mp)
  return move.Move(p0, p1, mp, tex)

def parameters(omv):
  mv, dr = unpack(omv)
  return mv.mp

def is_jump(omv):
  if omv == None: return False
  mv, dr = unpack(omv)
  return move_parms.width(mv.mp) == 0

def is_trace(omv):
  if omv == None: return False
  mv, dr = unpack(omv)
  return move_parms.width(mv.mp) > 0

def width(omv):
  mv, dr = unpack(omv)
  return move_parms.width(mv.mp)

def pini(omv):
  mv, dr = unpack(omv)
  return mv.endpts[dr]

def pfin(omv):
  mv, dr = unpack(omv)
  return mv.endpts[1-dr]
  # ----------------------------------------------------------------------

def endpoints(omv):
  mv, dr = unpack(omv)
  pts = (mv.endpts[dr], mv.endpts[1-dr])
  return pts
  # ----------------------------------------------------------------------

def length(omv):
  mv, dr = unpack(omv)
  L = rn.dist(mv.endpts[0], mv.endpts[1])
  return L
  # ----------------------------------------------------------------------

def bbox(OMVS):
  B = None
  for omv in OMVS:
    B = rn.box_include_point(B, pini(omv))
    B = rn.box_include_point(B, pfin(omv))
  return B
  # ----------------------------------------------------------------------

def rev(omv):  
  mv, dr = unpack(omv)
  return (mv, 1-dr)

def spin(omv, dr):
  mv1, dr1 = unpack(omv)
  return (mv1, (dr1 + dr) % 2)

def unpack(omv):
  if isinstance(omv, move.Move):
    return omv, 0
  else:
    # sys.stderr.write("omv =%s\n" % str(omv))
    assert type(omv) is tuple
    assert len(omv) == 2
    mv, dr = omv
    assert isinstance(mv, move.Move)
    assert dr == 0 or dr == 1
    return mv, dr

def displace(omv, ang, disp, mp):
  p = tuple(rn.add(rn.rotate2(pini(omv), ang), disp))
  q = tuple(rn.add(rn.rotate2(pfin(omv), ang), disp))
  return make(p, q, mp)
  
def sort_by_midpoint(omv0, omv1, ydir):
  pm0 = rn.mix(0.5, pini(omv0), 0.5, pfin(omv0))
  pm1 = rn.mix(0.5, pini(omv1), 0.5, pfin(omv1))
  if ydir == None:
    y0 = pm0[1]; y1 = pm1[1]
  else:
    y0 = rn.dot(pm0, ydir)
    y1 = rn.dot(pm1, ydir)
  if y0 < y1: 
    return omv0, omv1
  else:
    return omv1, omv0
  # ----------------------------------------------------------------------

def shared_border(omv0, omv1, mvdir, tol):
  debug = False
  
  if is_jump(omv0) or is_jump(omv1): 
    if debug: sys.stderr.write("one or both moves are jumps\n")
    p0 = None; p1 = None
  else:
    mv0, dr0 = unpack(omv0)
    e0a, e0z = mv0.endpts # Endpoints of axis segment {S0} of {mv0}.
    wd0 = width(mv0)
    
    mv1, dr1 = unpack(omv1)
    e1a, e1z = mv1.endpts # Endpoints of axis segment {S1} of {mv1}.
    wd1 = width(mv1)

    ysep = (wd0 + wd1)/2 # Ideal separation between the two moves.
    
    if mvdir == None:
      # Try to guess a {mvdir} from the directions of the two moves:
      mvdir0, len0 = rn.dir(rn.sub(pfin(omv0), pini(omv0)))
      mvdir1, len1 = rn.dir(rn.sub(pfin(omv1), pini(omv1)))
      # If the moves have opposite orientation, reverse one of {mvdir0,mvdir1}:
      if rn.dot(mvdir0,mvdir1) < 0: mvdir1 = rn.scale(-1, mvdir1)
      if len0 >= tol and len1 >= tol and rn.dot(mvdir0,mvdir1) > 0.90:
        mvdirm, lenm = rn.dir(rn.add(mvdir0, mvdir1))
        assert lenm > 1.0
        mvdir = mvdirm
    
    if mvdir == None:
      # Guess failed, give up:
      p0 = None; p1 = None
    else:
      xdir = mvdir
      ydir = (-xdir[1], +xdir[0])

      # Compute projection of endpoints in direction {xdir}:
      x0a = rn.dot(e0a, xdir); x0z = rn.dot(e0z, xdir)
      x1a = rn.dot(e1a, xdir); x1z = rn.dot(e1z, xdir)

      # Ensure that projections are oriented along {xdir}:
      xlo0 = min(x0a, x0z); xhi0 = max(x0a, x0z)
      xlo1 = min(x1a, x1z); xhi1 = max(x1a, x1z)

      # Compute ovrlap in direction {xdir}:
      xlo = max(xlo0,xlo1); xhi = min(xhi0,xhi1)
      dx = xhi - xlo;  # length of overlap, or negative if no overlap.
      if dx <= 1.0e-6*ysep:
        if debug: 
          sys.stderr.write("move axes do not overlap along {xdir}")
          sys.stderr.write(" mv0 = [ %+10.6f _ %+10.6f ]" % (xlo0,xhi0))
          sys.stderr.write(" mv1 = [ %+10.6f _ %+10.6f ]" % (xlo1,xhi1))
          sys.stderr.write(" ovr = [ %+10.6f _ %+10.6f ]\n" % (xlo,xhi))
        p0 = None; p1 = None
      else:

        # Lift the X-range of overlap onto the segments:
        r0a = (xlo - x0a)/(x0z - x0a)
        c0a = rn.mix(1-r0a, e0a, r0a, e0z)
        r0z = (xhi - x0a)/(x0z - x0a)
        c0z = rn.mix(1-r0z, e0a, r0z, e0z)

        r1a = (xlo - x1a)/(x1z - x1a)
        c1a = rn.mix(1-r1a, e1a, r1a, e1z)
        r1z = (xhi - x1a)/(x1z - x1a)
        c1z = rn.mix(1-r1z, e1a, r1z, e1z)

        # Project the lifted points in the direction {ydir}:
        y0a = rn.dot(c0a, ydir); y0z = rn.dot(c0z, ydir); y0m = (y0a + y0z)/2
        y1a = rn.dot(c1a, ydir); y1z = rn.dot(c1z, ydir); y1m = (y1a + y1z)/2

        # Check for near-parallelism and proper distance perpendicularly to {xdir}:
        dy01 = abs(y0m - y1m) # Distance in {ydir} between midpoints.
        if abs(y0a - y0z) > tol or abs(y1a - y1z) > tol:
          if debug: sys.stderr.write("dy0 = %+.8f dy1 = %+.8f tol = %.8f\n" % (y0z-y0a,y1z-y1a,tol))
          if debug: sys.stderr.write("one of both segments are not parallel to {xdir}\n")
          p0 = None; p1 = None
        elif dy01 > ysep + tol:
          if debug: sys.stderr.write("the segments are too far apart in {ydir}\n")
          p0 = None; p1 = None
        elif dy01 < ysep - tol:
          if debug: sys.stderr.write("the segments overlap too much in {ydir}\n")
          p0 = None; p1 = None
        else:    
          ym = (wd1*y0m + wd0*y1m)/(wd0 + wd1)
          xm = (xlo + xhi)/2
          xr = (xhi - xlo)/2
          p0 = tuple(rn.mix(xm - xr, xdir, ym, ydir))
          p1 = tuple(rn.mix(xm + xr, xdir, ym, ydir))
  return (p0,p1)
  # ----------------------------------------------------------------------

def fabtime(omv):
  mv, dr = unpack(omv)
  return mv.fabtime

def ud_penalty(omv):
  mv, dr = unpack(omv)
  ac, sp, ud = move_parms.dynamics(mv.mp)
  if not is_jump(omv): assert ud == 0
  return ud
 
def transition_penalty(omv0, omv1):
  if omv0 == None or omv1 == None: return 0
  assert pfin(omv0) == pini(omv1), "moves are not connected"
  if is_jump(omv0) and is_trace(omv1):
    return ud_penalty(omv0)
  elif is_trace(omv0) and is_jump(omv1):
    return ud_penalty(omv1)
  else:
    return 0

def cover_time(omv, m):
  mv, dr = unpack(omv) # To type-check.
  ac, sp, ud = move_parms.dynamics(mv.mp)
  # Compute cover time {tomv} from start of move {omv}:
  p, q = endpoints(omv)
  vpm = rn.sub(m, p)
  vpq = rn.sub(q, p)
  dpq = rn.norm(vpq)
  dpm = rn.dot(vpm,vpq)/dpq  # Distance from {p} to point nearest to {m}
  # sys.stderr.write("dpq = %12.8f dpm = %12.8f ratio = %12.8f\n" % (dpq, dpm, dpm/dpq))
  if dpm < 0: dpm = 0
  if dpm > dpq: dpm = dpq
  tc = move_parms.nozzle_travel_time(dpq, dpm, mv.mp)
  return tc

def plot_to_files(fname, OMVS, CLRS, rwd, wd_axes):  
  assert type(OMVS) is list or type(OMVS) is tuple
  
  # Compute the plot's bounding box:
  B = bbox(OMVS)

  c, szx,szy = hacks.make_canvas(hacks.round_box(B,0.5), None, None, True, True, 1, 1)

  axes = True
  dots = True
  arrows = True
  matter = True

  plot_standard(c, OMVS, None, None, CLRS, rwd, wd_axes, axes, dots, arrows, matter)
  hacks.write_plot(c, fname)
  return
  # ----------------------------------------------------------------------

def plot_standard(c, OMVS, dp, layer, CLRS, rwd, wd_axes, axes, dots, arrows, matter):
  
  assert type(OMVS) is list or type(OMVS) is tuple
  nmv = len(OMVS)
  assert nmv > 0
  
  assert wd_axes != None and wd_axes > 0
  
  if CLRS == None: 
    CLRS = [ pyx.color.rgb(0.050, 0.800, 0.000), ] # Default trace color.
  else:
    assert type(CLRS) is list or type(CLRS) is tuple
  nclr = len(CLRS)
  assert nclr == 1 or nclr == nmv
  
  def pick_colors(kmv):
    # Returns the colors for trace sausages and axes of move {OMVS[kmv]}.
    if nclr == 1:
      ctrace = CLRS[0]
    else:
      ctrace = CLRS[kmv]
    caxis =   pyx.color.rgb(0.6*ctrace.r, 0.6*ctrace.g, 0.6*ctrace.b) # Color of trace axis, dots, arrow.
    return ctrace, caxis

  # Colors:
  cmatter = hacks.matter_color(None) # Est. material footprint.
  cjumps = pyx.color.rgb.black                 # Axis lines, dots, and arrowheads of jumps.

  # Dimensions relative to trace nominal widths:
  rwd_matter = 1.13;  # Estimated material.
  
  # Absolute dimenstons (in mm):
  wd_dots = 2.5*wd_axes
  sz_arrows = 6*wd_axes

  # Get list {lys} of layers to plot:
  if layer == None:
    # Plot all four layers.
    lys = (range(4))
  else:
    # Plot only the selected layer.
    assert type (layer) is int
    assert layer >= 0 and layer < 4
    lys = (layer,)

  # Plot the layers:
  for ly in lys:
    # sys.stderr.write("{move.plot_standard}: layer %d\n" % ly)
    for kmv in range(nmv):
      omv = OMVS[kmv]
      mv, dr = unpack(omv) # For the typechecking.
      jmp = is_jump(omv)
      wd = width(omv)
      if ly == 0 and not jmp and matter:
        # plots the estimate of actual material:
        wd_matter = rwd_matter*wd
        plot_layer \
          (c, omv, dp, clr=cmatter, wd=wd_matter, dashed=False, wd_dots=0, sz_arrow=None)
      elif ly == 1 and not jmp:
        # plots the nominal trace material:
        wd_trace = rwd*wd
        ctrace, caxis = pick_colors(kmv)
        plot_layer \
          (c, omv, dp, clr=ctrace, wd=wd_trace, dashed=False, wd_dots=0, sz_arrow=None)
      elif ly == 2 and not jmp and (axes or dots or arrows):
        # Trace axis and/or dots and/or arrowhead:
        ctrace, caxis = pick_colors(kmv)
        t_wd_axis = wd_axes if axes else 0
        t_wd_dots = wd_dots if dots else 0
        t_sz_arrow = sz_arrows if arrows else 0
        plot_layer(c, omv, dp, clr=caxis, wd=t_wd_axis, dashed=False, wd_dots=t_wd_dots, sz_arrow=t_sz_arrow)
      elif ly == 3 and jmp:
        # Jump axis, dots, and arrowhead:
        j_wd_axis = wd_axes
        j_wd_dots = wd_dots
        j_sz_arrow = sz_arrows
        plot_layer(c, omv, dp, clr=cjumps, wd=j_wd_axis, dashed=True, wd_dots=j_wd_dots, sz_arrow=j_sz_arrow)
  return
  # ----------------------------------------------------------------------

def plot_layer(c, omv, dp, clr, wd, dashed, wd_dots, sz_arrow):
  
  if clr == None: return
  
  # Simplifications:
  if wd == None: wd = 0 
  if wd_dots == None: wd_dots = 0 
  if sz_arrow == None: sz_arrow = 0
  assert wd >= 0 and wd_dots >= 0 and sz_arrow >= 0 
  
  # Get the move endpoints:
  p, q = endpoints(omv)
  vpq = rn.sub(q,p)
  dpq = rn.norm(vpq)
  
  # Omit the arrowhead if not enough space for it:
  if dpq <= 2*sz_arrow: sz_arrow = 0
  
  # Nothing to plot if nothing is requested:
  if wd == 0 and wd_dots == 0 and sz_arrow == 0: return
  
  arrowpos = 0.5  # Position of arrow along axis.

  # Perturbation to force painting of zero-length lines.
  eps = 0.0001*max(wd,wd_dots,sz_arrow)  # For plotting the dots.
  assert eps > 0
  
  if wd == 0 and sz_arrow > 0:
    # We want an invisible line but still with the arrowhead.
    # Unfortunately setting linewidth(0) still draws a thin line.
    # So we cook things up, by moving {p} and {q} right _next 
    # to the arrowhead.
    m = rn.mix(1-arrowpos, p, arrowpos, q)  # Posititon of arrow.
    shaft = 0.9*sz_arrow  # Length of reduced arrow shaft.
    pa = arrowpos*shaft/dpq
    qa = (1-arrowpos)*shaft/dpq
    paxis = rn.mix(1, m, -pa, vpq)
    qaxis = rn.mix(1, m, +qa, vpq)
  elif dpq == 0:
    # Perturb {p,q} to ensure that the zero-length line is drawn as a dot:
    paxis = rn.sub(p,(eps,eps))
    qaxis = rn.add(q,(eps,eps))
  else:
    paxis = p; qaxis = q
  
  # Define styles {sty_axis} for the axis, {sty_dots} for the dots:
  sty_comm = [ pyx.style.linecap.round, pyx.style.linejoin.round, clr, ] # Common style.
  
  sty_axis = sty_comm + [ pyx.style.linewidth(wd) ]
  if dp != None: sty_axis.append(pyx.trafo.translate(dp[0], dp[1]))

  if sz_arrow > 0: 
    # Define the arrow style {sty_deco}:
    wdarrow = sz_arrow/8 # Linewidth for stroking the arrowhead (guess).
    sty_deco = sty_comm + [ pyx.style.linewidth(wdarrow) ]
    sty_deco = sty_deco + [ pyx.deco.stroked([pyx.style.linejoin.round]), pyx.deco.filled([]) ]
    
    # Add an arrowhead in style {sty_deco} to {sty_axis}:
    sty_axis = sty_axis + [ 
      pyx.deco.earrow(sty_deco, size=sz_arrow, constriction=None, pos=arrowpos, angle=35)
    ]
  
  if wd_dots > 0:
    # Plot dots:
    sty_dots = sty_comm + [ pyx.style.linewidth(wd_dots) ]
    if dp != None: sty_dots.append(pyx.trafo.translate(dp[0], dp[1]))
    c.stroke(pyx.path.line(p[0]+eps, p[1]+eps, p[0]-eps, p[1]-eps), sty_dots)
    c.stroke(pyx.path.line(q[0]+eps, q[1]+eps, q[0]-eps, q[1]-eps), sty_dots)

  if wd > 0 and dashed: 
    # Define dash pattern, or turn off dashing if too short:
    rdashpat = hacks.adjust_dash_pattern(dpq/wd, 1.75, 2.00)
    if rdashpat != None:
      # Make the line style dashed:
      sty_axis = sty_axis + [ pyx.style.linestyle(pyx.style.linecap.round, pyx.style.dash(rdashpat)) ]

  if wd > 0 or sz_arrow > 0:
    # Stroke the axis, with fixed endpoints:
    c.stroke(pyx.path.line(paxis[0], paxis[1], qaxis[0], qaxis[1]), sty_axis)

# DEBUGGING AND TESTING

def has_name(omv):
  mv, dr = unpack(omv)
  return mv.name != None
  # ----------------------------------------------------------------------

def get_name(omv):
  mv, dr = unpack(omv)
  name = mv.name
  if name == None: name = "J?" if is_jump(mv) else "T?"
  if dr == 1: name = "~" + name
  return name
  # ----------------------------------------------------------------------

def set_name(omv, name):
  assert type(name) is str
  mv, dr = unpack(omv)
  mv.name = name
  return
  # ----------------------------------------------------------------------

def tag_names(OMVS, tag):
  if tag != None and tag != "":
    assert type(tag) is str
    for omv in OMVS:
      mv, dr = unpack(omv)
      mv.name = tag + get_name(mv)
  return
  # ----------------------------------------------------------------------

def show(wr, pref, omv, suff, wna):
  mv, dr = unpack(omv)
  if pref != None: wr.write(pref)
  wr.write(" " if dr == 0 else "~")
  wr.write("%-*s" % (wna,get_name(mv)))
  p = pini(omv)
  q = pfin(omv)
  wr.write(" (%6.1f,%6.1f)" % (p[0],p[1]))
  wr.write(" (%6.1f,%6.1f)" % (q[0],q[1]))
  wd = width(mv)
  if wd == 0:
    wr.write(" jmp")
  else:
    wr.write(" %3.1f" % wd)
  if p == q:
    wr.write(" trivial")
  elif p[0] == q[0]:
    wr.write(" vertical")
  elif p[1] == q[1]:
    wr.write(" horizontal")
  if suff != None: wr.write(suff)
  return
  # ----------------------------------------------------------------------

def show_list(wr, pref, OMVS, suff):
  nmv = len(OMVS)
  if nmv == 0: return
  wix = len(str(nmv-1)) # Digits for index.
  wna = 3 # Width of "name" column; min 3 because of the header.
  for omv in OMVS:
    mv, dr = unpack(omv)
    wna = max(wna, len(get_name(mv)))
  
  wr.write("\n")

  # Write header:
  wr.write("%*s%*s %-*s %*s %*s  wd obs\n" % (len(pref),'',wix,"k",wna+1,"name",15,"pini",15,"pfin"))
  wr.write("%*s%s %s %s %s --- %s\n" % (len(pref),'',"-"*wix,"-"*(wna+1),"-"*15,"-"*15,"-"*15))
  
  # Write moves:
  for kmv in range(len(OMVS)):
    omv = OMVS[kmv]
    if pref != None: wr.write(pref)
    wr.write("%*d " % (wix, kmv))
    show(wr, None, omv, suff, wna)
    wr.write("\n")

  wr.write("\n")
  return
  # ----------------------------------------------------------------------
  
