#
# -*- tcl -*- 
# <20240131.1929.01>

# This routine attempts to find an intelligent window size and location
# for a text window.
# It works with config vars: geometery,max geometry,min geometry,location as
# well as one of geometry,textviewer or geometry,qedit (which is passed in as fixed)

# geometry,max has alternative forms:
#         option                      key         number
# percent of "available workarea"  :  availablewa number
# percent of "main window"         :  mainw       number
# fixed                            :  fixed       number

# These are combined with an "x" between them for width x height
# spaces are optional and only the first letter counts, i.e.
# a50 or a50xmainw75 or {mainw 50 x avail 50} all work.
# If height is missing, the width is used for both
# "fixed" may have a number, if not the passed in "fixed" is used.

# geometry,min is similar:
#         option                      key         number
# percent of actual maximum line length: percent  number
# percent of actual number of lines    : percent  number
# actual maximun line length plus "n"  : addspace number
# actual number of lines plus "n"      : addlines number
# fixed                                : fixed    number
#
# if 'number' is missing in the 'fixed' case the value from the
# parameter 'fixed' is used. In the other cases the number will
# restrained to be at least 10.
# 
# Same combining rules. We treat "addspace" and "addlines" the
# same, like wise "percent" line length or number depends on
# which side of the "x" it is on.

# For location we use geometry,location which has for X and Y:
#
# centered on the main window               : cmw     
# centered in the "available monitor area"  : cama
# on left/above  "available monitor area"   : left
# on right/below  "available monitor area"  : right
# random{fixed}                             : random
# fixed(set by config(geometry,textviewer)) : fixed
# These are combined as a list i.e. {random random} or {left above}
#                       
# Parameters:
#
# fixed size (usually one of config(geometry,textviewer) or config(geometry,qedit)
#         note that this is used only if one of the min/max numbers is missing.
# w     text-window   (should have the text and font already set up)
# args  optional params to override the config(geometry,*) and other values.

#       These should be doublets of form: one of {max min location}
#       followed by values as above. Recognized options:
#       key      value
#       max      same as geometry,max above
#       min      same as geometry,min above
#       increase <bool>  if true only increase the window size
#                        (assumes user changed it)         
#       location same as geometry,location plus these options:
#                nil (i.e. "") do not change
#                bound   keep window in its current monitor adjust if not wholly within.
#       Again this is a list for x & y, i.e. {"" bound}. If the second entry is missing
#       the first is repeated.

#
# The result is the window is sized and positioned.
# This may be recalled repeatedly, such as for a window which is still being given
# new text. We keep track of the windows and do not move the location for windows
# if this is not the first call.
#
# Notes on wm and winfo:
# On MSW wm geo * and winfo geo * give the same information. The sizes are the
# window size with out title and border. The location reported is the top left
# of the window with borders and title.

