# secop driver

namespace eval secop {} {
}

proc stdConfig::secop {{shownUnits ALL}} {
  variable node
  variable name
  
  set timeout 5
  controller secop timeout=$timeout
  prop commerror secop::errorscript
  prop connection_lost 0
  prop check secop::check
  prop write secop::write
  prop startcmd *IDN?
  prop end_fast 0
  prop secopPath /$name
  prop timeout $timeout
  prop active 0
  prop shownUnits $shownUnits
  
  set node $node/tasks
  prop start secop::start

  # the pollperiod does not really matter
  pollperiod 5 5
  obj SECoP -text wr

  prop read secop::readmsg
  prop test secop::test
  prop check secop::checkmsg
  prop write secop::writemsg
  prop cmd ""

  variable ctrl
  variable path
  hsetprop /sics/$ctrl ignore_no_response 1

}

proc secop::errorscript {} {
  if {[string match {ASCERR: no response*} [sct result]]} {
    sct send ping
    return secop::update
  }
  sct connection_lost 1
  error [sct result]
}

proc secop::checkmsg {} {
#  variable MQ[sct]
#  upvar 0 MQ[sct] mq
#  if {![info exists mq]} {
#    # create message queue
#    set mq [list]
#  }
#  if {[llength $mq] > 0} {
#    set next [lindex $mq 0]
#    set mq [lrange $mq 1 end]
#    lappend mq [sct target]
#    sct target $next
#  }
  
  # interrupt pending wait for update
  [sct controller] timeout 0.001
  # without the following line "0.001" appears on client, why?
  return ""
}

proc secop::writemsg {} {
  # we grabbed access, so restore normal timeout
  [sct controller] timeout [sct timeout]
  sct send [sct target]
  return secop::update
}

proc secop::readmsg {} {
  if {[sct connection_lost]} {
    sct connection_lost 0
    return [secop::start]
  }
  return [secop::get]
}

proc secop::test {} {
  clientput test
  return idle
}

proc secop::check {} {
  set validator [silent {} sct validator]
  eval $validator
  lassign [split [hinfo [sct]] ","] type
  if {$type eq "text"} {
    set msg "change [sct secopar] \"[sct target]\""
  } else {
    set msg "change [sct secopar] [sct target]"
  }
  [sct controller] que [sct secopPath] write [list secop::queuedwrite $msg]
}

proc secop::queuedwrite {msg} {
  sct changed [DoubleTime]
  # send message on /secop node
  sct send $msg
  return secop::update
}

proc secop::write {} {
  # dummy write
  # clientput "secop::write [sct] [hvali [sct]]"
  return idle
}

proc secop::get {} {
  sct send ""
  return secop::update
}

proc secop::check_range {{low None} {high None}} {
  [sct controller] timeout 0.001
  if {$low ne "None" && [sct target] < $low} {
    error "value [sct target] must be >= $low"
  }
  if {$high ne "None" && [sct target] > $high} {
    error "value [sct target] must be <= $high"
  }
}

proc secop::check_enum {} {
  [sct controller] timeout 0.001
}

proc secop::check_length {{low None} {high None}} {
  if {$low ne "None" && [string length [sct target]] < $low} {
    error "value [sct target] must not be shorter than $low"
  }
  if {$high ne "None" && [string length [sct target]] > $high} {
    error "value [sct target] must not be longer than $high"
  }
}

proc secop::make_par {secopar desc {kind ""}} {
  set path [topath $secopar [silent "" dict get $desc group]]
  array set props $desc
  set validator_args [lassign [silent "" set props(datatype)] secoptype0]
  if {$secoptype0 eq "tuple" && [string match *:status $secopar]} {
    lassign $validator_args elements
    make_par0 text ${path}_text $secopar $desc
    hsetprop ${path}_text width 24
    set validator_args [lassign [lindex $elements 0] secoptype]
    set status_node 1
  } else {
    set secoptype $secoptype0
    set status_node 0
  }
  switch -- $secoptype {
    double {set type float}
    int - enum {set type int}
    string {
      set type text
      # can not use SICS drivable for string
      set kind ""
    }
    bool {set type text}
    none {set type none}
    default {
      clientput "unknown type for $secopar (use text): $secoptype ($props(datatype))"
      set type text
    }
  }
  make_par0 $type $path $secopar $desc $kind
  hsetprop $path secoptype $secoptype0
  if {$status_node} {
    hsetprop $path nonewline 1
  }
  switch -- $secoptype {
    enum {
      set enumprop [list]
      set wid 8
      foreach {name value} [lindex $validator_args 0] {
        lappend enumprop "$name=$value"
        set wid [expr max($wid,[string length $name])]
      }
      hsetprop $path enum [join $enumprop ,]
      if {$wid > 8} {
        hsetprop $path width $wid
      }
    }
    bool {
      hsetprop $path enum 1
      hsetprop $path validator secop::check_enum
    }
    double - int {
      hsetprop $path validator [concat secop::check_range $validator_args]
    }
    string {
      hsetprop $path width 16
      hsetprop $path validator [concat secop::check_range $validator_args]
    }
  }
  return $path
}

