# SPDX-License-Identifier: LGPL-2.1-or-later

#***************************************************************************
#*   Copyright (c) 2012 Milos Koutny <milos.koutny@gmail.com>              *
#*                                                                         *
#*   This file is part of the FreeCAD CAx development system.              *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU Library General Public License (LGPL)   *
#*   as published by the Free Software Foundation; either version 2 of     *
#*   the License, or (at your option) any later version.                   *
#*   for detail see the LICENCE text file.                                 *
#*                                                                         *
#*   FreeCAD is distributed in the hope that it will be useful,            *
#*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#*   GNU Library General Public License for more details.                  *
#*                                                                         *
#*   You should have received a copy of the GNU Library General Public     *
#*   License along with FreeCAD; if not, write to the Free Software        *
#*   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
#*   USA                                                                   *
#*                                                                         *
#***************************************************************************/

import FreeCAD, Part, os, FreeCADGui
from FreeCAD import Base
from math import *
import ImportGui
from builtins import open as pyopen

##########################################################
# Script version dated 19-Jan-2012                       #
##########################################################
#Configuration parameters below - use standard slashes / #
##########################################################

## path to table file (simple comma separated values)

model_tab_filename = FreeCAD.getHomePath()+ "Mod/Idf/Idflibs/footprints_models.csv"

## path to directory containing step models

step_path=FreeCAD.getHomePath()+ "Mod/Idf/Idflibs/"

ignore_hole_size=0.5 # size in MM to prevent huge number of drilled holes
EmpDisplayMode=2 # 0='Flat Lines', 1='Shaded', 2='Wireframe', 3='Points'; recommended 2 or 0

IDF_sort=0 # 0-sort per refdes [1 - part number (not preferred)/refdes] 2-sort per footprint/refdes

IDF_diag=0 # 0/1=disabled/enabled output (footprint.lst/missing_models.lst)
IDF_diag_path="/tmp" # path for output of footprint.lst and missing_models.lst


########################################################################################
#              End config section do not touch code below                              #
########################################################################################

def open(filename):
    """called when freecad opens an Emn file"""
    docname = os.path.splitext(os.path.basename(filename))[0]
    doc = FreeCAD.newDocument(docname)
    message='Started with opening of "'+filename+'" file\n'
    FreeCAD.Console.PrintMessage(message)
    process_emn(doc,filename)

def insert(filename,docname):
    """called when freecad imports an Emn file"""
    FreeCAD.setActiveDocument(docname)
    doc=FreeCAD.getDocument(docname)
    FreeCAD.Console.PrintMessage('Started import of "'+filename+'" file')
    process_emn(doc,filename)

def process_emn(doc,filename):
   """process_emn(document, filename)-> adds emn geometry from emn file"""
   emnfile=pyopen(filename, "r")
   emn_unit=1.0 #presume millimeter like emn unit
   emn_version=2 #presume emn_version 2
   board_thickness=0 #presume 0 board height
   board_outline=[] #no outline
   drills=[] #no drills
   placement=[] #no placement
   place_item=[] #empty place item
   emnlines=emnfile.readlines()
   emnfile.close()
   passed_sections=[]
   current_section=""
   section_counter=0
   for emnline in emnlines:
       emnrecords=split_records(emnline)
       if len( emnrecords )==0 : continue
       if len( emnrecords[0] )>4 and emnrecords[0][0:4]==".END":
          passed_sections.append(current_section)
          current_section=""
       elif emnrecords[0][0]==".":
          current_section=emnrecords[0]
          section_counter=0
       section_counter+=1
       if current_section==".HEADER"  and section_counter==2:
          emn_version=int(float(emnrecords[1]))
          FreeCAD.Console.PrintMessage("Emn version: "+emnrecords[1]+"\n")
       if current_section==".HEADER"  and section_counter==3 and emnrecords[1]=="THOU":
          emn_unit=0.0254
          FreeCAD.Console.PrintMessage("UNIT THOU\n" )
       if current_section==".HEADER"  and section_counter==3 and emnrecords[1]=="TNM":
          emn_unit=0.000010
          FreeCAD.Console.PrintMessage("TNM\n" )
       if current_section==".BOARD_OUTLINE"  and section_counter==2:
          board_thickness=emn_unit*float(emnrecords[0])
          FreeCAD.Console.PrintMessage("Found board thickness "+emnrecords[0]+"\n")
       if current_section==".BOARD_OUTLINE"  and section_counter>2:
          board_outline.append([int(emnrecords[0]),float(emnrecords[1])*emn_unit,float(emnrecords[2])*emn_unit,float(emnrecords[3])])
       if current_section==".DRILLED_HOLES"  and section_counter>1 and float(emnrecords[0])*emn_unit>ignore_hole_size:
          drills.append([float(emnrecords[0])*emn_unit,float(emnrecords[1])*emn_unit,float(emnrecords[2])*emn_unit])
       if current_section==".PLACEMENT"  and section_counter>1 and fmod(section_counter,2)==0:
          place_item=[]
          place_item.append(emnrecords[2]) #Reference designator
          place_item.append(emnrecords[1]) #Component part number
          place_item.append(emnrecords[0]) #Package name
       if current_section==".PLACEMENT"  and section_counter>1 and fmod(section_counter,2)==1:
          place_item.append(float(emnrecords[0])*emn_unit) #X
          place_item.append(float(emnrecords[1])*emn_unit) #Y
          place_item.append(float(emnrecords[emn_version])) #Rotation
          place_item.append(emnrecords[emn_version+1]) #Side
          place_item.append(emnrecords[emn_version+2]) #Place Status
          FreeCAD.Console.PrintMessage(str(place_item)+"\n")
          placement.append(place_item)
   FreeCAD.Console.PrintMessage("\n".join(passed_sections)+"\n")
   FreeCAD.Console.PrintMessage("Proceed "+str(Process_board_outline(doc,board_outline,drills,board_thickness))+" outlines\n")
   placement.sort(key=lambda param: (param[IDF_sort],param[0]))
   process_emp(doc,filename,placement,board_thickness)
   place_steps(doc,placement,board_thickness)

