import move
import path
import block

import move_parms
import hacks
import rn 

import pyx 
from math import nan, inf, sqrt, sin, cos, pi, floor
import sys

class Path_IMP:
  # The initial and final points of the {Path} object, in its native
  # orientation, are {ph.pt[0]} and {ph.pt[1]}, respectively.
  #
  # The list of oriented moves (traces and jumps) that comprise a path
  # {ph}, in the native order, is {ph.OMVS}. Namely, tracing the oriented
  # path {(ph,0)} means executing the oriented moves
  # {ph.OMVS[0],ph.OMVS[1],...,ph.OMVS[nmv-1]}, in this sequence, where
  # {nmv=len(ph.OMVS)}. Tracing the oriented path {(ph,1)} means executing
  # {rev(ph.OMVS[nmv-1]),rev(ph.OMVS[nmv-2]),...,rev(ph.OMVS[0])}, in this
  # sequence.
  #
  # The field {ph.cumtex} is a list such that {ph.cumtex[k]} is the
  # cumulative time to execute moves {ph.OMVS[0]} through {ph.OMVS[k]},
  # for each {k} in {0..nmv-1}. These times include the penalty times for
  # each trace/move or move/trace transition. The transition penalty is
  # always assumed to be added  to the execution time of the jump.
  #
  # These times assume that the nozzle is stationary at the beginning
  # and end of each move, so that {ph.cumtex[k]} is just the sum of the
  # execution times of the individual moves plus the applicable
  # transition penalties.
  
  def __init__(self, p, q, OMVS, cumtex):
    # Read-only fields:
    self.OMVS = OMVS
    self.cumtex = cumtex
    self.pt = (p, q)

# CREATION

def make_empty(p):
  return path.Path(p, p, (), ())
  
def from_move(p, q, mp):
  mv = move.make(p, q, mp)
  return path.Path(p, q, ( mv, ), ( move.extime(mv), ) )

def from_moves(omvs):
  assert type(omvs) is list or type(omvs) is tuple
  nmv = len(omvs) # Number of moves.
  assert nmv >= 1, "must have at least one move"
  p = move.pini(omvs[0])   # Initial point of final path.
  q = move.pfin(omvs[nmv-1]) # Final point of final path.
  p_prev = p
  t_prev = 0 
  cumtex = []
  for imv in range(nmv):
    omvk = omvs[imv]
    mvk, drk = move.unpack(omvk) # For type checking.
    pk, qk = move.endpoints(omvk)
    assert pk == p_prev, "moves are not connected: %s %s" % (str(p_prev),str(pk))
    tfink = t_prev + move.extime(omvk)
    if move.is_jump(omvk):
      udk = move.ud_penalty(omvk)
      # Add the applicable move/jump transition penalties:
      if imv > 0 and not move.is_jump(omvs[imv-1]):
        tfink += udk
      if imv < nmv-1 and not move.is_jump(omvs[imv+1]):
        tfink += udk
    cumtex.append(tfink)
    p_prev = qk
    t_prev = tfink
  assert p_prev == q
  return path.Path(p, q, tuple(omvs), tuple(cumtex))
  
def from_points(pts, mp_trace, mp_jump):
  assert type(pts) is list or type(pts) is tuple
  m = len(pts) # Number of points or lists.
  assert m >= 1, "must have at least one point"
  p_prev = None
  omvs = []
  cumtex = []
  jmp = None # True if previous element of {pts} was list, false if it was point.
  for ptj in pts:
    if hacks.is_point(ptj):
      # Append a single move with the given width:
      if p_prev != None: 
        mv = move.make(p_prev, ptj, mp_trace)
        omvs.append(mv)
      p_prev = ptj
      jmp = False
    else:
      # {ptj}  must be a list of points. Appends a sequence of moves from {p_prev}
      # to those points.  The first move, if any, will be a jump, the rest 
      # will be traces.  Then forces a jump to whatever comes next (if any).
      assert type(ptj) is list or type(ptj) is tuple
      mpjk = mp_jump # Parameters of of next move.
      for ptjk in ptj:
        assert hacks.is_point(ptjk)
        if p_prev != None: 
          mv = move.make(p_prev, ptjk, mpjk)
          omvs.append(mv)
        p_prev = ptjk
        mpjk = mp_trace
      mpjk = mp_jump
  assert p_prev != None # Since there must have been at least one point.
  if len(omvs) == 0:
    return path.make_empty(p_prev)
  else:
    return path.from_moves(omvs)

