bl_info = {
    "name": "Bezier Surface",
    "author": "Clinton Reese",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "View3D > Toolbar > BezierToSurface",
    "description": "Construct a NURBS surface from bezier curves selected in order",
    "warning": "",
    "wiki_url": "",
    "category": "Add Mesh",
}

import bpy
import uuid

selected = []

def end_selection(intermediateSegments):
  intermediateSegments_p1 = intermediateSegments + 1
  if len(selected) < 2:
      ICR_OT_BezierSurfaceEndSelection.errorMsg = "Select at least two bezier curves"
      return

  bpy.ops.object.select_all(action='DESELECT')
  #check selection
  errMsg = ""
  notSplineObject = False
  for sel_item_name in selected:
    if not hasattr(bpy.data.objects[sel_item_name].data, 'splines'):
      errMsg = "\n" + sel_item_name + ' is not a spline' + errMsg
      notSplineObject = True
    elif len(bpy.data.objects[sel_item_name].data.splines[0].bezier_points) < 2:
      errMsg = "\n" + sel_item_name + ' is not a bezier spline' + errMsg
      notSplineObject = True

  if notSplineObject:
    errMsg = "Invalid selection:" + errMsg
    ICR_OT_BezierSurfaceEndSelection.errorMsg = errMsg
    return
  #TODO check same num spline points

  id = uuid.uuid1().hex
  newname = 'SurfCurve_' + id[:4]
  # make a new curve
  crv = bpy.data.curves.new(newname, 'SURFACE')
  crv.dimensions = '3D'

  # make a new spline in that surface curve
  newspline = crv.splines.new(type='NURBS')

  #read the first selected object splines attribute and matrix
  #NOTE ERROR HERE IF BOX SELECT OBJECTS
  splines = bpy.data.objects[selected[0]].data.splines
  matrix_world = bpy.data.objects[selected[0]].matrix_world

  #one object can have many splines get points for the first one only
  points = get_points(splines[0], matrix_world)

  # a spline point for each point
  newspline.points.add(len(points)-1) # theres already one point by default
  # assign the point coordinates to the spline points
  for p, new_co in zip(newspline.points, points):
      p.co = ([new_co[0], new_co[1], new_co[2], 1.0])

  # make a new object with the curve
  obj = bpy.data.objects.new(newname, crv)

  bpy.context.scene.collection.objects.link(obj)

  #select only the new object
  obj.select_set(state=True)
  bpy.context.view_layer.objects.active = obj

  #enter edit mode and extrude for all the rows
  bpy.ops.object.editmode_toggle()
  bpy.ops.curve.select_all(action='SELECT')

  for ex in range(1, len(selected)):
    if intermediateSegments > 0:
      for s in range(1, intermediateSegments_p1):
        bpy.ops.curve.extrude()
    bpy.ops.curve.extrude()

  #exit edit mode to modify the points
  bpy.ops.object.editmode_toggle()

  #need read spline so can see new extruded points
  spline = obj.data.splines[0]

  #read all points first so can do intermediate curves
  allsplinespoints = []
  for sel_item_name in selected:
    matrix_world = bpy.data.objects[sel_item_name].matrix_world
    splines = bpy.data.objects[sel_item_name].data.splines
    tspline = splines[0]
    points = get_points(tspline, matrix_world)
    allsplinespoints.append(points)

  print('process curve points')
  #set positions of the direct selected segments, weights are all = 1.0
  selCount = 0
  for points in allsplinespoints:
      numbezierpoints = len(points)
      splineIndex = selCount * numbezierpoints

      for point in points:
          spline.points[splineIndex].co = ([point[0], point[1], point[2], 1.0])
          splineIndex = splineIndex + 1

      selCount = selCount + 1 + intermediateSegments

  #do intermediate segments
  #allsplinepoints are all the selected spline points
  selCount = 0
  if intermediateSegments > 0:
    # for points in allsplinespoints:
    for tempvar in range(len(allsplinespoints)-1):
      print("selCount " + str(selCount))
      firstspline = allsplinespoints[selCount]
      lastspline = allsplinespoints[selCount+1]
      #calc each intemediate spline
      for s in range(1, intermediateSegments_p1):
        for splinepointindex in range(numbezierpoints):
          firstpoint = firstspline[splinepointindex]
          lastpoint = lastspline[splinepointindex]
          xdiff = lastpoint[0] - firstpoint[0]
          ydiff = lastpoint[1] - firstpoint[1]
          zdiff = lastpoint[2] - firstpoint[2]
          pointx = firstpoint[0] + xdiff * s / intermediateSegments_p1
          pointy = firstpoint[1] + ydiff * s / intermediateSegments_p1
          pointz = firstpoint[2] + zdiff * s / intermediateSegments_p1

          theindex = selCount * intermediateSegments_p1 * numbezierpoints +  numbezierpoints * s + splinepointindex
          print("index " + str(theindex))
          spline.points[theindex].co = ([pointx, pointy, pointz, 1.0])

      selCount = selCount + 1

  spline.use_bezier_u = True
  spline.use_endpoint_v = True
  spline.order_u = 4

  spline.order_v = 2
  if intermediateSegments > 0:
    v_order = len(selected) + (len(selected) - 1) * intermediateSegments - 1
    if v_order > 4:
      v_order = 4
    if v_order < 3:
      v_order = 3
    spline.order_v = v_order