def Process_board_outline(doc,board_outline,drills,board_thickness):
    """Process_board_outline(doc,board_outline,drills,board_thickness)-> number processed loops

        adds emn geometry from emn file"""
    vertex_index=-1; #presume no vertex
    lines=-1 #presume no lines
    out_shape=[]
    out_face=[]
    for point in board_outline:
       vertex=Base.Vector(point[1],point[2],0)
       vertex_index+=1
       if vertex_index==0:
          lines=point[0]
       elif lines==point[0]:
           if point[3]!=0 and point[3]!=360:
              out_shape.append(Part.Arc(prev_vertex,mid_point(prev_vertex,vertex,point[3]),vertex))
              FreeCAD.Console.PrintMessage("mid point "+str(mid_point)+"\n")
           elif point[3]==360:
              per_point=Per_point(prev_vertex,vertex)
              out_shape.append(Part.Arc(per_point,mid_point(per_point,vertex,point[3]/2),vertex))
              out_shape.append(Part.Arc(per_point,mid_point(per_point,vertex,-point[3]/2),vertex))
           else:
              out_shape.append(Part.LineSegment(prev_vertex,vertex))
       else:
          out_shape=Part.Shape(out_shape)
          out_shape=Part.Wire(out_shape.Edges)
          out_face.append(Part.Face(out_shape))
          out_shape=[]
          vertex_index=0
          lines=point[0]
       prev_vertex=vertex
    if lines!=-1:
      out_shape=Part.Shape(out_shape)
      out_shape=Part.Wire(out_shape.Edges)
      out_face.append(Part.Face(out_shape))
      outline=out_face[0]
      FreeCAD.Console.PrintMessage("Added outline\n")
      if len(out_face)>1:
        for otl_cut in out_face[1: ]:
          outline=outline.cut(otl_cut)
          FreeCAD.Console.PrintMessage("Cutting shape inside outline\n")
      for drill in drills:
        FreeCAD.Console.PrintMessage("Cutting hole inside outline\n")
        out_shape=Part.makeCircle(drill[0]/2, Base.Vector(drill[1],drill[2],0))
        out_shape=Part.Wire(out_shape.Edges)
        outline=outline.cut(Part.Face(out_shape))
      doc_outline=doc.addObject("Part::Feature","Board_outline")
      doc_outline.Shape=outline.extrude(Base.Vector(0,0,-board_thickness))
      grp=doc.addObject("App::DocumentObjectGroup", "Board_Geoms")
      grp.addObject(doc_outline)
      doc.Board_outline.ViewObject.ShapeColor=(0.0, 0.5, 0.0, 0.0)
    return lines+1

def mid_point(prev_vertex,vertex,angle):
    """mid_point(prev_vertex,vertex,angle)-> mid_vertex

       returns mid point on arc of angle between prev_vertex and vertex"""
    angle=radians(angle/2)
    basic_angle=atan2(vertex.y-prev_vertex.y,vertex.x-prev_vertex.x)-pi/2
    shift=(1-cos(angle))*hypot(vertex.y-prev_vertex.y,vertex.x-prev_vertex.x)/2/sin(angle)
    midpoint=Base.Vector((vertex.x+prev_vertex.x)/2+shift*cos(basic_angle),(vertex.y+prev_vertex.y)/2+shift*sin(basic_angle),0)
    return midpoint