# On X systems wm geo * and winfo geo * give different locations,
# wm gives location including the borders and title (same as MSW),
# however, winfo gives location with out the title
# and borders.
proc intelWinSize {fixed w args} {
  global config
  upvar #0 ::displays::monitors(width)  frameW
  upvar #0 ::displays::monitors(height) frameH
  upvar #0 ::displays::activeWindows    activeWindows
  frputs fixed w args
  # first a bit of book keeping
  foreach win [array names activeWindows] {
    if {[winfo exists $win]} {continue}
    unset activeWindows($win)
  }
  set NotFirstCall [info exists activeWindows($w)]
  incr activeWindows($w)
  
  set maxConfig      $config(geometry,max)
  set minConfig      $config(geometry,min)
  set locationConfig $config(geometry,location)

  # set overrides
  foreach {what val} $args {
    # frputs what val args
    if {$what ni {max min location increase}} {
      return -code error "intelWinSize override \"$what\" not one of {max min location}"
    }
    if {[llength $val] == 1} {
      set val [list $val $val]
    }
    set ${what}Config $val
  }
  # first lets collect the desired goodies. Set flags for percent etc. as we go
  set wtop [winfo toplevel $w]
  set noBorders [wm override $wtop]
  lassign [split [set masterGeo [wm geo $wtop]] x+] curW curH curX curY
  # we need the font info to translate line/chars to pixals
  lassign [split $fixed x+] fixedW fixedH fixedX fixedY
  foreach WH {W H X Y} {
    frputs WH
    if {![string is integer [set fixed$WH]] } {
      frputs WH fixedX fixedY
      return -code error "intelWinSize: $fixed is not a reasonable size value."
    }
  }
  lassign [split [wm geo .] x+] mainW mainH mainX mainY
  if {!$noBorders} {
    # if borders, adjust to true size
    incr mainW -$frameW
    incr mainH -$frameH
  }
  set baseLoc [expr {$NotFirstCall || $locationConfig in {"" bound} ?\
			 [list $curX $curY] : [winfo pointerxy .]}]
   
  lassign [::displays::get {*}$baseLoc] monXmin monXmax monYmin monYmax
  set monW [expr {$monXmax - $monXmin}]
  set monH [expr {$monYmax - $monYmin}]
  # a regexp to help us parse this
  # we allow f or fix or fixed and still find the w/h separator "x"
  set regMax { *([amf])(?:ix)?[^0-9x]*([0-9]+|[^x]*)x* *([amf]?)[^0-9]*([0-9]*)}
  set regMin { *([apf])(?:ix)?[^0-9x]*([0-9]+|[^x]*)x* *([apf]?)[^0-9]*([0-9]*)}

  set font [$w cget -font]
  set fontW [font measure $font -displayof $w "0"]
  set fontH [font metrics $font -displayof $w -linespace]
  # increaseConfig is a fudge we add just in case. It is currently 1 char wide 1 line high
  set increaseConfig "$fontW $fontH"
  if {![regexp $regMax $maxConfig match wkey wnum hkey hnum]||\
	  $wkey != "f" && $wnum == {} ||\
	  $hkey ni {f {}} && $hnum == {}} {
    return -code error "$maxConfig is not a valid maximum size spec"
  }
  # set the defaults...
  if {$wnum == {}} {
    set wnum $fixedW
  }
  if {$hkey == {}} {
    set hkey $wkey
    if {$hkey == "f"} {
      set hnum $fixedH
    } else {
      set hnum $wnum
    }
  }
   frputs wkey hkey wnum hnum
  set fixedFlag 0
  frputs wkey hkey fontW fontH font
  foreach wh {w h} WH {W H} {
    # let paranoia ...
    if {[set ${wh}num] == {} || [set ${wh}num] <= 9} {
      set ${wh}num 10
    }
    switch -exact [set ${wh}key] {
      a {
	set max$WH [expr {([set ${wh}num] * [set mon$WH]) / 100}]
      }      
      m { 
 	set max$WH [expr {([set ${wh}num] * [set main$WH]) / 100}]
      }
      f {
	# incr fixedFlag
	set max$WH [expr {[set ${wh}num] * [set font$WH]}]
      }
    }
  }
  
  if {![regexp $regMin $minConfig match Wkey minW Hkey minH]||\
	  $Wkey != "f" && $minW == {} ||\
	  $Hkey ni {f {}} && $minH == {}} {
    return -code error "$minConfig is not a valid minimum size spec"
  }
  # set the defaults...
  if {$minW == {}} {
    set minW $fixedW
  }
  if {$Hkey == {}} {
    set Hkey $Wkey
  }
  if {$minH == {}} {
    set minH [expr {$Hkey == "f" ? $fixedH : $minW}]
  }
  # frputs Wkey Hkey minW minH
  set count [set percent 0]
  foreach  WH {W H} {
    if {[set ${WH}key] == "f"} {
      incr fixedFlag
    }
  }
  # frputs minW minH maxW maxH Wkey Hkey wkey hkey
  set overheadH [set overheadW 0]
  while {($overheadH <= 0 || $overheadW <= 0) && [incr failsafe] < 100} {
    foreach WH {W H} wh {w h} {
      set overhead$WH [expr {[winfo req$wh $wtop]\
				 - [winfo req$wh $w]\
				 + ($noBorders ? 0 : [set frame$WH]) }]
    }
    update idletasks
  }
  frputs overheadW frameW overheadW noBorders
  # frputs failsafe maxW overheadW fontW
  # Do we need to count lines?
  if {$fixedFlag != 2} {
    # not fixed, so we have to count...
    # realWaitForIdle
    # first compute the maxW/maxH for the text window
    # this will exclude any scroll and border sizes
    # we use winfo here to avoid the grided window issue
    set textMaxW [expr {($maxW - $overheadW) / $fontW}]
    #
    set actH 0
    set actW 10
    set lineCount 0
    update idletasks
    foreach line [split [$w get 0.0 end] \n] {
      incr lineCount
      set lLen [string length $line]
      incr actH [expr {($lLen / $textMaxW) + 1}]
      set actW [expr {max($actW,$lLen)}]
    }
    # Since we don't look for wrap points (i.e. wrap 'word') assume we may be off
    # and add a line if there is wrapping.
    if {$lineCount < $actH} {
      incr actH
    }
  }
  # frputs actW actH fontW fontH font overheadW overheadH lineCount textMaxW
  
  # translate to pixals and add overhead
  foreach WH {W H} {
    # frputs ${WH}key min$WH act$WH font$WH overhead$WH
    set min$WH [expr {([set ${WH}key] == "a" ? ([set min$WH] + [set act$WH]) : \
			   ([set ${WH}key] == "p" ? (([set min$WH] * [set act$WH]) / 100) :\
				[set min$WH])) * [set font$WH]}]
    if {[set ${WH}key] != "f"} {
      incr min$WH [set overhead$WH]
    }
  }
  # We now have min and max from what ever calculation
  lassign $increaseConfig incW incH
  foreach WH {W H} {
     frputs WH max$WH min$WH frame$WH cur$WH inc$WH
    set $WH [expr {min([set max$WH], [set min$WH]) + ($noBorders ? 0 : [set frame$WH])}]
    set $WH [expr {[set $WH] < [set cur$WH] && [set inc$WH] ? [set cur$WH] : [set $WH]}]
  }
  # frputs W H
  # We have the size, now where to put it...
  # This is governed by config(geometry,location) which has the following options:
  #  centered on the main window           : cmw
  #  centered in the "monitor workarea"    : cama
  #  on left/above  "monitor workarea"     : left  or above
  #  on right/below  "monitor workarea"    : right or below
  #  tiled (starts at fixed Y)             : tiled
  #  random{fixed}                         : random
  #  fixed(by passed fixed parm)           : fixed

  foreach req $locationConfig xy {X Y}  wh {W H} {
    # adjustW and H are the actual w & h including the parenting frame
    set adjust$wh [expr {$noBorders ? [set $wh] : [set $wh]+ [set frame$wh]}]
    if {$NotFirstCall} {
      # On second and subsequent calls we treat the current location as fixed.
      # All we need to do is make sure W,H stay on the monitor.
      set ${xy}Loc [set cur$xy]
      set $wh [expr {[set adjust$wh] + [set cur$xy] > [set mon${xy}max] ? \
			 [set mon${xy}max] - [set cur$xy] -[set frame$wh] : [set $wh]}]
      # frputs $wh adjust$wh cur$xy mon${xy}max frame$wh
    } else {
      switch -glob $req {
	{}  {
	  set ${xy}Loc [set cur$wh]
	}
	bo* {
	  set ${xy}Loc [expr {[set adjust$wh] + [set cur$wh] > [set mon${xy}max] ? \
				  [set mon${xy}max] - [set adjust$wh] : \
				  [set cur$wh]}]
	}
	cm* {
	  set ${xy}Loc [expr {[set main$xy] + ([set main$wh] - [set adjust$wh]) / 2}]
	}
	ca* {
	  set ${xy}Loc [expr {([set mon${xy}min] + [set mon${xy}max] - [set adjust$wh]) / 2}]
	}
	be*  -
	ri* {
	  set ${xy}Loc [expr {[set mon${xy}max] - [set adjust$wh]}]	
	}

	a*  -
	l*  {
	  set ${xy}Loc [set mon${xy}min]
	}
	ra* {
	  set ${xy}Loc [expr {[set mon${xy}min]\
				  + int(([set mon${xy}max]\
					     - [set adjust$wh]\
					     - [set mon${xy}min]) * rand())}]
	}
	f*  {
	  # Assume the fixed location is relative to a monitor at 0,0
	  # We need to adjust it to fit if it throws us out the the display area
	  if {[set fixed$xy] == {}} {
	    set fixed$xy 0
	  }
	  set ${xy}Loc [expr {[set mon${xy}min] +
			      min([set fixed$xy],
				  ([set mon${xy}max] - \
				       [set mon${xy}min] - [set adjust$wh]))}]
	}
	t* {return -code error "intelWinSize: Location \"tiled\" not implemented yet"}
	default {return -code error \
		     "intelWinSize: location \"$req\" not one of\
                   {cmw cama left above below tiled random fixed}"}
      }
      # Now make sure we are still within the monitor...
      # frputs ${xy}Loc mon${xy}min $wh adjust$wh
      set ${xy}Loc [expr {[set ${xy}Loc] < [set mon${xy}min] ? [set mon${xy}min] :\
			      [set ${xy}Loc] + [set adjust$wh] > [set mon${xy}max] ? \
			      [set mon${xy}max] - [set adjust$wh] : [set ${xy}Loc]}]
    }
  }

  # And finally, if this is a grid-ed window, convert to grid units
  if {[$w cget -setgrid]} {
    set W [expr {$W * $fontW}]
    set H [expr {$H * $fontH}]
  }
  
  # set rt [list [expr {min($maxW,max($minW,$actW))}] [expr {min($maxH,max($minH,$actH))}]]
  # frputs w wtop W H minH maxH actH minW maxW actW rt XLoc YLoc monXmin\
      monXmax monYmin monYmax
  # With out this wait, at least on windows, the location is ignored (but size is not)
  realWaitForIdle
  catch {wm geo $wtop ${W}x${H}+${XLoc}+$YLoc}
  return [list $W $H $XLoc $YLoc]
}