proc secop::make_par0 {type path secopar desc {kind std}} {
  array set props $desc
  # clientput "$path $desc"
  set readonly [silent 0 set props(readonly)]
  if {$readonly} {
    set priv internal
  } else {
    set priv user
  }
  if {[silent "" hinfo $path] ne ""} {
    error "$path exists already"
  }
  lassign [split $path /] nul obj par
  if {$par eq ""} {
    if {$kind eq "driv"} {
      dynsctdriveobj $obj float user SECoP [sct controller]
      hfactory $path link $obj    
      hsetprop $obj checklimits secop::checklimits
      hsetprop $obj halt secop::halt
      # allow start without run:
      hsetprop $obj check secop::checklimits
      hsetprop $obj write secop::complete_run
      set readonly 0
      hsetprop $obj sicscommand "run $obj"
    } else {
      # clientput "OBJ $obj $type"
      dynsicsobj $obj SECoP $priv $type
      hfactory $path link $obj
    }
    hsetprop $path group $obj
    hsetprop $path objectPath $path
    hsetprop /sics/[sct controller] p_$secopar:value $path
  } else {
    if {$par eq "status"} {
      set path /$obj/s_status
    }
    # clientput "PAR $path $type"
    hfactory $path plain $priv $type
    if {[info exists props(visibility)]} {
      if {$props(visibility) >= 3} {
        hsetprop $path visible false      
      }
    }
  }
  hsetprop $path secopar $secopar
  hsetprop /sics/[sct controller] p_$secopar $path
  if {!$readonly} {
    [sct controller] write $path
  } else {
    [sct controller] connect $path
  }
  logsetup $path 1
  if {[info exists props(value)]} {
    clientput "VALUE in descr $path"
    if {[catch {hupdate /$path $props(value)} msg]} {
      clientput $msg
    }
    unset props(value)
  }
  set fmtunit ""
  if {[info exists props(unit)]} {
    set fmtunit [format { [%s]} $props(unit)]
    if {$par eq "" || $par eq "target"} {
      if {[sct shownUnits] eq "ALL" || [lsearch [sct shownUnits] $props(unit)] >= 0} {
        GraphAdd $path $props(unit) [join [lrange [split $path /] 1 end] .]
      }
    }
  }
  if {[info exists props(description)]} {
    hsetprop $path help "$props(description)$fmtunit"
    unset props(description)
  }
  foreach {prop item} [array get props] {
    hsetprop $path s_$prop $item
  }
}

proc secop::make_cmd {secopar desc {first 0}} {
  array set props $desc
  set path [topath $secopar [silent "" dict get $desc group]]
  lassign [split $secopar :] obj cmd
  lassign [silent "" set props(arguments)] secoptype validator
  if {$secoptype eq ""} {
    $obj makescriptfunc $cmd "secop::check_cmd [sct secopPath] $secopar" user
    hsetprop $path newline $first
    hsetprop $path secopar $secopar
    hsetprop $path sicscommand "$obj $cmd"
    if {[info exists props(visibility)]} {
      if {$props(visibility) >= 3} {
        hsetprop $path visible false      
      }
    }
  } else {
    if {[llength $secoptype] == 1} {
      set secoptype [lindex $secoptype 0]
    }
    clientput "MAKE_CMD $secoptype"
    dict set desc datatype $secoptype
    make_par $secopar $desc
    lassign $secoptype maintype
    if {$maintype eq "double" || $maintype eq "int" || $maintype eq "bool"} {
      hsetprop $path check "secop::check_cmd_num [sct secopPath] $secopar"
    } else {
      hsetprop $path check "secop::check_cmd_text [sct secopPath] $secopar"
    }
  }
}

proc secop::check_cmd {secopPath secopar} {
  hset $secopPath "do $secopar"
}

proc secop::check_cmd_num {secopPath secopar} {
  hset $secopPath [format {do %s [%.15g]} $secopar [sct target]]
  sct update [sct target]
}

proc secop::check_cmd_text {secopPath secopar} {
  hset $secopPath [format {do %s ["%s"]} $secopar [sct target]]
  sct update [sct target]
}

