#! /usr/bin/env tclsh
# -*- tcl -*-
# <20240519.1838.08>

# This little program syncs a destination directory with a source dir with
# the following features...
#
# Parm 1 is the source dir or file name
# Parm 2 is the destination (must be dir or file as parm 1)
# Parm 3 is an optional regexp or glob exp of files to exclude
#        we default this to exclude "*~" more than one pattern may
#        be included in the command line as additional parameters
#    Exclusions may also be put in the rc file (~/.syncWithSimrc):

# .syncWithSimrc format:
# we source ~/.syncWithSimrc to allow options.
# the format should be either:
# append filterOps " <diff option>"
#  (this line may be repeated for addition options)
# or
# set filterOps {}
#  {this eliminates " -aur --exclude=<command line filters>"
# and then the above or
# lappend filters <raw filter strings> where just the filter and not
#                  the exclude is in the the string(s)
# we also source [file dir $argv0]/syncExcludeList
#
# Note that 'filterOps' are fed directly to diff while
#           'filter'    will have '--exclude=' prepended to each element.
# Also a "-test" in filterOps will set test mode where all the actions
# except the actual file modifications are done.
#
# Only files that are changed are copied.
# We will use "diff" to find these.
# If the destination is a sym link to an underlying file, the underlying file
# will be updated (and the symlink preserved) (This is why rsync won't work)
#
# We flag (i.e. report) any new (i.e. un matched) files found in the source
# but do not attempt to copy them. (We might want to do this later...)

# We use diff to find the files and dos2unix to move the new versions.
# dos2unix does two things for us:
# a.) converts any dos (\r\n) line endings to unix (\n).
# b.) traces down the sym links (using the -F option)

# Tests we do to prevent disaster:
#   0) source and destination must both exist and both be either dirs or files
#   1) the tail of source and destination must be the same
#   2) the normalized source and destination must differ.
#   3) we expect (require) that the destination be related to the current
#      working dir (why??)
#   4) file by file we require the source to be newer than the destination.
#
# This routine may be refered to in a user button as follows (my usage):

# set config(userButton,sync) {
#                          com {/f/filerunner/Makefiles/syncWithSim.tcl %s %s}
#                          dir 1
#                          prior 1
#                          viewout 1
#                          numparms 2
#                          }
# One more thing. We save all the old files in a backup directory
# ([file dir $file]/bkupdir
# This will be the directory the file is linked from if the file is a link.

# The back up dir name is:
set backUpDir "BackUps~"
#
# Lets do it...
#
package require fileutil
puts "IN0 here argv0 is $argv0"

set exception "** EXCEPTION ** EXCEPTION ** EXCEPTION ** EXCEPTION ** EXCEPTION **"
set endException "** End of EXCEPTION ** End of EXCEPTION ** End of EXCEPTION ** "
proc backup {file} {
  if {![file exists $file]} {return}
  set afile $file
  if {[file type $file] == "link"} {
    set afile [file norm [file join [file dir $file] [file readlink $file]]]
  }
  if {![file exists [set bkupDir [file dir $afile]/$::backUpDir]]} {
    puts "BK1 making dir '$bkupDir'"
    file mkdir $bkupDir
  } else {
    if {![file isdir $bkupDir]} {
      puts "BK2 Backup directory '$bkupDir' is not a directory!!!"
      puts "BK3 We quit!!!"
      exit 1
    }
    set n 0
    set fileTail [file tail $afile]
    while {[file exists $bkupDir/$fileTail~[incr n]~]} {}
    puts "BK4 moving $file to $bkupDir/$fileTail~$n~"
    file rename $file  $bkupDir/$fileTail~$n~
    # file mtime $bkupDir/$fileTail~$n~ [clock seconds]
  }
}
lassign {} exceptions issues updated movedf

proc doCopy {sorFile desFile} {
  if {[catch {lindex [::fileutil::fileType $sorFile] 0} fileTyp] != 0} {
    puts "CO1 Lost file '$sorFile' somehow. fileutil::fileType failed with: $fileTyp"
    return
  }
  set tail [file tail $desFile]
  set dir  [file tail [file dir $desFile]]
  if {$fileTyp ni {"text" "directory" "empty"}} {
    lappend ::exceptions $dir/$tail
    puts "CO2 $::exception"
    puts "CO3 File type is '$fileTyp' not 'text or empty ...' We don't do these!:\
           \nCO3 Des: $desFile\nCO3 Sor: $sorFile\n\nCO3 Copy not done!"
    puts "CO4 $::endException"
    return
  }
  set t [expr {$::test == -1 ? {} : {TEST}}]
  puts "\nCO5 Copying $sorFile $t\nto \n$desFile $t"
  if {$::test != -1} {
  puts "CO5.5 TEST but: Would do it!! TEST" ; return
    return
  }
  set mes {}
  if {[file isdir $sorFile]} {
    file mkdir $desFile
  } else {
    backup $desFile
    set r [catch [list exec dos2unix -F -n $sorFile $desFile] mes]
    if {$mes != {}} {
      puts "CO6 From dos2unix: $mes"
    }
      lappend ::updated $dir/$tail
    if {$r == 0} {
    } else {
    #  lappend ::issues  [list $dir/$tail $mes]
    }
  }
}
# Get the source & dest.
set filterOps "-aur  -q"

