#!/usr/bin/python3

import re
from os.path import isfile,splitext,dirname,samefile,isabs,normpath,relpath
from os import environ
import argparse
import sys



fn="MopHeadHolderRing.scad"
fn2="/home/shared/0OpenSCAD/shadlib.scad"


fnarr=[]

farr=[]
# structure of array:
# - filename
# - original line
# - cooked line
# - used
# - level?

indent=4

builtin=['difference','union','intersection','translate','rotate','if','else','for']

verb=False

def note(s,out=False,err=True):
  if verb:
    if out: farr_addstr(0,'// '+s,True)
    if err: sys.stderr.write(s+'\n')

def err(s,out=False,err=True):
  if out: farr_addstr(0,'// '+s,True)
  if err: sys.stderr.write(s+'\n')


def isfloat(s):
  try: f=float(s);return True
  except: return False


def getset(a):
#  return a
  s=[]
  for x in a:
    if x=='': continue
    if isfloat(x): continue
    if x in builtin: continue
    s.append(x)
  return s

# $OPENSCADPATH
#defaultlibpath=['/home/shared/0OpenSCAD','/home/shared/0OpenSCAD/libdotSCAD']
defaultlibpath=[]
libpath=[] # TODO: add path of the file itself, for relative references
basepath=''


def fmtdir(fn):
  if args.abspath: return normpath(fn)
  return relpath(fn,basepath)


def setlibpath(fn,libdir=[]):
  #print(libdir)
  iswin=False
  global libpath
  try:
    envpath=environ['OPENSCADPATH']
    # crude windows autodetect, to avoid importing of platform and using platform.system()=='Windows'
    if ';' in envpath: envpath=envpath.split(';');iswin=True
    else: envpath=envpath.split(':')
  except:
    envpath=[]
  for x in range(0,len(libdir)):
    if isabs(libdir[x])=='/': continue
    libdir[x]=basepath+'/'+libdir[x]
  libpath=envpath+libdir+defaultlibpath
  if verb: note('using path: '+str([basepath]+libpath))



def findfile(fn,basefn=''):
  if isfile(fn): return fn # valid file
  xpath=[dirname(basefn)]+libpath
  if args.tracefile:
    err('...base='+fmtdir(basefn),out=True)
    err('...path='+str(xpath),out=True)
  for x in xpath:
    if x=='': continue
    fn2=x+'/'+fn
    if isfile(fn2):
      if args.tracefile: err('...FOUND: '+fmtdir(fn2),out=True)
      return fn2
    if args.tracefile: err('...tried '+fmtdir(fn2),out=True)
  return fn


def farr_addstr(fn,s,used):
  farr.append({'fn':fn,'used':used,'mod':False,'id':[],'uses':[],'cook':s,'raw':s})



def getfileparams(fni,fnn,lev=1):
  from pathlib import Path
  from datetime import datetime
  time=Path(fni).stat().st_mtime
  size=Path(fni).stat().st_size
  pref='// '+(' '*(lev-1)*indent)
  if lev>0: pref+='  '
  farr_addstr(fnn,pref+'file='+fmtdir(fni),True)
  farr_addstr(fnn,pref+'size='+str(size),True)
  farr_addstr(fnn,pref+'modtime='+datetime.utcfromtimestamp(time).strftime('%Y-%m-%d %H:%M:%S'),True)
  if lev==0: farr_addstr(fnn,'',True)


def readfile(fni,basefn='',lev=0,fnrel=''):
  global fnarr,farr

  fn=findfile(fni,basefn)
  if fn in fnarr:
    err('include: CIRCULAR REFERENCE, skipping: '+fmtdir(fn))
    return
  fnarr.append(fn)
  fnn=len(fnarr)-1
  try:
   with open(fn,'r') as f:
    note('include: USING FILE: '+fmtdir(fn))
    if args.tracefile: farr_addstr(fnn,'// USING FILE: '+fmtdir(fn),True)
    if not args.nofiledet: getfileparams(fn,fnn,lev)
    if args.treefile: print( (' '*lev*indent) + fmtdir(fn) )
    a=f.read().replace('\r','').split('\n')
    for s in a:
      ss=s.replace(' ','').replace('>','<')
      if ss[:8]=='include<' or  ss[:4]=='use<':
        if args.treefile and args.treefileinc: print( (' '*(lev*indent+indent)) +s) # show use/include command
        ffn=ss.split('<')[1]
        if args.tracefile: err('// ['+str(lev)+']USING FILE: '+fmtdir(fn))
        farr_addstr(fnn,'',True)
        farr_addstr(fnn,'//'+' '*lev*indent+' INCLUDE: '+s,True)
        readfile(ffn,basefn=fn,lev=lev+1)
        farr_addstr(fnn,'//'+' '*lev*indent+' INCLUDE END: '+s,True)
        farr_addstr(fnn,'',True)
        continue