proc secop::make_module {obj desc} {
  if {[obj_list exists $obj]} {
    clientput "$obj exists already"
    return
  }
  obj_list makeitem $obj /$obj

  array unset modprop
  set parlist [list]
  set cmdlist [list]
  set pardict [dict create]
  foreach {key item} $desc {
    switch $key {
      parameters {
        set parlist $item
      }
      commands {
        set cmdlist $item
      }
      default {
        set modprop($key) $item
      }
    }
  }
  if {[dict exists $parlist value]} {
    set value [dict get $parlist value]
    dict unset parlist value
  } else {
    set value [dict create datatype none]
  }
  set classes [silent "" set modprop(interface_class)]
  if {[string match "* Drivable *" " $classes "]} {
    set path [make_par $obj $value driv]
  } else {
    set path [make_par $obj $value]
  }
  if {[info exists modprop(visibility)] && $modprop(visibility) >= 3} {
    hdelprop $path group
  }
  foreach {prop val} [array get modprop] {
    hsetprop $obj sm_$prop $val
  }
  device_layout makeitem /$obj [silent 0 set modprop(layoutpos)]

  set groups [dict create]
  foreach {parname pardesc} [concat $parlist $cmdlist] {
    if {[dict exists $pardesc group]} {
      dict set groups [dict get $pardesc group] 1
    } 
  }
  foreach g [dict keys $groups] {
    clientput "GROUP $g"
    hfactory $obj/$g plain user none
    hsetprop $obj/$g group "group $g"
  }
  foreach {parname pardesc} $parlist {
    make_par $obj:$parname $pardesc
  }
  # first commands with arguments
  foreach {parname pardesc} $cmdlist {
    if {[dict get $pardesc arguments] ne ""} {
      make_cmd $obj:$parname $pardesc 1
    }
  }
  # then commands without arguments, on one line
  set first 1
  foreach {parname pardesc} $cmdlist {
    if {[dict get $pardesc arguments] eq ""} {
      make_cmd $obj:$parname $pardesc $first
      set first 0
    }
  }
}

proc secop::make_node {desc} {
  array unset nodeprop
  set modlist [list]
  foreach {key item} $desc {
    switch $key {
      modules {
        set modlist $item
      }
      default {
        set nodeprop($key) $item
      }
    }
  }
  foreach {modname moddesc} $modlist {
    make_module $modname $moddesc
  }
  foreach {prop val} [array get nodeprop] {
    sct sn_$prop $val
  }
  sort_layout
}

proc secop::topath {secopar {pargroup ""}} {
  lassign [split [string tolower $secopar] :] module parameter
  if {$parameter eq "value" || $parameter eq ""} {
    return "/$module"
  }
  if {$parameter eq "status"} {
    set parameter s_status
  }
  if {[string match {_*} $parameter]} {
    set parameter [string range $parameter 1 end]
  }
  if {$pargroup ne ""} {
    return "/$module/$pargroup/$parameter"
  }
  return "/$module/$parameter"
}

