#
package provide processwindir 0.22.05.24

# <20220524.1532.29>
#
# Here is a routine to eliminate symbolic links in a path
# In the *nix world pwd is the way. In MSW it is not so simple
#
# proc getPath {dir} {
#   set parts [file split [file norm $dir]]
#   set name {}
#   foreach part $parts {
#     set name [file join $name $part]
#     if {[catch {file readlink $name} an] != 0} {continue}
#     set name [getPath $an]
#   }
#   return $name
# }

# This is code to read a windows directory, as accessed by 'ssh'
# As a remote cpu we do not have access to no standard routines such as 'twapi'

# Windows get dir info using windows DIR command
# integer min and max for a list...
namespace eval processWinDir {
  variable splitDate {([^/.,\-]+)([/ .,\-]+)([^/.,\-]+)([/ .,\-]+)([^/. ,\-]+) *$}
  variable lastDirLn
}

# The init code sets up the char positions in the 'dir' strings
# The issue with the date is that it depends on what how the  user has set the
# time and date format. Here we check and set up the conversion format
# and the width to expect in the dir listing.

# The date! The date is the first thing on the line. It starts in character 0
# and extends as needed with no spaces. We can use regexp to pull it out.
#
# The next entry on a line is the time which contains the hour:min followed by
# a space and either AM or PM.

# Here is a routine to return the proper format for 'clock scan'

proc ::processWinDir::clockScanFormat {date} {
  variable splitdate
  upvar timeFormat timeFormat
  upvar timeAlmost timeAlmost
  upvar Pm   Pm
  upvar fmt1 fmt1
  upvar fmt2 fmt2
  upvar fmt3 fmt3
  
  
  if {$timeFormat == {}} {
    # Put together a format to convert the 'dir' time to seconds
    # first we need to find it. We allow the date to have spaces in it.
    # The date delimiter (month day year) is one of "/.-,". The month
    # may be spelled out (in the current language). The year may be 2
    # or 4 digits.
    # Hours/mins, on the other hand will always have a ":" between them.

    # We have three ways to figure the format:
    # 1. By inspection of the first date, i.e.:
    #    If year is 4 digits, it is unique, also the year, if first will be 4 digits
    #    Also, if year is first, the format should be 'yr mon day'
    #    If the year is not first (4-digits) it will be last.
    #    If an element is not an integer, it is the month abbreviation
    #    If an element is > 31, it is a day or a two digit year
    # 2. By inspection of a system formated date (echo %date%):
    #    Compare to tcl clock time (requires all digits to be unique),
    #     If so, we have it.
    # 3. Changeing the system date (date\r new-date) and seeing what changed
    #    In this method we need to pick a date with unique digits. This is
    #    a bit invasive and we must reset the proper date ASAP
    #    
    lassign {} p1 s1 p2 s2 p3
    set splitDate {(^[^/.,\-]+)([/ .,\-]+)([^/.,\-]+)([/ .,\-]+)([^/. ,\-]+) *$}
    if {![regexp $splitDate $date -> p1 s1 p2 s2 p3]} {
      return -code error "Expected $date to split to 3 parts but got \"$p1 $p2 $p3\""
    }

    # METHOD 1.:
    frputs -> p1 s1 p2 s2 p3 date timeAlmost
	     
    if {!$timeAlmost} {
      if {[string is digit $p1] && [string length $p1] == 4} {
	# This must be the year
	set fmt1 %Y
	if {[string is digit $p2]} {
	  set fmt2 %N
	} else {
	  set fmt2 %b
	}
	set fmt3 %d
	# 
      } else {
	if {[string length $p3] == 4} {
	  set fmt3 %Y
	} else {
	  set fmt3 %y
	}
	# year is done. What is left is the fight over month or day first
	if {![string is digit $p2]} {
	  # not an int, must be month
	  set fmt1 %d
	  set fmt2 %b
	} elseif {![string is digit $p1]} {
	  set fmt1 %b
	  set fmt2 %d
	} else {
	  set timeAlmost 1
	}
      }
    }
    if {$timeAlmost} {
      frputs p1 p2
      set timeAlmost 0
      # 
      if {[string index $p2 0] != 0 && $p2 > 12} {
	set fmt2 %d
	set fmt1 %N
	frputs STILL p2 fmt1 fmt2 fmt3 timeFormat
      } elseif {[string index $p1 0] != 0 && $p1 > 12} {
	set fmt2 %N
	set fmt1 %d
      } else {
	set timeAlmost 1
	return
      }
    }
    if {!$timeAlmost} {
      set timeFormat [string trim "$fmt1$s1$fmt2$s2$fmt3 %H:%M [list {*}$Pm]"]
    }
  }
}

