#!/usr/bin/env wish

package require json
package require json::write

wm attributes . -fullscreen 1
wm protocol . WM_DELETE_WINDOW { }      ;# do not allow window close

set bg_image {
  width      -1
  height     -1
  new_width  0
  new_height 0
  path       ""
  bg_color   "#6392ac"
}

set image_info {
  image_path ""
  image_size ""
  status     ""
  version    ""
}

set ui_messages {}
array set rotating_port_labels ""
array set port_labels ""
set writable_labels false

set content_ids [dict create]

set images_downloader_fhs       [dict create]
set images_downloading_versions [dict create]
set upcoming_download_events    [dict create]

array set requested_versions       [list]
array set requested_version_types  [list]
array set requested_version_values [list]

array set paths [list \
  flash_drive_blue      /usr/share/puavo-usb-factory/flash-drive-blue.png    \
  flash_drive_green     /usr/share/puavo-usb-factory/flash-drive-green.png   \
  flash_drive_grey      /usr/share/puavo-usb-factory/flash-drive-grey.png    \
  flash_drive_magenta   /usr/share/puavo-usb-factory/flash-drive-magenta.png \
  flash_drive_red       /usr/share/puavo-usb-factory/flash-drive-red.png     \
  flash_drive_white     /usr/share/puavo-usb-factory/flash-drive-white.png   \
  flash_drive_yellow    /usr/share/puavo-usb-factory/flash-drive-yellow.png  \
  puavo_usb_factory_workdir $::env(HOME)/.puavo/usb-factory                  \
]

set pci_path_dir /dev/disk/by-path

set diskdevices  [dict create]
set usbports     [dict create]
set new_usbports [dict create]

set calibration_in_progress true
set next_calibration_entry ""

proc logger {args} {
  catch { exec logger -t puavo-usb-factory {*}$args & }
}

proc read_file {path} {
  set f [open $path]
  try {
    set data [read $f]
  } finally {
    close $f
  }
  return [string trimright $data "\n"]
}

proc download_images {} {
  global content_ids images_downloader_fhs images_downloading_versions \
         requested_versions upcoming_download_events

  dict for {content_dir content_id} $content_ids {
    if {[dict exists $upcoming_download_events $content_id]} {
      after cancel [dict get $upcoming_download_events $content_id]
      dict unset upcoming_download_events $content_id
    }

    set requested_version $requested_versions($content_id)

    if {[dict exists $images_downloader_fhs $content_id]} {
      set images_downloading_version [
        dict get $images_downloading_versions $content_id
      ]
      if {$images_downloading_version eq $requested_version} { return }

      set fh [dict get $images_downloader_fhs $content_id]
      set pid [pid $fh]
      catch { exec pkill -s $pid }
      catch { close $fh }
    }

    dict set images_downloading_versions $content_id $requested_version

    logger -p user.warning "starting download for $content_dir"

    set fh [
      open "|[list setsid -w puavo-download-usb-factory-images $content_dir]"
    ]
    dict set images_downloader_fhs $content_id $fh

    fconfigure $fh -blocking 0
    fileevent $fh readable [list handle_image_download $content_id]
  }
}

proc handle_image_download {content_id} {
  global images_downloader_fhs upcoming_download_events

  set fh [dict get $images_downloader_fhs $content_id]
  catch { read $fh }
  if {[eof $fh]} {
    if {[dict exists $upcoming_download_events $content_id]} {
      after cancel [dict get $upcoming_download_events $content_id]
    }
    fconfigure $fh -blocking 1
    if {[catch { close $fh }]} {
      set wait_ms 30000         ; # failed, try again after 30 seconds
    } else {
      set wait_ms 3600000       ; # success, try again after 1 hour
    }
    dict set upcoming_download_events $content_id [
      after $wait_ms download_images
    ]
    dict unset images_downloader_fhs $content_id
  }
}

proc update_background {image imagepath new_width new_height} {
  # XXX it would be better to use standard output
  set tmpfile [exec mktemp /tmp/puavo-usb-factory-image.XXXXXXX]
  set img_size "${new_width}x${new_height}!"
  exec -ignorestderr convert $imagepath -resize $img_size png:$tmpfile
  $image read $tmpfile -shrink
  exec rm -f $tmpfile
}

proc do_background_resizing {} {
  global bg_image canvas_image_index

  set update false
  if {[dict get $bg_image path] != [dict get $bg_image new_path]} {
    set update true
  } else {
    set size_diff [ expr {
      max(abs([dict get $bg_image width]  - [dict get $bg_image new_width]),
          abs([dict get $bg_image height] - [dict get $bg_image new_height]))
    }]
    if {$size_diff >= 4} {
      set update true
    }
  }

  if {!$update} { return }

  set new_width  [dict get $bg_image new_width]
  set new_height [dict get $bg_image new_height]
  set new_path   [dict get $bg_image new_path]

  setup_styles

  update_background bg_photo $new_path $new_width $new_height
  .f coords $canvas_image_index [expr { int($new_width/2)  }] \
                                [expr { int($new_height/2) }]

  dict set bg_image width  $new_width
  dict set bg_image height $new_height
  dict set bg_image path   $new_path
}

set bg_resizing_event ""
proc queue_background_resizing {ms {width ""} {height ""}} {
  global bg_image bg_resizing_event

  if {$width  ne ""} { dict set bg_image new_width  $width  }
  if {$height ne ""} { dict set bg_image new_height $height }

  if {$bg_resizing_event ne ""} { after cancel $bg_resizing_event }
  set bg_resizing_event [after $ms do_background_resizing]
}

proc ui_msg {args} {
  global ui_messages
  dict get $ui_messages {*}$args
}

proc update_ui_info_state {} {
  global image_info

  set download_status [dict get $image_info status]
  set version         [dict get $image_info version]

  switch -glob -- $download_status {
    {in progress *} {
      set download_version "?"
      regexp {^in progress (.*)$} $download_status _ download_version
      set download_message \
          "[ui_msg {download state} "in progress"] $download_version"
    }
    up-to-date -
    failed     -
    missing { set download_message [ui_msg {download state} $download_status] }
    default { set download_message [ui_msg {download state} undefined] }
  }
  .f.version_status.download.status configure -text $download_message

  if {$version ne ""} {
    set version_message $version
  } else {
    set version_message -
  }
  .f.version_status.version.number configure -text $version_message
}