proc secop::update {} {
  set sent_message [silent 0 sct sent_message]
  set message_to_client ""
    
  lassign [sct result] messagetype par val
  set path [silent "" hgetpropval /sics/[sct controller] p_$par]
  lassign [split $par :] obj
  switch $messagetype {
    update - changed {
      #if {[sct] ne "/secop"} {
      #  clientput "[sct] is not /secop, why?"
      #}
      #if {![sct active]} {
      #  clientput [sct result]
      #}
      if {$messagetype eq "changed"} {
        if {[string match *:target $par]} {
          hsetprop /$obj writestatus done
        }
        hsetprop $path changed 0
        if {[lrange $sent_message 0 1] eq [list change $par]} {
          set message_to_client [sct result]
        }
        # clientput "CH $path [sct result]"
      } else {
        if {[DoubleTime] < [silent 0 hgetpropval $path changed] + 10} {
          # ignore updates of variables during change
          # clientput "ignore [sct result]"
          return idle
        }
        if {[lrange $sent_message 0 1] eq [list read $par]} {
          set message_to_client [sct result]
        }
      }
      lassign $val value qual
      if {[silent 0 hgetpropval $path secoptype] eq "tuple" &&
          [string match *:status $par]} {
        if {[llength $value] > 2} {
          set text_value [lrange $value 1 end]
        } else {
          set text_value [lindex $value 1]
        }
        set objpath [sct parent $path]
        set visible_old [silent true hgetpropval $objpath visible]
        if {$text_value ne "disabled"} {
          set visible_new true
          set shown 1
        } else {
          set visible_new false
          set shown 0
        }
        if {$visible_new ne $visible_old} {
          hsetprop $objpath visible $visible_new
          GraphItem shown $objpath $shown
        }
        lassign $value value
        if {[catch {updateval ${path}_text $text_value}]} {
          clientput "cannot update ${path}_text to $text_value"
          clientput "MSG([sct result])"
        }
        if {[string match *:status $par]} {
          set oldstatus [silent idle hgetpropval /$obj status]
          if {[silent 0 hgetpropval /$obj writestatus] eq "start"} {
            set status run
          } elseif {[string match 3* $value]} {
            set status run
          } elseif {[string match 4* $value]} {
            set status posfault
          } else {
            set status idle
            if {[silent "" hgetpropval $obj type] eq "drivable" && $oldstatus eq "run"} {
              if {[catch {
                set oldvalue [silent 0 hgetpropval $obj value_before_run]
                set oldtarget [silent 0 hgetpropval $obj target_before_run]
                set value [hval /$obj]
                set target [silent 0 hgetpropval /$obj target]
                set delay [expr [DoubleTime] - [silent 0 hgetpropval $obj write_time]]
                if {abs($value - $oldvalue) < abs($target - $oldtarget) * 0.1 && $delay < 10} {
                  # clientput "WARNING: $obj status is idle, but value has not moved: abs($value - $oldvalue) < abs($target - $oldtarget) * 0.1 delay $delay"
                  set status run
                } 
              } msg]} {
                clientput "WARNING: $msg"
              }
            }
          }
          hsetprop /$obj status $status
        }
      }
      if {[catch {updateval $path $value} msg]} {
        if {$value eq "None"} {
          hsetprop $path geterror None
        } else {
          clientput "cannot update $path to $value"
          clientput $msg
        }
      } elseif {[string match *:target $par]} {
        # clientput [sct result]/[silent "" hgetpropval /$obj status]
        if {[silent "" hgetpropval /$obj status] eq "idle"} {
          hsetprop /$obj target $value
        }
      }
      catch {
        hsetprop $path timestamp [dict get $qual t]
      }
    }
    pong {
      if {[lindex $sent_message 0] eq "ping"} {
        set message_to_client [sct result]
      }
    }
    done {
      if {[lrange $sent_message 0 2] eq [list do $par]} {
        set message_to_client [sct result]
      } else {
        clientput "done $par $val"
      }
    }
    active {
      if {[lindex $sent_message 0] eq "activate"} {
        set message_to_client [sct result]
      } else {
        clientput ACTIVE
      }
      sct active 1
      sct end_fast 0
    }
    error {
      lassign $val origin info
      lassign $origin requesttype requestpar requestval
      set path [silent "" hgetpropval /sics/[sct controller] p_$requestpar]
      if {$requesttype eq "change" && $path ne ""} {
        hsetprop $path changed 0
      }
      set errortext [lindex [dict get $info errorinfo] 0]
      if {$origin eq $sent_message} {
        set message_to_client [sct result]
      } else {
        clientput "ERROR: $path $errortext"
      }
    }
    default {
      if {$sent_message ne ""} {
        set message_to_client [sct result]
      } else {
        # show untreated message
        clientput [sct result]
      }
    }
  }
  if {$message_to_client ne ""} {
    clientput "> $sent_message\n< $message_to_client"
    sct sent_message ""
    sct sent_time 1e20
  } elseif {[DoubleTime] > [silent 1e20 sct sent_time] + 10} {
    clientput "timeout waiting for response to $sent_message"
    sct sent_message ""
    sct sent_time 1e20
  }
  if {[DoubleTime] < [sct end_fast]} {
    return secop::get
  }
  [sct controller] queue [sct] read secop::get
  return idle
}

proc secop::checklimits {} {
  # for whatever strange reason checklimits is called twice
  # in addition again as write script of the obj node
  # do this only once
  if {[silent 0 sct status] ne "run" ||
      [sct target] != [silent "x" hgetpropval [sct]/target target]} {
    sct value_before_run [hval [sct]]
    sct target_before_run [hval [sct]/target]
    hset [sct]/target [sct target]
    sct status run
  }
}

proc secop::complete_run {} {
  sct print "run [sct objectName] to [sct target]"
  return idle
}

proc secop::halt {} {
  [sct objectName] stop
  sct writestatus done
  sct target_before_run [silent 0 sct target]
  # sct status posfault
  # clientput HALT:[sct]
  return idle
}

proc secop::start {} {
  [sct controller] timeout [sct timeout]
  sct send *IDN?
  return secop::getidn
}

proc secop::getidn {} {
  clientput [sct result]
  [sct controller] queue [sct secopPath] start secop::describe
  sct active 0
  return idle
}

proc secop::describe {} {
  sct send describe
  return secop::describing
}

proc secop::describing {} {
  lassign [sct result] messagetype par val
  switch $messagetype {
    describing {
      do_as_manager {
        make_node $val
      }
    }
    default {
      clientput "ignore $messagetype $par ..."
    }
  }
  sct send activate
  sct end_fast [expr [DoubleTime] + 5]
  return secop::update
}

proc secop_send {args} {
  hset /secop $args
  hsetprop /secop sent_message $args
  hsetprop /secop sent_time [DoubleTime]
}

publishLazy secop_send