# The isDate function tries to verify that the submitted string is a date
# If so it returns true, else false

proc ::processWinDir::isDate {str} {
  set splitDate {([^/.,\-]+)([/ .,\-]+)([^/.,\-]+)([/ .,\-]+)([^/. ,\-]+) .*:[0-9][0-9] .*$}
  # first we need month, day, year which require 2 separators at least 2 digits
  # for each, i.e. 8 (+1 for a trailing space) characters.
  
  if {[string length $str] < 9} {return "L"}
  # a date will be followed by a space (but the separators may also be spaces)
  # try separating the date fields
  
  if {![regexp $splitDate $str -> p1 s1 p2 s2 p3]} {return "r"}
  # The two separators should be the same and at least two of the fields must be
  # numeric. The max size of a field is 4 and a size 4 field must be numeric.
  # digit fields must be either 1, 2 or 4 digits (do we ever encounter 1 digit fields?)
  frputs p1 p2 p3 s1 s2
  if {$s1 != $s2} {return "$s1 - >$s2<"}
  
  set notDigCount -1
  foreach fld "$p1 $p2 $p3" {
    if {[set slen [string length $fld]] > 4} {return 2}
    if {![set dig [string is digit $fld]] && [incr notDigCount]} {
      frputs fld notDigCount
      return 3}
    if {$slen == 4 && !$dig} {return 4}
    if {$dig && $slen ni {1 2 4}} {return 5}
  }
  # Seems to look like a date...
  return 1 
}



#
# We get a 'dir' command output which will have the following fields all aligned:
# 'time (date hr:min)' 'type or size' 'account/user' 'file name [link]'
# Time always has a date in one of the above formats. The hr:min may be either
# on a 24 hour clock or it will end in AM or PM. These can change from time to
# time so we need to check each call.

# getDirLine manages the position (indx) in the incoming list of dirlines (dirList).
# It purges (skips) empty lines and eliminates duplicate lines.
# It also keep the maximum line length which is used to verify continuation lines.

proc ::processWinDir::getDirLine {} {
  upvar dirList   dirList
  upvar lnDirList lnDirList
  upvar indx      indx
  upvar skip      skip
  upvar maxLineLn maxLineLn
  upvar DUPLICATE DUPLICATE
  variable lastDirLn
  set skip 0
  set ent {}
  while {[incr indx] < $lnDirList && \
	     ([set ent [lindex $dirList $indx]] == {} || $ent == $lastDirLn)} {
    if {$ent == {}} {
      incr skip
    } else {
      incr DUPLICATE
    }
    frputs skip indx DUPLICATE lnDirList ent
  }
  frputs #2 indx skip ent lastDirLn
  set lastDirLn $ent
  if {[set lnLine [string length $ent]] >= $maxLineLn} {
    set maxLineLn $lnLine
  }
  frputs indx ent
  return $ent
}

proc ::processWinDir::getFullLine {} {
  ###[llength $testLn] > 3 &&
  # think of this as a dowhile ...
  upvar dirList   dirList
  upvar lnDirList lnDirList
  upvar indx      indx
  upvar skip      skip
  upvar maxLineLn maxLineLn
  upvar testLn    testLn
  upvar ent       ent
  upvar DUPLICATE DUPLICATE
  set maxToContinue 0
  while {1} {
    incr maxToContinue $maxLineLn
    set testLn [getDirLine]
    # These are the reasons that a line may not be a continuation
    # It is preceeded by a null line
    # The line it continues length is not exactly the max we have seen
    # It starts with a date
    # It has 5 or more blanks in it. (a directory with one file otherwise
    #                                 makes the summary line look like a continue)
    if {$skip == 0 &&\
	    [string length $ent] == $maxToContinue &&\
	    [isDate $testLn] != 1 &&\
	    ![string match {*     *} $testLn]} {
      append ent $testLn
      frputs EXTENDEDLINE ent testLn
      continue
    }
    break
  } ; # end of while {1}
}