def split_records(line_record):
    """split_records(line_record)-> list of strings(records)

       standard separator list separator is space, records containing encapsulated by " """
    split_result=[]
    quote_pos=line_record.find('"')
    while quote_pos!=-1:
       if quote_pos>0:
          split_result.extend(line_record[ :quote_pos].split())
          line_record=line_record[quote_pos: ]
          quote_pos=line_record.find('"',1)
       else:
          quote_pos=line_record.find('"',1)
       if quote_pos!=-1:
          split_result.append(line_record[ :quote_pos+1])
          line_record=line_record[quote_pos+1: ]
       else:
          split_result.append(line_record)
          line_record=""
       quote_pos=line_record.find('"')
    split_result.extend(line_record.split())
    return split_result

def process_emp(doc,filename,placement,board_thickness):
   """process_emp(doc,filename,placement,board_thickness) -> place components from emn file to board"""
   filename=filename.partition(".emn")[0]+".emp"
   empfile=pyopen(filename, "r")
   emp_unit=1.0 #presume millimeter like emn unit
   emp_version=2 #presume emn_version 2
   comp_height=0 #presume 0 part height
   comp_outline=[] #no part outline
   comp_GeometryName="" # no geometry name
   comp_PartNumber="" # no Part Number
   comp_height=0 # no Comp Height
   emplines=empfile.readlines()
   empfile.close()
   passed_sections=[]
   current_section=""
   section_counter=0
   comps=[]
   for empline in emplines:
     emprecords=split_records(empline)
     if len( emprecords )==0 : continue
     if len( emprecords[0] )>4 and emprecords[0][0:4]==".END":
        passed_sections.append(current_section)
        current_section=""
        if comp_PartNumber!="":
          if comp_height==0:
            comp_height=0.1
          comps.append((comp_PartNumber,[Process_comp_outline(doc,comp_outline,comp_height),comp_GeometryName]))
          comp_PartNumber=""
          comp_outline=[]
     elif emprecords[0][0]==".":
        current_section=emprecords[0]
        section_counter=0
     section_counter+=1
     if current_section==".HEADER"  and section_counter==2:
        emp_version=int(float(emprecords[1]))
        FreeCAD.Console.PrintMessage("Emp version: "+emprecords[1]+"\n")
     if (current_section==".ELECTRICAL" or current_section==".MECHANICAL") and section_counter==2 and emprecords[2]=="THOU":
        emp_unit=0.0254
     if (current_section==".ELECTRICAL" or current_section==".MECHANICAL") and section_counter==2 and emprecords[2]=="MM":
        emp_unit=1
     if (current_section==".ELECTRICAL" or current_section==".MECHANICAL") and section_counter==2:
        comp_outline=[] #no part outline
        comp_GeometryName=emprecords[0] # geometry name
        comp_PartNumber=emprecords[1] # Part Number
        comp_height=emp_unit*float(emprecords[3]) # Comp Height
     if (current_section==".ELECTRICAL" or current_section==".MECHANICAL") and section_counter>2:
        comp_outline.append([float(emprecords[1])*emp_unit,float(emprecords[2])*emp_unit,float(emprecords[3])]) #add point of outline
   FreeCAD.Console.PrintMessage("\n".join(passed_sections)+"\n")
   #Write file with list of footprint
   if IDF_diag==1:
     empfile=pyopen(IDF_diag_path+"/footprint.lst", "w")
     for compx in comps:
       empfile.writelines(str(compx[1][1])+"\n")
     empfile.close()
   #End section of list footprint
   comps=dict(comps)
   grp=doc.addObject("App::DocumentObjectGroup", "EMP Models")
   for place_item in placement:
     if place_item[1] in comps:
       doc_comp=doc.addObject("Part::Feature",place_item[0])
       FreeCAD.Console.PrintMessage("Adding EMP model "+str(place_item[0])+"\n")
       doc_comp.Shape=comps[place_item[1]][0]
       doc_comp.ViewObject.DisplayMode=EmpDisplayMode
       z_pos=0
       rotateY=0
       if place_item[6]=='BOTTOM':
          rotateY=pi
          z_pos=-board_thickness
       placmnt=Base.Placement(Base.Vector(place_item[3],place_item[4],z_pos),toQuaternion(rotateY,place_item[5]*pi/180,0))
       doc_comp.Placement=placmnt
       grp.addObject(doc_comp)
   return 1