#      d={'fn':fnn,'used':False,'mod':False,'id':[],'uses':[],'cook':s,'raw':s}
#      farr.append(d)
      else:
        farr_addstr(fnn,s,False)
  except:
    if args.treefile or args.tracefile: print(' '*(lev*indent+indent)+'INCLUDE ERROR: CANNOT FIND/READ FILE: '+fni)
    else: err('INCLUDE ERROR: CANNOT FIND/READ FILE: '+fmtdir(fni),out=True)



def cleanfiles():
  for x in farr: # set initial used
    if x['fn']==0: x['used']=True
  for x in farr: # remove inline comments, inline comment blocks
    n=x['cook'].find('//')
    if n>=0: x['cook']=x['cook'][:n]
    x['cook']=re.sub('(/\*.*\*/)','',x['cook'])
  isblock=0
  for x in farr: # remove comment blocks /* ... */
    if isblock>0:
      n=x['cook'].find('*/')
      if n>=0: x['cook']=x['cook'][n+2:];isblock-=1
    n=x['cook'].find('/*')
    if n>=0: x['cook']=x['cook'][:n];isblock+=1;continue
    if isblock: x['cook']=''
  for x in farr: # set initial used
    x['cook']=x['cook'].strip().replace('$','SSS')


def getmodules():
  ismod=False
  modname=''
  lev=0
  nlev=0
  for x in farr: # set initial used
    s=x['cook']
    if not ismod:
      if s.find('module')>=0:
        ismod=True
        x['mod']=True
        nlev=1
        a=re.split(r'\W+',s)
        modname=a[1]
        x['id']=modname
#        x['id']=re.split(r'\W+',s)[1]
    if ismod:
      for i in s:
        if i=='{': lev+=1
        elif i=='}': lev-=1
        if i==')': nlev=0
      x['mod']=ismod
      if lev==0 and nlev==0: ismod=False

    x['uses']=getset(re.split(r'\W+',s))

    try:
      if x['uses'][0]=='module':
#        if modname=='': modname=x['uses'][1]
#        x['id']=modname
        x['uses']=x['uses'][2:]
    except: pass

    if x['mod']: x['mod']=modname
    else: modname=''
#    if ismod:

  ismod=False
  for x in farr: # scan for functions
    s=x['cook']
    if s.find('function')>=0:
      a=re.split(r'\W+',s)
      modname=a[1]
      ismod=True
    if ismod:
      x['id']=modname
      x['mod']=ismod
      if s.find(';')>=0: ismod=False;modname=''

  for x in farr: # scan for variables
    if x['mod']!=False: continue
    s=x['cook']
    ss=s.replace(' ','')
    if '=' in ss:
      a=ss.split('=')
      x['id']=a[0]
      x['uses']=x['uses'][1:]
#    x['cook']=ss


usedid=set()

def scanused():
  n=0
  global usedid
  for x in farr:
    if not x['used']: continue
    for y in x['uses']:
      usedid.add(y)
  for x in farr:
    if x['used']: continue
    if len(x['id'])==0: s=x['mod']
    else: s=x['id']
    if s in usedid:
      x['used']=True
      n=n+1
      #print(x['raw'])
  return n


def processfile(fn):
  readfile(fn)
  #readfile(fn2)
  cleanfiles()
  getmodules()

  while True:
    #print('...scanning...')
    n=scanused()
    if n==0: break


def printresultfile(fn):
  if verb: note('writing output to file: '+fn)
  f=open(fn,'w')
  for x in farr:
    if x['used']: f.write(x['raw']+"\n")
  f.close()


def printresult(fn=''):
  if args.dryrun: return
  if args.treefile: return
  if fn=='' or fn=='-':
    for x in farr:
      if x['used']: print(x['raw'])
  else: printresultfile(fn)



def getfilepath(fn):
  pass

def getmergedfilename(fn):
  name,ext=splitext(fn)
  return name+'.merged'+ext


def ifstr(cond,s):
  if(cond): return s
  return ''