proc ::processWinDir::processWinDirList {URL dirList dir} {
  # Once we find the timeformat we want to keep it for the duration
  # And allow other connections to differ.
  VFSglobal timeFormat pwd tmpdir
  variable lastDirLn
 
  #
  # We get the dirList split into a list of lines
  lassign {0 0 0 0 0 0 0 0 0} \
	  endDate allset endTime startFile  end dateF bf timeAlmost
  if {![info exists timeFormat] || [string index $timeFormat 0] != "%"} {
    set timeFormat {}
  }
  set Pm {}
  # Here we need to, possibly rebuild lines that MSW in its wisdom, broke into
  # two or more lines. 
  set lnDirList [llength $dirList]
  set maxLineLn 0
  set testLn {}
  while {[incr failSafe] < 3} {
    set lastDirLn {}
    set indx -1
    set notYet 1
    set atStart 0
    set result [list {listF a}]
    frputs AT-LINE-1 failSafe
    
    while {[set ent $testLn] != {} || [set ent [getDirLine]] != {}} {
      incr foo
      frputs notYet indx foo testLn ent
      if {$notYet} {
		if {![string match "*Directory of *"  $ent ]} {
		  set testLn {}
		  continue
		}
		# make sure this line is not continued (should have a nil line here)
		# yeah, but not always. Be more agressive
		set notYet 0
		getFullLine
		continue
      } ; # end of if {notYet...}
      # preamble done. The next line that starts with a space
	  # begins the postamble
      incr atStart

      # First correct any line breaks that were inserted because
	  # the line was too long
      ###[llength $testLn] > 3 &&
      getFullLine
      
      # The line should now be complete...
      
      frputs ent atStart allset endDate startFile timeFormat
      # set testLn {}
      
      # We make at most two scans over the dirList.
      # On the first we try try to find the start of the file names.
      # We look for two contiguous spaces scanning back from the end
      # of the line. If we end up prior to the 'account/user' field,
      # we assume the file name is to the right (depends on not
      # having "  " in a file name). If this is successful on the
      # first file, we only need do one scan, if not, we restart
	  # the scan once we do have success.

      # We collect info from the date field and try to finish a time format
      # by continuing to look at dates as we go. If we can not resolve this by
      # the end of the pass, more extream measures are taken (see below).
      
      # frputs dir dirList'
      # skip over what we don't need/want
      # frputs ent allset timeFormat startFile

      # All lines that get here until one with a blank as the first char.
      # are dir entry lines.
      if {[string index $ent 0] == " "} {
		# no more files. find the bytes free line
		# Lets cross check. We get the number of entries and use it
		# to verify that we got them all.
		if {[set la [string first "File" $ent]] != -1} {
		  set total [string trim [string range $ent 0 $la-1]]
		}
		if {[set la [string first " bytes free" $ent]] == -1} {continue}
		set fi [string last " " [string range $ent 0 $la-1]]
		lappend result [list df [formatDF [string range $ent $fi $la]]]
		if {[set la [string first "Dir" $ent]] != -1 && [info exists total]} {
		  incr total [string trim [string range $ent 0 $la-1]]
		  set found [llength $result]
		  incr found -2
		  if {$total != $found} {
			PopInfo "Found $found dir entries but MS says there are $total entries."
		  }
		}
		incr bf
		# all done...
		frputs bf
		break
      } ; # end of if {[string index... ]}
      
      # 'allset' gets set when we have figured out what the date looks like and
      # have all the column numbers needed to parse the entry
      
      if {!$allset} {
		if {$endDate == 0} {
		  set ln [string length $ent]
		  # Put together what we can of the column numbers
		  if {[set minMark [string first ":" $ent]] == -1} {
			return -code error "Could not find \":\" in \"$ent\""
		  }
		  set endDate [expr {$minMark - 3}]
		  set endTime [expr {$minMark + 2}]
		  if {[string match -nocase {* [ap]m *} \
				   [string range $ent $minMark $minMark+7]]} {
			while {![string match -nocase {[ap]}\
						 [string index $ent $minMark+[incr loc]]]} {}
			set endTime [expr {$minMark + $loc + 1}]
			set Pm " %P"
		  }
		  # After the date is the '<type>' or the size. After this is
		  # the 'account/user'
		  set startAcct $endTime
		  while {[string index $ent [incr startAcct]] == " " && $startAcct < $ln} {}
		  while {[string index $ent [incr startAcct]] != " " && $startAcct < $ln} {}
		  while {[string index $ent [incr startAcct]] == " " && $startAcct < $ln} {}
		  # Was that fun or what?
		  # Now we need to know where the file name starts. This can be messy
		  # as there may not be a space prior to the name and the 'account/user'
		  # will often have a space in it.
		  # 
		  # Try this...
		} ; # end of if {..endDate...}
		
		if {$startFile == 0} {
		  if {[set maybe [string last "  " $ent]] > $startAcct} {
			set startFile $maybe
			incr startFile 2
		  } elseif {[string match "* ." $ent]} {
			# This should work on dirs that have parents
			# (c: does not have a . or a ..)
			set startFile [string length $ent]
			incr startFile -1
		  } elseif {[set maybe [string first " $" $ent]] > $startAcct} {
			set startFile $maybe
			incr startFile
		  }
		} ; # end of if {..startFile...}
		
		frputs timeFormat endDate endTime
		if {$timeFormat == {}} {
		  clockScanFormat [string range $ent 0 $endDate]
		}
		frputs startFile
		# Here we determine if we have the resources to continue, i.e.:
		# the 'timeFormat' and the column address of the file names.
		# If both of these are true on the first line of the dir (first
		# with a file name) we can continue. If not, we restart the
		# foreach loop which should now complete.

		if {$startFile == 0 || $timeFormat == {}} {continue}
		incr allset
		# If this is still the first dir line, just continue, otherwise, restart
		if {$atStart != 1} {
		  frputs RESTART
		  break
		}
		frputs CONTINUE failsafe atStart
      } ; # end of if {allset}

      # This is the dictionary we are trying to build:
      # 
      #                 file        <files name>
      #                 type <one of: {d n ld ln}>  d->directory n->file l->link
      #                 -optional-
      #                 size        <file/dir size in bytes>
      #                 sec         <last modify time>
      #                 og          <owner/group string>
      #                 link        <link to string>
      #                 nlinks      <number of links> refers to hard links
      #                 flags       <a set of r/w flags per ls>
      
      # Any we don't know i.e. provide are defaulted by the caller

      set sizeType [string trim [string range $ent $endTime+1 $startAcct-1]]
	  # No trim on the file name (catch leading & trailing blanks 
      set file     [string range $ent $startFile end]
      set linkname {}
      set size 0
      if {[string index $sizeType 0] == "<" &&\
			  [string index $sizeType 1] != "D"} {
		set lnkLoc [string first " \[" $file]
		set linkname [regsub {^/\?\?/} [string range $file $lnkLoc+2 end-1] {}]
		set file [string range $file 0 $lnkLoc-1]
      }
      switch -exact -nocase $sizeType {
		"<dir>"      {set filetype d}
		"<symlinkd>"  -
		"<junction>" {set filetype ld}
		"<symlink>"  {set filetype ln}
		default    {
		  set filetype n
		  set size $sizeType
		  frputs file
		  if {[file extension $file] == ".lnk"} {
			frputs pwd tmpdir
			VFSssh::SSHget $URL [file join $pwd $file] $tmpdir
			if {[catch {LnkFileOld $tmpdir/$file to dir} out] == 0 && $out == 1} {
			  frputs to dir
		      set filetype [expr {$dir ? {wld} : {wl}}]
		       set linkname $to
		     }	  
		   }
		}
      } ; # end of switch
      
      set owner    [string trim [string range $ent $startAcct $startFile-1]]
      # frputs " TIME [string range $ent 0 $endTime] " timeFormat
      if {$timeFormat ni {0 {}} &&\
			  [catch {clock scan [string range $ent 0 $endTime] \
						  -format $timeFormat} fileTime] != 0} {
		set fileTime 0
      } else {
		# frputs timeFormat "[string range $ent 0 $endTime] "
      }
      lappend result [list file $file\
						  link [regsub {^/\?\?/} $linkname {}]\
						  og $owner \
						  type $filetype\
						  size $size\
						  sec  $fileTime]
	  
    } ; # end of while ..set ent $testln... (line processing loop)
	if {$bf != 0} {break}
	
	# If we now have the time format, just let the while take us
	# back to the start
	if {$timeFormat == {}} {
	  # If we are here and still don't have a timeFormat, it is time for
	  # drastic action.  All we need is to know the day from the month.
	  # If today's day and month are not the same, we can just ask for the date
	  # and see which it is.

	  lassign [clock format [clock seconds] -format "%N %d %y"] month day year
	  VFScommand $URL "date\r$month-13-$year"
	  set err {}
	  set an [VFScommand $URL "date\r$month-$day-$year"]
	  set dat [lindex [split $an \n] 0 end]
	  frputs an dat
	  if {[catch {clockScanFormat $dat} err] != 0 || $timeFormat == {}} {
		PopError "Failed to configure the time format.\
                ($err)\nAll file times will be 0"
	  }
	  set timeFormat 0
	  set allset 1
	  frputs
	} ; # if {timeFormat...}
  } ;   # end of while {[set..]}
  frputs result'
  
  frputs failSafe DUPLICATE
  return $result
  return -code error "what the"
}