proc get_available_content_dirs {} {
  global paths

  if {[catch { glob $paths(puavo_usb_factory_workdir)/* } res]} {
    error "$paths(puavo_usb_factory_workdir)/* did not match any files: $res"
  }
  lsort [
    lmap path $res { expr { [file isdirectory $path] ? $path : [continue] } }
  ]
}

proc get_default_content_dir {} {
  set content_dirs [get_available_content_dirs]

  set dir_count [llength $content_dirs]
  if {$dir_count == 0} {
    error "no content directories"
  }

  # get the the first one (alphabetically) in case there are many
  lindex $content_dirs 0
}

proc update_image_info {} {
  global content_dir image_info

  dict set image_info image_path ""
  dict set image_info image_size ""
  dict set image_info status     ""
  dict set image_info version    ""

  set content_name [file tail $content_dir]

  if {![catch { read_file "${content_dir}/DOWNLOAD_STATUS" } res]} {
    dict set image_info status [string trim $res]
  } else {
    dict set image_info status missing
  }

  if {![catch { read_file "${content_dir}/LATEST_IMAGE" } res]} {
    set image_path "${content_dir}/[string trim $res]"
    if {[file exists $image_path]} {
      dict set image_info image_path $image_path
      dict set image_info image_size [file size $image_path]
    }
  }

  if {![catch { read_file "${content_dir}/VERSION" } res]} {
    dict set image_info version [string trim $res]
  }

  update_ui_info_state
}

proc update_image_info_loop {} {
  catch { update_image_info }
  after 10000 update_image_info_loop
}

proc close_if_open {devpath} {
  global diskdevices

  set fh [dict get $diskdevices $devpath fh]
  if {$fh ne ""} {
    catch { close $fh }
  }
  dict set diskdevices $devpath fh ""
}

proc start_preparation {devpath countdown} {
  global diskdevices

  if {![dict exists $diskdevices $devpath]} {
    return
  }
  if {[dict get $diskdevices $devpath state] ne "start_writing"} {
    return
  }

  set_usbicon_state $devpath start_writing

  if {$countdown == 0} {
    set_devstate $devpath writing
    return
  }

  incr countdown -1
  after 250 [list start_preparation $devpath $countdown]
}

proc start_operating {devpath cmd} {
  set fh [open "| $cmd"]
  fconfigure $fh -blocking 0
  fileevent $fh readable [list handle_fileevent $devpath]
  return $fh
}

proc start_verifying {devpath} {
  global diskdevices pci_path_dir

  set image_path [dict get $diskdevices $devpath image_path]
  set image_size [dict get $diskdevices $devpath image_size]
  set version    [dict get $diskdevices $devpath version]
  if {$image_path eq "" || $image_size eq "" || $version eq ""} {
    set_devstate $devpath error
    return ""
  }

  # set access time of image file so that it can be used
  # by other scripts to test if it is still in use (or may be removed?)
  catch { file atime $image_path [clock seconds] }

  set cmd "nice -n 20 ionice -c 3 pv -b -n $image_path | nice -n 20 ionice -c 3 cmp -n $image_size ${pci_path_dir}/${devpath} - 2>@1"
  start_operating $devpath $cmd
}

proc start_writing {devpath} {
  global diskdevices pci_path_dir

  set image_path [dict get $diskdevices $devpath image_path]
  set image_size [dict get $diskdevices $devpath image_size]
  set version    [dict get $diskdevices $devpath version]
  if {$image_path eq "" || $image_size eq "" || $version eq ""} {
    set_devstate $devpath error
    return ""
  }

  # set access time of image file so that it can be used
  # by other scripts to test if it is still in use (or may be removed?)
  catch { file atime $image_path [clock seconds] }

  set cmd "nice -n 20 ionice -c 3 pv -b -n $image_path | nice -n 20 ionice -c 3 dd of=${pci_path_dir}/${devpath} conv=fsync,nocreat status=none 2>@1"
  start_operating $devpath $cmd
}

proc set_device_eta {devpath bytes_handled} {
  global diskdevices

  set image_size [dict get $diskdevices $devpath image_size]

  set current_time_in_ms [clock milliseconds]

  set progress_list [dict get $diskdevices $devpath progress_list]
  lappend progress_list $current_time_in_ms $bytes_handled
  dict set diskdevices $devpath progress_list $progress_list

  set time_cutpoint [expr { $current_time_in_ms - 60000 }]

  set cut_progress_times [list]
  set progress_list [dict get $diskdevices $devpath progress_list]
  foreach {old_time_in_ms old_bytes_handled} $progress_list {
    if {$old_time_in_ms >= $time_cutpoint} {
      lappend cut_progress_times $old_time_in_ms $old_bytes_handled
    }
  }
  dict set diskdevices $devpath progress_list $cut_progress_times

  lassign $cut_progress_times first_time first_bytes_handled

  set percentage [expr { round(100.0 * $bytes_handled / $image_size) }]

  if {[dict get $diskdevices $devpath state] eq "writing"} {
    # When we are in writing phase, we do some tricks in the ETA
    # calculation, because the initial estimates are too optimistic
    # and there will be a verification step afterwards.
    set eta_image_size [expr { int(1.17 * $image_size) }]
  } else {
    set eta_image_size $image_size
  }

  set eta ""
  if {$first_time ne "" && $first_bytes_handled ne ""} {
    set bytes_diff [expr { $bytes_handled - $first_bytes_handled }]
    set time_diff  [expr { $current_time_in_ms - $first_time }]
    set bytes_left [expr { $eta_image_size - $bytes_handled }]

    if {$time_diff > 0} {
      set speed [expr { $bytes_diff / $time_diff }]
      if {$speed > 0} {
        set eta_in_ms [expr { $bytes_left / $speed }]
        set eta_in_s [expr { round($eta_in_ms / 1000.0) }]
        set eta "[expr { $eta_in_s / 60 }]min [expr { $eta_in_s % 60 }]s"
      }
    }
  }

  set_devstate $devpath progress $percentage $eta
}

proc lookup_usbdevice_model {devpath} {
  set usbdevice_model "?"

  if {![regexp {usb-[0-9]+:(.*)-scsi-} $devpath _ usbport]} {
    return $usbdevice_model
  }
  catch {
    set paths [glob "/sys/bus/usb/devices/*-${usbport}"]
    if {[llength $paths] == 1} {
      set usbport_syspath [lindex $paths 0]
      set manufacturer [read_file "${usbport_syspath}/../manufacturer"]
      set product      [read_file "${usbport_syspath}/../product"]
      set usbdevice_model "${manufacturer} ${product}"
    }
  }

  return $usbdevice_model
}

proc dev_logid {devpath} {
  global diskdevices pci_path_dir

  set port_id [dict get $diskdevices $devpath port_id]
  set label [get_label $port_id]
  set usbdevice_model [dict get $diskdevices $devpath model]
  return "$label (${pci_path_dir}/${devpath} | $usbdevice_model)"
}

proc log_operation_time {devpath op_name op_time_key} {
  global diskdevices
  set op_time [expr {
    int(([clock milliseconds]
           - [dict get $diskdevices $devpath $op_time_key]) / 1000.0)
  }]
  logger -p user.notice \
         "$op_name of [dev_logid $devpath] finished in $op_time seconds"
}

proc handle_fileevent {devpath} {
  global diskdevices

  set fh [dict get $diskdevices $devpath fh]
  set chars_returned [gets $fh progressline]

  if {[eof $fh]} {
    fconfigure $fh -blocking 1
    if {[catch { close $fh }]} {
      set state [dict get $diskdevices $devpath state]
      if {$state eq "verifying"} {
        logger -p user.notice "verification failed on [dev_logid $devpath]"
        set_devstate $devpath start_writing
      } else {
        set_devstate $devpath error
      }
    } else {
      switch -- [dict get $diskdevices $devpath state] {
        writing { set_devstate $devpath verifying_after_write }
        verifying -
        verifying_after_write {
          set_devstate $devpath finished
        }
      }
    }
    return
  }

  if {$chars_returned <= 0} { return }

  set bytes_written [string trim $progressline]
  # $bytes_written might be an error string, do not handle that
  if {[string is integer -strict $bytes_written]} {
    set_device_eta $devpath $bytes_written
  }
}

proc set_ui_status_to_nomedia {devpath} {
  global diskdevices

  if {![dict exists $diskdevices $devpath]} {
    return
  }

  if {[dict get $diskdevices $devpath state] eq "nomedia"} {
    logger -p user.info "no media on [dev_logid $devpath]"
    set_usbicon_state $devpath nomedia
  }
}

proc quick_verify_check {devpath} {
  global diskdevices pci_path_dir

  # A heuristic, not exact... check if first and last megabytes match
  # on device with image.  If this succeeds we must verify the whole
  # disk image on device, and if that fails we will start writing.

  set image_path [dict get $diskdevices $devpath image_path]
  set image_size [dict get $diskdevices $devpath image_size]

  if {$image_path eq ""} { error "could not lookup image path" }
  if {$image_size eq ""} { error "could not lookup image size" }

  set chk_bytecount   [expr { min($image_size, 4096) }]
  set end_block_start [expr { $image_size - $chk_bytecount }]

  set full_devpath "${pci_path_dir}/${devpath}"

  try {
    exec cmp -n $chk_bytecount $full_devpath $image_path
  } on error {} { return false }

  try {
    exec cmp -i $end_block_start -n $chk_bytecount $full_devpath $image_path
  } on error {} { return false }

  return true
}

proc set_devstate {devpath state args} {
  global diskdevices port_labels

  set port_id [dict get $diskdevices $devpath port_id]
  if {($port_labels($port_id) eq "") && ($state ne "nomedia")} {
    # do not act on devices that are not mapped to UI, except for "nomedia"
    return
  }

  switch -- $state {
    error {
      logger -p user.warning "error occurred on [dev_logid $devpath]"

      close_if_open $devpath
      dict set diskdevices $devpath state error
      set_usbicon_state $devpath error
    }

    finished {
      log_operation_time $devpath verifying verify_start_time

      close_if_open $devpath
      dict set diskdevices $devpath state finished
      set_usbicon_state $devpath finished
    }

    nomedia {
      set current_state [dict get $diskdevices $devpath state]
      switch -- $current_state {
        nomedia               {}
        verifying             -
        verifying_after_write -
        writing               {
          set_devstate $devpath error
        }
        default {
          close_if_open $devpath

          # preserve some of the attributes
          dict set diskdevices $devpath fh            ""
          dict set diskdevices $devpath image_path    ""
          dict set diskdevices $devpath image_size    ""
          dict set diskdevices $devpath model         ""
          dict set diskdevices $devpath progress_list [list]
          dict set diskdevices $devpath state         nomedia
          dict set diskdevices $devpath version       ""

          if {$current_state eq "error"} {
            # show error message for five seconds in UI
            after 5000 [list set_ui_status_to_nomedia $devpath]
          } else {
            set_ui_status_to_nomedia $devpath
          }
        }
      }
    }

    nospaceondevice {
      logger -p user.warning "not enough space on [dev_logid $devpath]"

      close_if_open $devpath
      dict set diskdevices $devpath state nospaceondevice
      set_usbicon_state $devpath nospaceondevice
    }

    progress {
      lassign $args percentage eta
      set state [dict get $diskdevices $devpath state]
      switch -- $state {
        verifying -
        verifying_after_write -
        writing { set_usbicon_state $devpath $state $percentage $eta }
      }
    }

    start_writing {
      logger -p user.notice "preparing to write to [dev_logid $devpath]"

      dict set diskdevices $devpath state start_writing
      start_preparation $devpath 41
    }

    verifying_after_write -
    verifying {
      if {$state eq "verifying_after_write"} {
        log_operation_time $devpath writing write_start_time
      }

      logger -p user.warning "verifying device [dev_logid $devpath]"

      if {[quick_verify_check $devpath]} {
        set fh [start_verifying $devpath]
        dict set diskdevices $devpath fh $fh
        if {$fh ne ""} {
          dict set diskdevices $devpath verify_start_time [clock milliseconds]
          dict set diskdevices $devpath state $state
          set_usbicon_state $devpath $state
        }
      } else {
        # if image is not in disk, move on to start_writing
        logger -p user.warning \
               "quick verification check failed on [dev_logid $devpath]"
        set_devstate $devpath start_writing
      }
    }

    writing {
      logger -p user.info "writing to device [dev_logid $devpath]"

      set fh [start_writing $devpath]
      dict set diskdevices $devpath fh $fh

      if {$fh ne ""} {
        dict set diskdevices $devpath write_start_time [clock milliseconds]
        dict set diskdevices $devpath state writing
        set_usbicon_state $devpath writing
      }
    }
  }
}

proc get_device_size {devpath} {
  global pci_path_dir

  set device_size ""
  set full_devpath "${pci_path_dir}/${devpath}"

  if {[catch { set f [open $full_devpath r] } err]} {
    logger -p user.warning "error while opening $full_devpath: $err"
    return $device_size
  }
  catch {
    seek $f 0 end
    set device_size [tell $f]
  }

  catch { close $f }
  return $device_size
}

proc check_for_space_in_device {devpath} {
  global diskdevices

  set device_size [get_device_size $devpath]
  set image_size  [dict get $diskdevices $devpath image_size]

  if {$device_size eq ""} { error "could not get device size" }
  if {$image_size  eq ""} { error "could not get image size"  }

  expr { $device_size >= $image_size }
}

proc find_next_calibration_entry {} {
  global next_calibration_entry port_labels

  if {$next_calibration_entry eq ""} {
    set sorted_entries [
      lsort -dictionary \
            [lmap {k v} [array get port_labels] { expr {$v} } ]
    ]
    if {[llength $sorted_entries] == 0} {
      set next_calibration_entry 1
    } else {
      set next_calibration_entry [string trim [lindex $sorted_entries end]]
    }
  }

  if {[regexp {^(.*?)([0-9]+)$} $next_calibration_entry _ prefix number]} {
    set next_calibration_entry "${prefix}[expr { $number + 1 }]"
  } else {
    set next_calibration_entry "${next_calibration_entry}2"
  }
}

proc set_port_label {port_id value} {
  global port_labels

  set port_labels($port_id) $value
  set_rotating_label $port_id port $value
  set_rotating_label $port_id messages [list]
}

proc activate_calibration {{mode ""}} {
  global diskdevices next_calibration_entry port_labels writable_labels

  if {$mode eq "initial"} {
    foreach {k v} [array get port_labels] {
      set_port_label $k ""
    }

    set next_calibration_entry 1

    update_port_labels_on_disk
    update_diskdevices true
  }

  if {$writable_labels} {
    switch_ui_mode calibration
  } else {
    switch_ui_mode normal
  }
}

proc add_new_port_if_calibrating {port_id} {
  global calibration_in_progress next_calibration_entry port_labels

  if {!$calibration_in_progress} { return }
  if {$port_labels($port_id) ne ""} {
    return
  }

  set next_calibration_entry [string trim $next_calibration_entry]
  if {$next_calibration_entry eq ""} {
    set next_calibration_entry [find_next_calibration_entry]
  }

  set_port_label $port_id $next_calibration_entry
  set next_calibration_entry [find_next_calibration_entry]

  update_port_labels_on_disk
  update_diskdevices true
}

proc check_files {} {
  global diskdevices image_info pci_path_dir

  dict for {devpath devstate} $diskdevices {
    set full_devpath "${pci_path_dir}/${devpath}"
    if {[file exists $full_devpath]} {
      if {[dict get $devstate state] eq "nomedia"} {
        # new usb device has been inserted, do something
        add_new_port_if_calibrating [dict get $devstate port_id]
        dict set diskdevices $devpath model [lookup_usbdevice_model $devpath]

        # set current device image information from $image_info
        dict set diskdevices $devpath image_path \
                 [dict get $image_info image_path]
        dict set diskdevices $devpath image_size \
                 [dict get $image_info image_size]
        dict set diskdevices $devpath version \
                 [dict get $image_info version]

        if {[catch { check_for_space_in_device $devpath } res]} {
          logger -p user.warning \
                 "error while checking for space on [dev_logid $devpath]: $res"
          set_devstate $devpath error
        } elseif {$res} {
          set_devstate $devpath verifying
        } else {
          set_devstate $devpath nospaceondevice
        }
      }
    } else {
      set_devstate $devpath nomedia
    }
  }

  after 250 check_files
}

proc roll_rotating_port_labels {list_index} {
  global diskdevices rotating_port_labels writable_labels

  if {!$writable_labels} {
    dict for {devpath devstate} $diskdevices {
      set port_id [dict get $devstate port_id]
      set port_ui [dict get $devstate ui]
      set i [expr { $list_index % [llength $rotating_port_labels($port_id)] }]
      set labeltext [lindex $rotating_port_labels($port_id) $i]
      $port_ui itemconfigure port_label -text $labeltext
    }
  }

  if {$list_index % (2 * 3 * 5 * 7) == 0} {
    set list_index 0
  }

  incr list_index
  after 1500 [list roll_rotating_port_labels $list_index]
}

proc set_rotating_label {port_id type messages} {
  global rotating_port_labels

  if {$type eq "port"} {
    set list_start [list $messages $messages]
    if {[info exists rotating_port_labels($port_id)]} {
      set list_end [lrange $rotating_port_labels($port_id) 2 3]
    } else {
      set list_end [list]
    }
  } else {
    set list_start [lrange $rotating_port_labels($port_id) 0 1]
    switch -- [llength $messages] {
      0 { set list_end $list_start }
      1 { set list_end [list {*}$messages {*}$messages] }
      2 {
          if {[lindex $messages 1] eq ""} {
            set list_end [list [lindex $messages 0] [lindex $messages 0]]
          } else {
            set list_end $messages
          }
        }
      default { error "unsupported rotating usb labels count" }
    }
  }

  set rotating_port_labels($port_id) [list {*}$list_start {*}$list_end]
}

proc validate_label {value} { expr {[string length $value] <= 12} }

proc make_usbport_label {devpath port_id port_ui {port_state ""}} {
  global bg_image port_labels rotating_port_labels writable_labels

  if {$port_state ne ""} { destroy $port_ui }

  set_port_label $port_id [get_label $port_id]

  if {$writable_labels} {
    ttk::entry $port_ui -textvariable port_labels($port_id) \
                        -font smallInfoFont -justify center -width 16 \
           -validatecommand [list validate_label %P] \
           -validate all
  } else {
    set bg_color [dict get $bg_image bg_color]
    canvas $port_ui -background $bg_color -height 42 -highlightthickness 0 \
                    -width 240
    $port_ui create image 0 22 -image flash_drive_white -anchor w

    if {$port_state eq ""} { set port_state nomedia }
    set_usbicon_state $devpath $port_state

    $port_ui create image 0 22 -image "${port_ui}_overlay_image" -anchor w
    $port_ui create text  132 18 -font smallInfoFont -tags port_label \
             -text $port_labels($port_id)
  }

  return $port_labels($port_id)
}

proc set_usbport_image {port_ui imagename {new_width ""}} {
  global flash_drive_image_width paths
  set ui_image "${port_ui}_overlay_image"

  if {$new_width ne ""} {
    set new_width_in_pixels [
      expr { int($new_width * $flash_drive_image_width) }
    ]

    set current_width [$ui_image cget -width]
    if {$current_width != $new_width_in_pixels} {
      image create photo $ui_image -file $paths($imagename) \
                                   -width $new_width_in_pixels
    }
  } else {
    image create photo $ui_image -file $paths($imagename)
  }
}

proc set_usbicon_state {devpath state {percentage ""} {eta ""}} {
  global diskdevices

  set port_id [dict get $diskdevices $devpath port_id]
  set port_ui [dict get $diskdevices $devpath ui]

  # Make writing progress go from 1/4 to 7/8,
  # the rest is verification progress (and change colour to indicate that).
  set initial_offset [expr { 1.0/4 }]
  set cutpoint       [expr { 7.0/8 }]

  set startpoint ""
  set endpoint   1.0

  set msg ""

  switch -- $state {
    error {
      set imagename flash_drive_red
      set msg [ui_msg messages error]
    }
    finished {
      set version [dict get $diskdevices $devpath version]
      set imagename flash_drive_green
      set msg "[ui_msg messages finished] $version"
    }
    nomedia {
      set imagename flash_drive_white
    }
    nospaceondevice {
      set imagename flash_drive_red
      set msg [ui_msg messages nospaceondevice]
    }
    start_writing {
      if {([clock milliseconds] / 250) % 2 == 0} {
        set imagename flash_drive_magenta
      } else {
        set imagename flash_drive_white
      }
      set msg [ui_msg messages start_writing]
    }
    verifying {
      set version [dict get $diskdevices $devpath version]
      set imagename  flash_drive_yellow
      set startpoint $initial_offset
      set msg        "[ui_msg messages verifying] $version"
    }
    verifying_after_write {
      set version [dict get $diskdevices $devpath version]
      set imagename  flash_drive_yellow
      set startpoint [expr { $cutpoint + 0.02 }]
      set msg        "[ui_msg messages verifying] $version"
    }
    writing {
      set version [dict get $diskdevices $devpath version]
      set imagename  flash_drive_blue
      set startpoint $initial_offset
      set endpoint   $cutpoint
      set msg        "[ui_msg messages writing] $version"
    }
  }

  set new_width ""
  if {$msg ne ""} {
    if {($percentage ne "") && ($eta ne "")} {
      set new_width [expr {
        $startpoint + ($percentage/100.0) * ($endpoint - $startpoint)
      }]
      set_rotating_label $port_id messages [list $msg $eta]
    } else {
      if {$startpoint ne ""} { set new_width $startpoint }
      set_rotating_label $port_id messages [list $msg]
    }
  } else {
    set_rotating_label $port_id messages [list]
  }

  set_usbport_image $port_ui $imagename $new_width
}

proc update_new_usbports {} {
  global new_usbports

  try {
    set _new_usbports [dict create]

    foreach pci_device_path [glob /sys/devices/pci*/0000:*] {
      set pci_id [file tail $pci_device_path]

      set paths [exec find $pci_device_path -type d \
                           -regex {.*[.-][0-9]+-port[0-9]+$}]

      foreach path $paths {
        if {![regexp {([0-9]+)$} $path _ portname]} {
          continue
        }

        set _portbase [file tail [file dirname $path]]
        if {![regexp {^[0-9]+-(.*)$} $_portbase _ portbase]} {
          continue
        }

        set usbport [string map [list {:} .$portname:] \
                                $portbase]
        set port_id "${pci_id}/${usbport}"
        set devpath "pci-${pci_id}-usb-0:${usbport}-scsi-0:0:0:0"
        dict set _new_usbports $port_id $devpath
      }
    }
  } on error {errmsg} {
    logger -p user.warning "error updating usbports: $errmsg"
    return
  }

  set new_usbports $_new_usbports
}

proc update_diskdevices_loop {} {
  update_diskdevices
  after 3000 update_diskdevices_loop
}

proc get_label {port_id} {
  global port_labels
  expr {
    [info exists port_labels($port_id)]
      ? $port_labels($port_id)
      : ""
  }
}

proc dictionary_sort_by_label {lst} {
  set labels [lmap p $lst { list $p [get_label $p] } ]
  lmap x [lsort -index 1 -dictionary $labels] \
         { lindex $x 0 }
}

proc update_diskdevices {{force_ui_update false}} {
  global bg_image diskdevices pci_path_dir new_usbports usbports port_labels \
         writable_labels

  update_new_usbports

  set regrid false

  set diskdevices_in_hubs [dict create]
  # list all diskdevices in new hubs
  foreach devpath [dict values $new_usbports] {
    dict set diskdevices_in_hubs $devpath 1
  }

  # check all our current diskdevices and remove those
  # that are not in current hubs
  foreach devpath [dict keys $diskdevices] {
    if {[dict exists $diskdevices_in_hubs $devpath]} {
      continue
    }

    # $devpath is gone from usbports, remove it and its UI

    logger -p user.info \
           "removing [dev_logid $devpath] because it is no longer attached"

    destroy [dict get $diskdevices $devpath ui]
    close_if_open $devpath
    dict unset diskdevices $devpath
    set regrid true

    dict for {port_id usbport_devpath} $usbports {
      if {$devpath eq $usbport_devpath} {
        dict unset usbports $port_id
      }
    }
  }

  set ui_elements [list]

  set sorted_ports [dictionary_sort_by_label [dict keys $new_usbports]]

  foreach port_id $sorted_ports {
    set devpath [dict get $new_usbports $port_id]
    set port_ui ".f.disks.port_[string map {. _} $port_id]"

    # Add UI for port and add it to diskdevices to manage.
    # Test for diskdevice existence, because the same $devpath
    # may be in two different products (USB2.0 vs. USB3.0).
    if {[dict exists $diskdevices $devpath]} {
      if {$force_ui_update} {
        set port_state [dict get $diskdevices $devpath state]
        make_usbport_label $devpath $port_id $port_ui $port_state
        set regrid true
      }

    } else {
      dict set diskdevices $devpath [
        dict create fh                ""       \
                    image_size        ""       \
                    model             ""       \
                    port_id           $port_id \
                    progress_list     [list]   \
                    state             nomedia  \
                    ui                $port_ui \
                    verify_start_time ""       \
                    version           ""       \
                    write_start_time  ""       ]
      make_usbport_label $devpath $port_id $port_ui
      logger -p user.info "device [dev_logid $devpath] attached"
      set regrid true
    }

    if {$port_labels($port_id) ne ""} {
      if {$port_ui ni $ui_elements} {
        lappend ui_elements $port_ui
      }
      dict set usbports $port_id $devpath
    }
  }

  if {[llength $ui_elements] == 0} {
    grid .f.disks.noports_message
  } else {
    grid forget .f.disks.noports_message
    if {$regrid} {
      set max_row_elements 13
      set total_element_count [llength $ui_elements]
      set element_count $total_element_count
      set need_for_columns 1
      while {$element_count > $max_row_elements} {
        set element_count [expr { $element_count - $max_row_elements }]
        incr need_for_columns
      }
      set divisor [expr {
        int(ceil((0.0 + $total_element_count) / $need_for_columns)) }
      ]

      set row_pos 1
      set column_pos 1
      foreach ui $ui_elements {
        grid forget $ui
        grid $ui -row $row_pos -column $column_pos -sticky w -ipadx 5
        if {$row_pos % $divisor == 0} { incr column_pos; set row_pos 0 }
        incr row_pos
      }
    }
  }
}

proc read_port_labels_from_disk {} {
  global calibration_in_progress next_calibration_entry paths port_labels
  set config_json_path "$paths(puavo_usb_factory_workdir)/config.json"

  if {![file exists $config_json_path]} {
    set next_calibration_entry 1
    return
  }

  try {
    set config [::json::json2dict [read_file $config_json_path]]
  } on error {errmsg} {
    logger -p user.warning "error reading config from disk: $errmsg"
    set next_calibration_entry 1
    return
  }

  dict for {port_id value} [dict get $config port_labels] {
    # we have at least one port label, thus we do not need to force calibration
    set calibration_in_progress false
    set port_labels($port_id) $value
  }

  set next_calibration_entry [find_next_calibration_entry]
}

proc write_file {path data} {
  set tmpfile "${path}.tmp"

  set fh [open $tmpfile w]
  puts $fh $data
  close $fh

  file rename -force $tmpfile $path

}

proc update_port_labels_on_disk {} {
  global paths port_labels
  set usblabels_json_path "$paths(puavo_usb_factory_workdir)/config.json"

  set port_labels_obj [dict create]
  foreach {k v} [array get port_labels] {
    if {$v eq ""} { continue }
    dict set port_labels_obj $k [::json::write string $v]
  }

  write_file $usblabels_json_path \
             [::json::write object port_labels \
                                   [::json::write object {*}$port_labels_obj]]
}

proc scroll_topbanner {} {
  global top_banner_pos topbanner_text_id
  incr top_banner_pos -1
  if {$top_banner_pos < 1928} {
    set top_banner_pos 3000
  }

  .f.top_banner coords $topbanner_text_id $top_banner_pos 20
  after 50 scroll_topbanner
}

proc update_content_dir {} {
  global bg_image content_dir

  dict set bg_image new_path "${content_dir}/UI.png"
  catch {
    dict set bg_image bg_color [read_file "${content_dir}/UI.png.bg_color"]
  }

  update_image_info
  update_content_dirs_ui true
  queue_background_resizing 0
}

proc setup_styles {} {
  global bg_image

  set bg_color [dict get $bg_image bg_color]

  ttk::style configure TFrame -background $bg_color
  ttk::style configure TLabel -background $bg_color -font infoFont
  ttk::style configure Changes.TFrame -background $bg_color
  ttk::style configure Instructions.TLabel -foreground white
  ttk::style configure Status.TLabel -foreground #c3c8cc -background #46606d
}

proc update_requested_versions {content_dir content_id} {
  global bg_image requested_versions requested_version_types \
         requested_version_values

  set requested_version_path "${content_dir}/REQUESTED_VERSION"

  if {[array names requested_version_types -exact $content_id] eq ""} {
    # values for $content_id is not yet set, initialize
    set requested_version ""
    catch { set requested_version [read_file $requested_version_path] }
    set requested_versions($content_id) $requested_version
    set requested_version_values($content_id) ""

    switch -- $requested_version {
      {by puavo-conf} {
        set requested_version_types($content_id) "by puavo-conf"
      }
      latest {
        set requested_version_types($content_id) "latest"
      }
      default {
        set requested_version_types($content_id) "this version"
        set requested_version_values($content_id) $requested_version
      }
    }
  }

  set requested_versions($content_id) $requested_version_types($content_id)
  if {$requested_versions($content_id) eq "this version"} {
    set trimmed_value [string trim $requested_version_values($content_id)]
    if {$trimmed_value ne ""} {
      set requested_versions($content_id)      $trimmed_value
      set requested_version_value($content_id) $trimmed_value
    } else {
      set requested_versions($content_id) "by puavo-conf"
      set requested_versions_value($content_id) ""
    }
  }

  write_file $requested_version_path $requested_versions($content_id)
}

proc setup_content_dir_version_ui {content_dir content_id} {
  global bg_image requested_version_types requested_version_values

  set bg_color [dict get $bg_image bg_color]
  set button_opts [list -background         $bg_color     \
                        -font               smallInfoFont \
                        -highlightthickness 0]

  set ui_frame .f.content_dirs.$content_id

  ttk::frame $ui_frame -style Changes.TFrame

  ttk::label $ui_frame.label -text [file tail $content_dir] \
                             -font infoFont

  ttk::frame $ui_frame.number -style Changes.TFrame
  ttk::frame $ui_frame.number.version

  radiobutton $ui_frame.number.by_puavo_conf \
              {*}$button_opts -value "by puavo-conf" \
              -variable requested_version_types($content_id)
  radiobutton $ui_frame.number.latest {*}$button_opts \
              -text [ui_msg version latest] -value "latest" \
              -variable requested_version_types($content_id)
  radiobutton $ui_frame.number.version.button \
              {*}$button_opts \
              -text "[ui_msg version {this version}]:" \
              -value "this version" \
              -variable requested_version_types($content_id)
  ttk::entry $ui_frame.number.version.number \
             -justify center -font smallInfoFont -width 16 \
             -textvariable requested_version_values($content_id)
  pack $ui_frame.label -anchor w
  pack $ui_frame.number -anchor w
  pack $ui_frame.number.by_puavo_conf \
       $ui_frame.number.latest \
       $ui_frame.number.version \
       -anchor w
  pack $ui_frame.number.version.button -anchor w -side left
  pack $ui_frame.number.version.number -anchor w -side left

  $ui_frame.number.by_puavo_conf  configure -background $bg_color
  $ui_frame.number.latest         configure -background $bg_color
  $ui_frame.number.version.button configure -background $bg_color

  pack $ui_frame -anchor w -pady 10
}

proc update_content_dirs_ui {visible} {
  global bg_image content_dir_paths content_ids requested_versions \
         requested_version_values

  set new_content_dir_paths [get_available_content_dirs]

  set content_ids [dict create]
  foreach content_dir $new_content_dir_paths {
    set content_id [
      string tolower [
        regsub -all {[^[:alpha:]]+} [file tail $content_dir] ""
      ]
    ]
    dict set content_ids $content_dir $content_id
  }

  dict for {content_dir content_id} $content_ids {
    update_requested_versions $content_dir $content_id
  }

  if {!$visible} {
    pack forget .f.content_dirs
    return
  }

  set bg_color [dict get $bg_image bg_color]

  if {$content_dir_paths ne $new_content_dir_paths} {
    catch { destroy .f.content_dirs }
    ttk::frame .f.content_dirs -style Changes.TFrame

    dict for {content_dir content_id} $content_ids {
      setup_content_dir_version_ui $content_dir $content_id
    }

    ttk::frame .f.content_dirs.series -style Changes.TFrame
    ttk::label .f.content_dirs.series.label -text [ui_msg "Current choice:"] \
                                            -font infoFont
    pack .f.content_dirs.series.label

    dict for {content_dir content_id} $content_ids {
      radiobutton .f.content_dirs.series.$content_id \
        -background $bg_color -font smallInfoFont -highlightthickness 0 \
        -text [file tail $content_dir] -value $content_dir \
        -variable content_dir -command update_content_dir
      pack .f.content_dirs.series.$content_id -anchor w
    }
    set content_dir_paths $new_content_dir_paths
  }

  dict for {content_dir content_id} $content_ids {
    .f.content_dirs.$content_id.number.by_puavo_conf  \
      configure -background $bg_color
    .f.content_dirs.$content_id.number.latest \
      configure -background $bg_color
    .f.content_dirs.$content_id.number.version.button \
      configure -background $bg_color
    .f.content_dirs.series.$content_id configure -background $bg_color

    set puavo_conf_requested_version ""
    catch {
      set puavo_conf_key [
        string trim [read_file "${content_dir}/PUAVOCONF_KEY"]
      ]
      set puavo_conf_requested_version [exec puavo-conf $puavo_conf_key]
    }

    set ui_frame .f.content_dirs.$content_id

    set by_puavo_conf_msg \
        "[ui_msg version {by puavo-conf}] ($puavo_conf_requested_version)"
    $ui_frame.number.by_puavo_conf configure -text $by_puavo_conf_msg

    switch -- $requested_versions($content_id) {
      ""              -
      {by puavo-conf} { $ui_frame.number.by_puavo_conf invoke }
      latest { $ui_frame.number.latest invoke }
      default {
        $ui_frame.number.version.button invoke
        set requested_version_values($content_dir) \
            $requested_versions($content_id)
      }
    }
  }

  pack .f.content_dirs.series -anchor w -pady 10
  pack .f.content_dirs -after .f.disks -side right -anchor w -padx 10
}

proc switch_ui_mode {{uimode ""}} {
  global calibration_in_progress writable_labels

  switch -- $uimode {
    calibration {
      set calibration_in_progress true
      set writable_labels false
    }
    normal {
      set calibration_in_progress false
      set writable_labels false
    }
    default {
      set calibration_in_progress false
      set writable_labels [expr { $writable_labels ? "false" : "true" }]
    }
  }

  if {!$writable_labels} {
    wm attributes . -fullscreen 1
    update_calibration_ui true
    update_content_dirs_ui false
    update_port_labels_on_disk
    download_images
  } else {
    update_content_dirs_ui true
    update_calibration_ui false
    wm attributes . -fullscreen 0
  }
  update_diskdevices true
}

proc update_calibration_ui {show_calibration} {
  global calibration_in_progress

  if {$show_calibration} {
    if {$calibration_in_progress} {
      pack .f.oper.calibration.input -anchor w
      .f.oper.calibration.info configure \
        -text [ui_msg "Stop calibration by pressing Ctrl+R"]
      pack .f.oper.calibration.info -anchor w
    } else {
      pack forget .f.oper.calibration.input .f.oper.calibration.info
    }
  } else {
    pack forget .f.oper.calibration.input .f.oper.calibration.info
    set msg [ui_msg "Continue calibration by pressing Ctrl+R"]
    set msg "${msg}\n[ui_msg "Recalibrate all by pressing Ctrl+Shift+R"]"
    .f.oper.calibration.info configure -text $msg
    pack .f.oper.calibration.info -anchor w
  }
}

logger -p user.notice "starting up"

#
# setup UI
#

font create smallInfoFont  -family {Ubuntu Condensed} -size 12 -weight bold
font create infoFont       -family {Ubuntu Condensed} -size 20 -weight bold
font create biggerInfoFont -family {Ubuntu Condensed} -size 24 -weight bold

# ui messages and background image

# setup downloader and content dirs
try {
  exec -ignorestderr puavo-download-usb-factory-images --setup
} on error {errmsg} {
  logger -p user.err \
         "error in running :: puavo-download-usb-factory-images --setup"
  exit 1
}

set content_dir_paths ""
try {
  set content_dir [get_default_content_dir]
} on error {errmsg} {
  logger -p user.err "could not get the default content dir: $errmsg"
  exit 1
}

dict set bg_image path     "${content_dir}/UI.png"
dict set bg_image new_path "${content_dir}/UI.png"

try {
  set ui_messages [
    ::json::json2dict [read_file "$paths(puavo_usb_factory_workdir)/UI.json"]
  ]
} on error {} {
  logger -p user.err "could not read $paths(puavo_usb_factory_workdir)/UI.json"
  exit 1
}

# ui elements

setup_styles

canvas .f
image create photo bg_photo
set canvas_image_index [.f create image 0 0 -image bg_photo]

image create photo flash_drive_white -file $paths(flash_drive_white)
set flash_drive_image_width [image width flash_drive_white]

set top_banner_pos 3000
set top_banner_description ""
foreach i {1 2 3 4 5 6 7 8 9} {
  set top_banner_description "$top_banner_description   [ui_msg description]"
}
canvas .f.top_banner -background #95b6bf -height 40 \
                     -highlightthickness 0
set topbanner_text_id [
  .f.top_banner create text $top_banner_pos 30                     \
                -justify center -font biggerInfoFont -fill #fefefe \
                -text $top_banner_description -width 10000
]

ttk::label .f.instructions -text [ui_msg instructions] \
                           -wraplength 600             \
                           -padding 20                 \
                           -font infoFont              \
                           -style Instructions.TLabel

ttk::frame .f.version_status -style Status.TLabel
ttk::frame .f.version_status.version -style Status.TLabel
ttk::frame .f.version_status.download -style Status.TLabel
ttk::frame .f.version_status.hostname -style Status.TLabel

ttk::label .f.version_status.version.label               \
           -text [ui_msg "version label"] -font infoFont \
           -style Status.TLabel
ttk::label .f.version_status.version.number -font infoFont \
           -style Status.TLabel
ttk::label .f.version_status.download.label                      \
           -text [ui_msg "download status label"] -font infoFont \
           -style Status.TLabel
ttk::label .f.version_status.download.status -font infoFont \
           -style Status.TLabel
ttk::label .f.version_status.hostname.label               \
           -text [ui_msg "hostname label"] -font infoFont \
           -style Status.TLabel
ttk::label .f.version_status.hostname.value -text [exec hostname] \
                                            -font infoFont        \
                                            -style Status.TLabel

ttk::frame .f.disks
ttk::label .f.disks.noports_message -font infoFont \
                                    -text [ui_msg "waiting usb drives"] \
                                    -padding 40

ttk::frame .f.oper
ttk::frame .f.oper.calibration
ttk::frame .f.oper.calibration.input
ttk::label .f.oper.calibration.input.descr -font infoFont \
           -text [ui_msg "Put the next usb drive into port:"]
ttk::entry .f.oper.calibration.input.entry -font infoFont -justify center \
           -width 16 -textvariable next_calibration_entry \
           -validatecommand [list validate_label %P] \
           -validate all
ttk::label .f.oper.calibration.info -font infoFont

ttk::label .f.oper.support -font infoFont -anchor e -justify right \
                           -padding {0 5 25 10} -text [ui_msg "support info"]

pack .f.top_banner -side top -fill x

pack .f.version_status -side bottom -ipady 14 -fill x
pack .f.version_status.hostname \
     .f.version_status.version  \
     .f.version_status.download -side left -padx 14
pack .f.version_status.hostname.label .f.version_status.hostname.value \
     -side left -padx 5
pack .f.version_status.version.label .f.version_status.version.number \
     -side left -padx 5
pack .f.version_status.download.label .f.version_status.download.status \
     -side left -padx 5

pack .f.oper -side bottom -fill x
pack .f.oper.calibration -padx {100 0}
pack .f.oper.calibration .f.oper.support -side left -expand 1 -fill x
pack .f.oper.calibration.input .f.oper.calibration.info
pack .f.oper.calibration.input.descr .f.oper.calibration.input.entry -side left
pack .f.oper.calibration.input.entry -padx {24 0}

pack .f.disks -side right -after .f.oper
grid .f.disks.noports_message

pack .f.instructions -side left -anchor n -padx 40 -pady 40 -after .f.disks

pack .f -fill both -expand 1

bind . <Configure> {
  if {"%W" eq [winfo toplevel %W]} {
    queue_background_resizing 500 %w %h
  }
}

bind . <Shift-Control-r> { activate_calibration initial }
bind . <Shift-Control-R> { activate_calibration initial }
bind . <Control-r> activate_calibration
bind . <Control-R> activate_calibration
bind . <Control-x> switch_ui_mode
bind . <Control-X> switch_ui_mode

scroll_topbanner
read_port_labels_from_disk
update_image_info_loop
update_diskdevices_loop
check_files
roll_rotating_port_labels 0

if {$calibration_in_progress} {
  switch_ui_mode calibration
} else {
  switch_ui_mode normal
}

download_images

logger -p user.info "gui done, processing events"