def concat(ophs, use_jumps, use_links, mp_jump):
  #assert type(ophs) is list or type(ophs) is tuple
  #assert type(use_jumps) is bool
  #assert type(use_links) is bool
  #assert mp_jump == None or isinstance(mp_jump, move_parms.Move_Parms)
  
  m = len(ophs) # Number of paths.
  #assert m >= 1, "must have at least one path"
  p = pini(ophs[0])   # Initial point of final path.
  q = pfin(ophs[m-1]) # Final point of final path.
  p_prev = p
  omvs = []
  for ophj in ophs:
    phj, drj = unpack(ophj) # For type checking.
    nmvj = nelems(ophj)
    pj = pini(ophj)
    qj = pfin(ophj)
    if p_prev != pj:
      # Needs a connector. Get one:
      if use_jumps or nmvj == 0 or len(omvs) == 0:
        # Use a jump:
        cnj = move.make(p_prev, pj, mp_jump)
      else:
        # Use a link if possible and desired or favorable
        #assert nmvj > 0
        use_jump = False
        use_link = use_links
        omv_prev = omvs[-1]
        omv_next = path.elem(ophj,0)
        cnj = move.connector(omv_prev, omv_next, use_jump, use_link, mp_jump)
      # Insert the connector:
      #assert isinstance(cnj, move.Move)
      omvs.append(cnj)
      p_prev = pj
    # Append the moves of {ophj}:
    for imv in range(nmvj):
      omvk = elem(ophj, imv)
      pk, qk = move.endpoints(omvk)
      #assert p_prev == pk
      omvs.append(omvk)
      p_prev = qk
  #assert p_prev == q
  return path.from_moves(omvs)

def displace(oph, ang, v):
  nmv = nelems(oph)
  if (nmv == 0):
    p = pini(oph)
    return empty(p)
  else:
    omvs = []
    for imv in range(nmv):
      omvk = elem(oph, imv)
      mpk = move.parameters(omvk)
      omvs.append(move.displace(omvk, ang, v, mpk))
    return from_moves(omvs)

# ATTRIBUTES

def nelems(oph):
  ph, dr = unpack(oph)
  return len(ph.OMVS)
  
def elem(oph, imv): 
  ph, dr = unpack(oph)
  # sys.stderr.write("oph = %s ph = %s dr = %s\n" % (str(oph), str(ph), str(dr)))
  if dr == 0:
    return ph.OMVS[imv]
  else:
    nmv = len(ph.OMVS)
    return move.rev(ph.OMVS[nmv-1-imv])
    
# GEOMETRY

def pini(oph):
  ph, dr = unpack(oph)
  return ph.pt[dr]
  
def pfin(oph):
  ph, dr = unpack(oph)
  return ph.pt[1-dr]

def bbox(OPHS):
  B = None
  for oph in OPHS:
    ph, dr = unpack(oph)
    B = rn.box_include_point(B, pini(oph)) # In case the path is empty.
    B = rn.box_join(B, move.bbox(ph.OMVS))
  return B
  # ----------------------------------------------------------------------

def find(oph, omv):
  nmv = nelems(oph)
  mv, dr = move.unpack(omv)
  for imv in range(nmv):
    omvk = elem(oph,imv)
    mvk, drk = move.unpack(omvk)
    if mvk == mv: return imv
  return None