# Here we try to provide an intelligent window positioning function.
# There are two problems we are trying to address:
# 1.) tcl/tk does NOT id displays as opposed to screens. This means
#     that it will put a new window anywhere on the screen (the screen
#     is composed of all the displays, by definition).
# 2.) We really would like to have some control over where new windows
#     are placed. Since we don't really want to put them all in the
#     same place, we will try to come up with some controlled randomization.
#
# Since tcl/tk does not provide a display breakdown, we will get that from
# OS dependent code. We assume that that code has built a list of display
# info consisting of a sublist for each display of the min x, max x, min y
# and max y for each display (this is "::displays::monitors(monitors)")


# This function "::displays::get" takes a x,y location and returns the list
# entry from the display list that contains or is closest to this x,y.
# We also rearrange the list so that the found display is at the head of the list.
#


# An attempt is made to get the display area closest to the mouse. If
# the mouse is in a docked window and near more than one area, well...
# For areas outside of the display area we use the sum of the squares of
# the x & y distances.
#
proc ::displays::get {x y} {
  upvar #0 ::displays::monitors(monitors) displayList
  if {[llength $displayList] != 1} {
    # set mIndx 10
    set mDist 100000000
    set index -1
    set mIndex 0
    foreach display $displayList {
      incr index
      foreach {min max} $display xy {x y} {
	set under [expr {$min - [set $xy]}]
	set over  [expr {[set $xy] - $max}]
	# If both under and over are negative we are in a display area (set to zero)
	# else one will be positive, use its square.
	set rs$xy [expr {$under <= 0 && $over < 0 ? 0 : (($under > 0 ? $under : $over) << 1)}]
      }
      if {[set rs [expr {$rsx + $rsy}]] == 0} {
	# Both x & y are in the current display area, return it.
	set mIndex $index
	break
      }
      # If this area is closer than the last one, set it up.
      if {$rs < $mDist} {
	set mDist $rs
	set mIndex $index
      }
    }
    # If the chosen area is not first in the list, make it so.
    if {$mIndex != 0} {
      set displayList [list [lindex $displayList $mIndex] \
			   {*}[lrange $displayList 0 $mIndex-1] \
			   {*}[lrange $displayList $mIndex+1 end]]
    }
  }
  if {[lindex $displayList 0] != {}} {
    return [lindex $displayList 0]
  }
  # fail safe
  return [list 0 [expr {[winfo screenwidth .] - 1}] \
	      0 [expr {[winfo screenheight .] - 1}]]

}
    