set filters [lassign $argv source dest]
if {$filters == {}} {
  set filters {*~}
}
# puts "here 2"
if {[file exists [file dir $argv0]/syncExcludeList]} {
  puts "IN1 sourceing [file dir $argv0]/syncExcludeList"
  source [file dir $argv0]/syncExcludeList
  puts "IN2 test defined? [info exists ::test]"
}
# If we have an rc file, apply it
if {[file exists "~/.syncWithSimrc"]} {
  puts "In3 sourcing [file norm ~/.syncWithSimrc]"
  source "~/.syncWithSimrc"
}
# build filters options
foreach ent $filters {
  append filterOps " --exclude=$ent"
}
set tes [string first " -test" $filterOps]
if {$tes != -1} {
  set filterOps [string replace $filterOps $test $test+5]
} 
if {![info exists ::test] || $::test == -1 } {
  set ::test $tes
}
  puts "IN4 test is $::test"
if {$::test != -1} {
  puts "IN5 TEST TEST TEST TEST TEST"
}
puts "IN6 filter $filterOps \nIN6 source $source \nIN6 dest $dest \nIN6 pwd [pwd]"
# Lets jump up and down and scream if we find file names that aren't.
set source [file norm $source]
set dest   [file norm $dest  ]

foreach file [list $source $dest] {
  if {[file exists $file]} {continue}
  # Bad things....
  puts "\nIN7 $file is not a file!\
        \nIN8 We quit."
  exit 1
}
# So we know we have two files. Lets reguire that the tails are the same...
if {[file tail $source] != [file tail $dest]} {
  puts "\nIN9 Source and destination 'tails' mismatch ([file tail $source] != [file tail $dest])!\
   \nIN10 This will not fly.\nWe quit!"
  exit 1
}
if {[file isdir $source] ^ [file isdir $dest]} {
  puts "\nIN11 Both files must be either directories or files!\nWe quit!"
  #exit 1
}
if {$source == $dest} {
  puts "\nIN12 Source and destination must differ!\nWe quit"
  exit 1
}
# puts "Post rc file options $filterOps"
# Just as a further sanity check, lets require that the pwd is related to the
# destination and not the source.  If both are related, require that the destination
# be a shorter path.
set splitS [file split $source]
set splitD [file split $dest  ]
set splitP [file split [pwd]  ]

lassign {0 0 0} splitStoP splitDtoP

foreach toP {splitS splitD} {
  while {[lindex [set $toP] [set ${toP}toP]] == [lindex $splitP [set ${toP}toP]]} {
    incr ${toP}toP
  }
}
# puts "Source $source \nDest $dest\npwd [pwd]"
# puts "Common dest parts $splitDtoP common source parts $splitStoP"
if {$splitDtoP < $splitStoP || ($splitDtoP == $splitStoP && [llength $splitS] < [llength $splitD])} {
  # Looks like we got dest and souce switched
  puts "\nIN13 Looks like the files to be modified are not related to the pwd!\
       \nIN14 We don't allow this! \nWe quit!"
 # exit 1
}
# if {[llength $splitS] < [llength $splitD]} {
#   puts "\nDestination path longer than source!\nWe assume this is in error!\nWe quit!"
#   exit 1
# }

puts "IN15  diff $filterOps $dest $source"
# exit 1

catch [list exec diff {*}$filterOps $dest $source] difResult

set lines [split $difResult \n]
# Ok, now we try and pull out the file names....
# puts $difResult
# Ok, here we find the file names to be modified (destination with "---", source with "+++")
# These must come in pairs, the "---" first and the "+++" on the very next line.
set lineNo 0
set lnLines [llength $lines]
puts ""
puts "IN16 Processing $lnLines lines of diff."
puts ""
#
set destLn [string length $dest]
incr destLn