def Process_comp_outline(doc,comp_outline,comp_height):
    """Process_comp_outline(doc,comp_outline,comp_height)->part shape
       Create solid component shape base on its outline"""
    vertex_index=-1; #presume no vertex
    out_shape=[]
    if comp_outline==[]:  #force 0.2mm circle shape for components without place outline definition
       comp_outline.append([0.0,0.0,0.0])
       comp_outline.append([0.1,0.0,360.0])
    for point in comp_outline:
       vertex=Base.Vector(point[0],point[1],0)
       vertex_index+=1
       if vertex_index>0:
         if point[2]!=0 and point[2]!=360:
            out_shape.append(Part.Arc(prev_vertex,mid_point(prev_vertex,vertex,point[2]),vertex))
            FreeCAD.Console.PrintMessage("mid point "+str(mid_point)+"\n")
         elif point[2]==360:
            per_point=Per_point(prev_vertex,vertex)
            out_shape.append(Part.Arc(per_point,mid_point(per_point,vertex,point[2]/2),vertex))
            out_shape.append(Part.Arc(per_point,mid_point(per_point,vertex,-point[2]/2),vertex))
         else:
            out_shape.append(Part.LineSegment(prev_vertex,vertex))
       prev_vertex=vertex
    out_shape=Part.Shape(out_shape)
    out_shape=Part.Wire(out_shape.Edges)
    out_shape=Part.Face(out_shape)
    out_shape=out_shape.extrude(Base.Vector(0,0,comp_height))
    return out_shape

def place_steps(doc,placement,board_thickness):
    """ place_steps(doc,placement,board_thickness)->place step models on board

        list of models and path to step files is set at start of this script
                 model_tab_filename= "" &   step_path="" """
    model_file=pyopen(model_tab_filename, "r")
    model_lines=model_file.readlines()
    model_file.close()
    model_dict=[]
    if IDF_diag==1:
        model_file=pyopen(IDF_diag_path+"/missing_models.lst", "w")
    keys=[]
    step_dict=[]
    for model_line in model_lines:
        model_records=split_records(model_line)
        if len(model_records)>1 and model_records[0] and not model_records[0] in keys:
           keys.append(model_records[0])
           model_dict.append((str(model_records[0]).replace('"',''),str(model_records[1]).replace('"','')))
    model_dict=dict(model_dict)
    validkeys=filter(lambda x:x in  [place_item[2] for place_item in placement], model_dict.keys())
    FreeCAD.Console.PrintMessage("Step models to be loaded for footprints: "+str(validkeys)+"\n")
    grp=doc.addObject("App::DocumentObjectGroup", "Step Lib")
    for validkey in validkeys:
         ImportGui.insert(step_path+model_dict[validkey],FreeCAD.ActiveDocument.Name)
         impPart=FreeCAD.ActiveDocument.ActiveObject
         impPart.ViewObject.Visibility=0
         impPart.Label=validkey
         grp.addObject(impPart)
         step_dict.append((validkey,impPart))
         FreeCAD.Console.PrintMessage("Reading step file "+str(model_dict[validkey])+" for footprint "+str(validkey)+"\n")
    step_dict=dict(step_dict)
    grp=doc.addObject("App::DocumentObjectGroup", "Step Models")
    for place_item in placement:
      if place_item[2] in step_dict:
        step_model=doc.addObject("Part::Feature",place_item[0]+"_s")
        FreeCAD.Console.PrintMessage("Adding STEP model "+str(place_item[0])+"\n")
        step_model.Shape=step_dict[place_item[2]].Shape
        step_model.ViewObject.DiffuseColor=step_dict[place_item[2]].ViewObject.DiffuseColor
        z_pos=0
        rotateY=0
        if place_item[6]=='BOTTOM':
           rotateY=pi
           z_pos=-board_thickness
        placmnt=Base.Placement(Base.Vector(place_item[3],place_item[4],z_pos),toQuaternion(rotateY,place_item[5]*pi/180,0))
        step_model.Placement=placmnt
        grp.addObject(step_model)
      else:
        if IDF_diag==1:
            model_file.writelines(str(place_item[0])+" "+str(place_item[2])+"\n")
            model_file.close()

def toQuaternion(heading, attitude,bank): # rotation heading=around Y, attitude =around Z,  bank attitude =around X
    """toQuaternion(heading, attitude,bank)->FreeCAD.Base.Rotation(Quternion)"""
    c1 = cos(heading/2)
    s1 = sin(heading/2)
    c2 = cos(attitude/2)
    s2 = sin(attitude/2)
    c3 = cos(bank/2)
    s3 = sin(bank/2)
    c1c2 = c1*c2
    s1s2 = s1*s2
    w = c1c2*c3 - s1s2*s3
    x = c1c2*s3 + s1s2*c3
    y = s1*c2*c3 + c1*s2*s3
    z = c1*s2*c3 - s1*c2*s3
    return  FreeCAD.Base.Rotation(x,y,z,w)

def Per_point(prev_vertex,vertex):
    """Per_point(center,vertex)->per point

       returns opposite perimeter point of circle"""
    perpoint=Base.Vector(2*prev_vertex.x-vertex.x,2*prev_vertex.y-vertex.y,0)
    return perpoint