def cmdlineparse():
  parser = argparse.ArgumentParser(prog='mergescad.py',description='merges together OpenSCAD source files',epilog='')
  #if dofiles: 
  parser.add_argument('fni', metavar='file.scad', default='', help = 'input filename, OpenSCAD')
#  parser.add_argument('-d' ,'--device',      type=str, default='', help='/dev/input device')
#  parser.add_argument('-dn','--devname',     type=str, default=PHYSDEVNAME, help='fraction of device name, default="'+PHYSDEVNAME+'"')
#  parser.add_argument('-da','--devaddr',     type=str, default=PHYSDEV, help='device phys address or suffix (eg. 4, 2.1, 3.2.3), match to last slash')
#  parser.add_argument('-dp','--devprefix',   type=str, default=PHYSDEVPREFIX, help='device address prefix, default="'+PHYSDEVPREFIX+'"')
#  parser.add_argument('-A' ,'--matchall',    action='store_const', default=False, const=True, help='match all devices')
#  parser.add_argument('-l' ,'--list',        action='store_const', default=False, const=True, help='list HID devices present')
#  parser.add_argument('-L' ,'--listevents',  action='store_const', default=False, const=True, help='list events for matching device(s)')
#  parser.add_argument('-E' ,'--showevents',  action='store_const', default=False, const=True, help='show detected event names and source devices, ignore config')
#  parser.add_argument('-M' ,'--forcemods',   action='store_const', default=False, const=True, help='like -E, but force reading of config/modifier')
#  parser.add_argument('-C' ,'--listcommands',action='store_const', default=False, const=True, help='list set commands')
#  parser.add_argument('-q' ,'--quiet',       action='store_const', default=False, const=True, help='suppress most output')
#  parser.add_argument('-v' ,'--verbose',     action='store_const', default=verb, const=True, help='print more details')
  parser.add_argument('-q' ,'--quiet',       action='store_const', default=verb, const=True, help='print fewer details')
  parser.add_argument('-o' ,'--tofile',      action='store_const', default=False, const=True, help='output to filename.merged.scad instead to stdout')
  parser.add_argument('-F' ,'--nofiledet',   action='store_const', default=False, const=True, help='do not include date/size/name of source files to comments')
  parser.add_argument('-O' ,'--outfile',     type=str, default='', help='output to specified filename instead to stdout')
  parser.add_argument('-L' ,'--libdir',      action='append', default=[], nargs='?', const=True, help='add library directory (multiple possible)')
  parser.add_argument('-t' ,'--tracefile',   action='store_const', default=False, const=True, help='show directories searched for includes')
  parser.add_argument('-T' ,'--treefile',    action='store_const', default=False, const=True, help='show directories searched for includes, tree form')
  parser.add_argument('-I' ,'--treefileinc', action='store_const', default=False, const=True, help='show includes in tree form, forces -T')
  parser.add_argument('-a' ,'--abspath',     action='store_const', default=False, const=True, help='show absolute paths, otherwise relative to input file')
  parser.add_argument('-A' ,'--listarr',     action='store_const', default=False, const=True, help='list internal array (debug)')
  parser.add_argument('-d' ,'--dryrun',      action='store_const', default=False, const=True, help='dry run, errors output only')
#  parser.add_argument('-T' ,'--stricttime',  action='store_const', default=False, const=True, help='do not try VLONG-LONG-normal if longer command not found')
#  parser.add_argument('-c' ,'--config',      type=str, default='', help='keys config file, default='+str(config_files))
#  parser.add_argument('--printdefaultconfig',action='store_const', default=False, const=True, help='print default config file content')
#  parser.add_argument('-F' ,'--showallevents',action='store_const', default=False, const=True, help='show ALL detected events (not just keypresses), ignore config; -q to suppress SYN_REPORT')
  return parser


parser=cmdlineparse()
args = parser.parse_args()
verb=not args.quiet # avoid retyping it all around

fno=''
if args.tofile: fno=getmergedfilename(args.fni)
if args.outfile!='': fno=args.outfile
if args.treefileinc: args.treefile=True

#print(dirname(args.fni))
#print(libpath)
#sys.exit(0)

#libpath=[dirname(args.fni)]+libpath
basepath=dirname(args.fni)
setlibpath(args.fni,args.libdir)
if not args.treefile: processfile(args.fni)

if args.listarr:
  for x in farr: print(x)

elif fno!='':
  try:
    if samefile(args.fni,fno):
      err('ERR: input and output files are the same, refusing to overwrite input')
      sys.exit(1)
  except: pass
  printresult(fno)

else:
  printresult()
#print(args)
#print(usedid)