def get_points(spline, matrix_world):

    bezier_points = spline.bezier_points

    if len(bezier_points) < 2:
        return []

    r = spline.resolution_u + 1
    if r < 2:
       return []
    intermediateSegments = len(bezier_points)

    point_list = []
    for i in range(intermediateSegments):
        bezier_point = matrix_world @ bezier_points[i].co
        handleL = matrix_world @ bezier_points[i].handle_left
        handleR = matrix_world @ bezier_points[i].handle_right

        point_list.append(handleL)
        point_list.append(bezier_point)
        point_list.append(handleR)

    return point_list

def my_handler(scene):
   global selected

   curSelectionRaw = bpy.context.selected_objects

   #don't know how to get key events so commit by selecting nothing
  #  if len(curSelectionRaw) == 0:
  #     print('final selection')
  #     bpy.app.handlers.depsgraph_update_post.clear()
  #     end_selection(scene.beziertosurface_props.intermediateSegments)
  #     return

   curSelectionRawNames = {obb.name for obb in curSelectionRaw}

   if len(selected) == 0:
      selected = curSelectionRawNames
      return

   #remove deselected
   newselected = []
   for objname in selected:
         if objname in curSelectionRawNames:
            newselected.append(objname)

   #add new selected
   for obraw in curSelectionRawNames:
         if obraw in selected:
            continue
         else:
            newselected.append(obraw)

   selected = newselected

   print('ordered selection')
   if len(selected) > 0:
         for obordered in selected:
            print(obordered)

class BezierToSurface_Settings(bpy.types.PropertyGroup):
   intermediateSegments: bpy.props.IntProperty(name='Intermediate Segments', description='Number of rows to generate inbetween the selected bezier curves', default= 0, min=0, max=5)

#panel draw is run when mouse events - not just at creation time
class ICR_PT_BezierSurfacePanel(bpy.types.Panel):
   bl_label = "Bezier To Surface"
   bl_idname = "ICR_PT_BezierSurfacePanel"
   bl_space_type = 'VIEW_3D'
   bl_region_type = 'UI'
   bl_category = 'Clintons3D'

   startBezierEnabled= True
   endBezierEnabled= False
   cancelBezierEnabled= False

   def draw(self, context):
      layout = self.layout

      row = layout.row()
      row.prop(context.scene.beziertosurface_props, 'intermediateSegments')

      startBezierRow = layout.row()
      startBezierRow.operator('icr.beziersurfacestartselection', text = 'Start Bezier Selection')
      startBezierRow.enabled = self.startBezierEnabled

      endBezierRow = layout.row()
      endBezierRow.operator('icr.beziersurfaceendselection', text = 'End Bezier Selection')
      endBezierRow.enabled = self.endBezierEnabled

      cancelBezierRow = layout.row()
      cancelBezierRow.operator('icr.beziersurfacecancel', text = 'Cancel')
      cancelBezierRow.enabled = self.cancelBezierEnabled