# ORIENTATION

def rev(oph):
  ph, dr = unpack(oph)
  return (ph, 1-dr)

def unpack(oph):
  # sys.stderr.write("oph = %s\n" % str(oph))
  if isinstance(oph, path.Path):
    # sys.stderr.write("returning %s, 0\n" % str(oph))
    return oph, 0
  else:
    assert type(oph) is tuple
    assert len(oph) == 2
    ph, dr = oph
    # sys.stderr.write("ph = %s dr = %s\n" % (str(ph), str(dr)))
    assert isinstance(ph, path.Path)
    assert dr == 0 or dr == 1
    return ph, dr

# TIMING

def extime(oph):
  ph, dr = unpack(oph)
  nmv = len(ph.OMVS)
  return 0 if nmv == 0 else ph.cumtex[nmv-1]
  
def tini(oph, imv):
  ph, dr = unpack(oph)
  nmv = len(ph.OMVS)
  assert imv >= 0 and imv <= nmv
  if imv == 0: return 0
  if imv == nmv: return extime(oph)
  if dr == 0:
    # Native direction:
    return ph.cumtex[imv-1]
  else:
    # Reverse direction
    return ph.cumtex[nmv-1] - ph.cumtex[nmv-1-imv] 
  
def tfin(oph, imv):
  return tini(oph, imv+1)

# PLOTTING

def plot_standard(c, OPHS, CLRS, wd_axes, d):
  assert isinstance(OPHS, path.Path)

  def pick_colors(k):
    nclr = len(CLRS)

    # Returns the colors for trace sausages and axes of move {OMVS[k]}.
    if nclr == 1:
      ctrace = CLRS[0]
    else:
      ctrace = CLRS[k]
    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

  moves_list = split_at_jumps(OPHS, d)
  block_id = 0
  isBlock = False

  # Dimensions relative to nominal trace widths:
  rtraces = 0.80; # Trace sausage width.

  # Absolute dimensions (mm):
  j_wd_dots =   2.5*wd_axes;  # Dots at ends of moves.
  j_sz_arrows = 8*wd_axes     # Size of arrows. 

  if CLRS == None:
    CLRS = hacks.trace_colors(len(moves_list))

  for moves in moves_list:
    if len(moves) == 1 and move.is_jump(moves[0]):
      isBlock = False
      clr = pyx.color.rgb.black
      rwd = 0
      wd = wd_axes
      dashed = True
      wd_dots = j_wd_dots
      sz_arrows = j_sz_arrows

    else:
      pini = move.pini(moves[0])
      pfin = move.pfin(moves[-1])

      if pini == pfin:
        isBlock = False
        clr = pyx.color.rgb.black

      else:
        isBlock = True
        clr, caxis = pick_colors(block_id)
        block_id += 1
      
      rwd = rtraces
      wd = 0
      dashed = False
      wd_dots = 0
      sz_arrows = 0
    
    for mv in moves:  
      wdk = rwd*move.width(mv) + wd    
      move.plot_layer(c, mv, None, clr, wdk, dashed, wd_dots, sz_arrows)

      if isBlock:
        move.plot_layer(c, mv, None, caxis, wd_axes, True, j_wd_dots, j_sz_arrows)

  return moves_list

def split_at_jumps(oph, d):
  moves_list = []
  moves_aux = []

  for k in range(nelems(oph)):
    omvk_aux = elem(oph, k)
    omvk = omvk_aux

    if not isinstance(omvk_aux, move.Move):
      omvk_aux = omvk_aux[0]
    elif d != None:
      omvk = (omvk, d)

    if move.is_jump(omvk_aux):
      if len(moves_aux) > 0:
        moves_list.append(moves_aux)
      moves_list.append([omvk])
      moves_aux = []

    else:
      moves_aux.append(omvk)

  if len(moves_aux) > 0:
    moves_list.append(moves_aux)

  return moves_list