foreach line $lines {
  if {[string match {Only in *} $line]} {continue}

  # The file line looks like: "Files "<destinationfile>" and "<sourcefile>" differ"
  # lets pick this apart in a way that will not mess up spaces in a file name
  # Since 'dest' and 'source' contain the paths, we know that the remainder should be
  # exactly the same.
  
  set destLoc [string first $dest $line]
  # if this is not found we are in trouble...
  if {$destLoc < 4 || $destLoc > 10} {
    if {[lindex $line 0] ni {File Only} } {continue}
    error "INFE1 $dest not found early in $line, should be > 4 and < 10 is '$destLoc'"
  }
  set endOfFileName [string first { and } $line $destLoc+$destLn]
  # puts "destLoc =$destLoc $destLn $endOfFileName"
  set fileName [string range $line $destLoc+$destLn $endOfFileName-1]
  # puts "file name =$fileName< from line=$line"
  
    
  set desFile [file join $dest $fileName]
  set sorFile [file join $source $fileName]

  if {[file mtime $desFile] > [file mtime $sorFile]} {
    puts "INFE2 $exception"
    puts "INFE3 Destination newer than Source:\nINFE3 Des: $desFile\nINFE3 Sor: $sorFile\n\nINFE3 This is not allowed, copy not done!"
    puts "INFE4 $endException"
    continue
  }
  doCopy $sorFile $desFile
}

# Look for files Only in
puts ""
puts "PH21 Looking for Only in files"
puts ""
set newFiles {}
set newTails {}
set masterlines $lines
while {$lines != {}} {
  set newlines {}
  foreach line $lines {
    if {[string match "Only in $source*" $line]} {
      puts "PH22 $line"
      set desFile $dest
      append desFile [string range $line [string length "Only in $source"] end]
      set desFile [regsub {: } $desFile {/}]
      set sorFile [string range $line [string length "Only in "] end]
      set sorFile [regsub {: } $sorFile {/}]
      puts "PH23 possible: $sorFile \nPH23 to \nPH23 $desFile"
      puts "\nPH24 Copying $sorFile \nPH23 to \nPH23 $desFile"
      lappend newFiles $desFile
      lappend newTails [file tail $desFile]
      if {[file isdir $sorFile]} {
	file mkdir $desFile
	# Diff will not have descended here, so set up a new diff to get the
	# new dir contents.
	set difResult2 {}
	catch [list exec diff {*}$filterOps $desFile $sorFile] difResult2
	set newlines [concat $newlines [split $difResult2 \n]]
	append difResult "\nNEW DIFF RUN TO PICK UP $desFile FILES\n"
	append difResult $difResult2
      } else {
	doCopy $sorFile $desFile
      }
      incr found
    }
  }
  set lines $newlines
  # puts ">>>>>>$lines<<<<<<<<<<<<"
  puts "PH31 PICKING UP NEW DIRS"
  lappend  masterlines $newlines
}
if {![info exists found]} {
  puts "PH32 No 'Only in' lines found"
} else {
  puts "\nPH41 Checking for moved files\n"
  # Lets look for files found only in source and also in dest.
  # This would usually indicate a file has been moved
  foreach line $masterlines {
    if {[string match "Only in $dest*" $line]} {
      set desFile [string range $line [string length "Only in "] end]
      set desFile [regsub {: } $desFile {/}]
      if {[set indx [lsearch -exact $newTails [file tail $desFile]]] != -1} {
	if {[incr moved] == 1} {
	  puts "PH42 Possibly moved files **********************************************\n"
	}
	puts "PH43 It appears that file $desFile has been moved to [lindex $newFiles $indx]"
	
	lappend movedf [file tail [file dir $desFile]]/[file tail $desFile]
      }
    }
  }
  if {[info exists moved]} {
    puts "\nPH44 End of possibly moved files ***********************************************"
  }
}

puts "PHE1 Summary ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"

if {[llength $updated] > 0} {
  puts "PHE2 Updated files...."
  foreach fl $updated {
    puts "PHE3 $fl"
  }
}

if {[llength $movedf] > 0} {
  puts "PHE4 Moved files...."
  foreach fl $movedf {
    puts "$fl"
  }
}
if {[llength $issues] > 0} {
  puts "PHE5 Files with issues..."
  foreach fl $issues {
    puts "$fl"
  }
}
if {[llength $exceptions] > 0} {
  puts "PHE6 Files that need updateing but we don't know how...."
  foreach fl $exceptions {
    puts "PHE7 $fl"
  }
}
puts ""
# the following is for debug only...
puts "PHE8 All the diffs follow."
puts "\nPHE8 $difResult"
puts "\nPHE9 done"



  