class ICR_OT_BezierSurfacePanel(bpy.types.Operator):

   bl_idname = 'icr.beziersurfacepanel'
   #this is the label that essentially is the text displayed on the button
   bl_label = 'Bezier Surface Panel Op'
   #these are the options for the operator, this one makes it not appear
   #in the search bar and only accessible by script, useful
   #NOTE: it's a list of strings in {} braces, see blender documentation on types.operator
   bl_options = {'INTERNAL'}

   def execute(self, context):
      # scene = context.scene
      #return value that tells blender we finished without failure
      return {'FINISHED'}

class ICR_OT_BezierSurfaceStartSelection(bpy.types.Operator):
   """ Begin the process to select bezier curves one by one """
   bl_idname = 'icr.beziersurfacestartselection'
   #this is the label that essentially is the text displayed on the button
   bl_label = 'Bezier Surface Op'

   errorMsg = ''

   def execute(self, context):
      if not ICR_PT_BezierSurfacePanel.startBezierEnabled:
         print('start not enabled')
         return {'FINISHED'}

      ICR_PT_BezierSurfacePanel.startBezierEnabled = False
      ICR_PT_BezierSurfacePanel.endBezierEnabled = True
      ICR_PT_BezierSurfacePanel.cancelBezierEnabled = True
      # scene = context.scene

      bpy.app.handlers.depsgraph_update_post.append(my_handler)
      #return value that tells blender we finished without failure
      return {'FINISHED'}

class ICR_OT_BezierSurfaceEndSelection(bpy.types.Operator):
   """ End selection mode and create the NURBS surface """
   bl_idname = 'icr.beziersurfaceendselection'
   #this is the label that essentially is the text displayed on the button
   bl_label = 'Bezier Surface Op'

   errorMsg = ''

   def execute(self, context):
      if not ICR_PT_BezierSurfacePanel.endBezierEnabled:
         return {'FINISHED'}

      ICR_PT_BezierSurfacePanel.startBezierEnabled = True
      ICR_PT_BezierSurfacePanel.endBezierEnabled = False
      ICR_PT_BezierSurfacePanel.cancelBezierEnabled = False
      scene = context.scene

      bpy.app.handlers.depsgraph_update_post.clear()
      end_selection(scene.beziertosurface_props.intermediateSegments)
      if not self.errorMsg == '':
         self.report({"ERROR"}, self.errorMsg)
      #return value that tells blender we finished without failure
      return {'FINISHED'}

class ICR_OT_BezierSurfaceCancel(bpy.types.Operator):
   """ Cancel the operation """
   bl_idname = 'icr.beziersurfacecancel'
   #this is the label that essentially is the text displayed on the button
   bl_label = 'Bezier Surface Op'

   def execute(self, context):
      if not ICR_PT_BezierSurfacePanel.cancelBezierEnabled:
         return {'FINISHED'}

      ICR_PT_BezierSurfacePanel.startBezierEnabled = True
      ICR_PT_BezierSurfacePanel.endBezierEnabled = False
      ICR_PT_BezierSurfacePanel. cancelBezierEnabled = False

      bpy.app.handlers.depsgraph_update_post.clear()

      self.report({"INFO"}, 'operation cancelled')
      #return value that tells blender we finished without failure
      return {'FINISHED'}

#Here we are Registering the Classes       
classes = (
    BezierToSurface_Settings,
    ICR_PT_BezierSurfacePanel,
    ICR_OT_BezierSurfacePanel,
    ICR_OT_BezierSurfaceStartSelection,
    ICR_OT_BezierSurfaceEndSelection,
    ICR_OT_BezierSurfaceCancel
)

def register():
   from bpy.utils import register_class
   for cls in classes:
      register_class(cls)
   bpy.types.Scene.beziertosurface_props = bpy.props.PointerProperty(type=BezierToSurface_Settings)

def unregister():
    from bpy.utils import unregister_class
    del bpy.types.Scene.beziertosurface_props 
    for cls in reversed(classes):
        unregister_class(cls)

   #This is required in order for the script to run in the text editor   
if __name__ == "__main__":
   register()  

#https://blenderartists.org/t/rna-uiitemr-property-not-found/608561
#You can’t have panel properties, use global properties instead (bpy.types.Scene, bpy.types.WindowManager, …)

#finally some good information!
#https://gist.github.com/tin2tin/ce4696795ad918448dfbad56668ed4d5