###############################################################################
#
# deployAndTestCe200x.eagle -- Windows CE Deployment & Testing Tool
#
# Written by Joe Mistachkin.
# Released to the public domain, use at your own risk!
#
###############################################################################

package require Eagle

proc usage { error } {
  if {[string length $error] > 0} then {puts stdout $error}

  puts stdout "usage:\
[file tail [info nameofexecutable]]\
[file tail [info script]] \[year\] \[platform\] \[configuration\]\
\[culture\] \[platformId\] \[deviceId\] \[quiet\]"

  #
  # NOTE: Indicate to the caller, if any, that we have failed.
  #
  exit 1
}

#
# NOTE: This procedure will output a diagnostic message, typically to the
#       standard output channel, using the [puts] command unless the global
#       variable "quiet" is non-zero.
#
proc qputs { args } {
  if {![info exists ::quiet] || !$::quiet} then {
    eval puts $args; flush stdout
  }
}

proc showPlatforms { cultureInfo } {
  set datastoreManager [object create -alias \
      Microsoft.SmartDevice.Connectivity.DatastoreManager \
      [$cultureInfo LCID]]

  object foreach -alias platform [$datastoreManager GetPlatforms] {
    qputs stdout [appendArgs \
        "found platform \"" [$platform Id.ToString] "\" with name \"" \
        [$platform Name] \"...]
  }
}

#
# NOTE: This procedure looks up and returns the target device based on the
#       locale, platform Id, and device Id.
#
proc getDevice { cultureInfo platformId deviceId } {
  set datastoreManager [object create -alias \
      Microsoft.SmartDevice.Connectivity.DatastoreManager \
      [$cultureInfo LCID]]

  set platform [$datastoreManager -alias GetPlatform [object create \
      Microsoft.SmartDevice.Connectivity.ObjectId $platformId]]

  if {[string length $deviceId] == 0} then {
    set deviceId [$platform GetDefaultDeviceId]
  }

  set device [$platform -alias GetDevice $deviceId]

  qputs stdout [appendArgs \
      "returning device \"" [$device Name] "\" of platform \"" \
      [$device Platform.ToString] "\" with Id \"" [$device Id.ToString] \
      \"...]

  return $device
}

#
# NOTE: This procedure starts a process on the target device and optionally
#       waits for it to complete.
#
proc startRemoteProcess { device fileName arguments {wait true} } {
  set remoteProcess [$device -alias GetRemoteProcess]

  if {![$remoteProcess Start $fileName $arguments]} then {
    error [appendArgs "could not start remote process \"" $fileName \"]
  }

  if {$wait} then {
    qputs stdout [appendArgs \
        "waiting for remote process " [$remoteProcess GetId] ...]

    while {![$remoteProcess HasExited]} {
      qputs -nonewline stdout .
      after 1000
    }

    qputs stdout ""
    return [$remoteProcess GetExitCode]
  }

  return -1
}

set argc [llength $argv]

if {$argc >= 0 && $argc <= 7} then {
  #
  # NOTE: Setup the default values for all command line options.
  #
  array set default {
            year 2008
        platform {Pocket PC 2003 (ARMV4)}
   configuration Release
         culture en-US
      platformId 3c41c503-53ef-4c2a-8dd4-a8217cad115e
        deviceId {}
           quiet false
  }

  #
  # NOTE: Process all the command line options.  If a command line option
  #       is not present, use the default value.
  #
  set names [list \
      year platform configuration culture platformId deviceId quiet]

  for {set index 0} {$index < [llength $names]} {incr index} {
    set name [lindex $names $index]; set value ""

    if {$argc > $index} then {
      set value [string trim [lindex $argv $index]]
    }

    if {[string length $value] > 0} then {
      set $name $value; set defaultValue false
    } else {
      set $name $default($name); set defaultValue true
    }

    qputs stdout [appendArgs \
        "named parameter \"" $name "\" value is now \"" [set $name] \" \
        [expr {$defaultValue ? " (default)" : ""}] .]
  }

  #
  # NOTE: Grab the culture instance based on the configured culture name.
  #
  set cultureInfo [object invoke -alias System.Globalization.CultureInfo \
      GetCultureInfo $culture]

  #
  # NOTE: Build the list of .NET Compact Framework 2.0 packages that need to
  #       be deployed to the target device, if necessary.
  #
  if {![info exists packages(2005)]} then {
    #
    # NOTE: The three letter Windows language name is needed when building
    #       the default list of .NET Compact Framework packages because one
    #       of them is a localized resource package.
    #
    set language3 [string toupper \
        [$cultureInfo ThreeLetterWindowsLanguageName]]

    #
    # NOTE: The default list of .NET Compact Framework 2.0 packages contains
    #       the .NET Compact Framework 2.0 installation CAB file for ARMV4
    #       on the Pocket PC and its associated resource installation CAB
    #       files.
    #
    set packages(2005) [list \
        abd785f0-cda7-41c5-8375-2451a7cbff26 \
        \\Windows\\NETCFv2.ppc.armv4.cab \
        c0ccf48e-4bfb-4d84-827c-981a595e40b4 \
        [appendArgs \\Windows\\System_SR_ $language3 .cab]]
  }

  #
  # NOTE: Build the list of .NET Compact Framework 3.5 packages that need to
  #       be deployed to the target device, if necessary.
  #
  if {![info exists packages(2008)]} then {
    #
    # NOTE: The two letter ISO language name is needed when building the
    #       default list of .NET Compact Framework packages because one of
    #       them is a localized resource package.
    #
    set language2 [string toupper \
        [$cultureInfo TwoLetterISOLanguageName]]

    #
    # NOTE: The default list of .NET Compact Framework 3.5 packages contains
    #       the .NET Compact Framework 3.5 installation CAB file for ARMV4
    #       on the Pocket PC and its associated resource installation CAB
    #       files.
    #
    set packages(2008) [list \
        abd785f0-cda7-41c5-8375-2451a7cbff37 \
        \\Windows\\NETCFv35.ppc.armv4.cab \
        c0ccf48e-4bfb-4d84-827c-981a595e40c5 \
        [appendArgs \\Windows\\NETCFv35.Messages. $language2 .cab]]
  }

  #
  # NOTE: Save the path where this script is running from.
  #
  set path [file dirname [info script]]

  #
  # NOTE: The base path should be the project root directory, which should
  #       be one level above the one containing this script.
  #
  set base_path [file dirname $path]

  #
  # NOTE: The managed binaries to be deployed to the target device should
  #       be located in the "<base>\bin\<year>\<configuration>Compact\bin"
  #       directory.
  #
  set managed_directory [file join \
      $base_path bin $year [appendArgs $configuration Compact] bin]

  #
  # NOTE: The native binaries to be deployed to the target device should
  #       be located in the "<base>\bin\<year>\<platform>\<configuration>"
  #       directory.
  #
  set native_directory [file join \
      $base_path bin $year $platform $configuration]

  #
  # NOTE: Build the list of all application files that need to be deployed to
  #       the target device, including all the native and managed binaries.
  #
  if {![info exists fileNames]} then {
    #
    # NOTE: Grab the assembly name instance based on the primary managed
    #       assembly file name.  This is needed because the build portion of
    #       the assembly version is used when building the default list of
    #       application files to be deployed to the target device.
    #
    set assemblyName [object invoke -alias System.Reflection.AssemblyName \
        GetAssemblyName [file join $managed_directory System.Data.SQLite.dll]]

    #
    # NOTE: The default list of application files includes the test application
    #       itself, the System.Data.SQLite managed assembly, the SQLite interop
    #       assembly, and the test application configuration file.
    #
    set fileNames [list [file join $managed_directory testce.exe] [file \
        join $managed_directory System.Data.SQLite.dll] [file join \
        $native_directory [appendArgs SQLite.Interop. [format %03d \
        [$assemblyName Version.Build]] .dll]] [file join $managed_directory \
        test.cfg] [file join $managed_directory test.sql]]
  }

  #
  # NOTE: Setup the directory on the target device where the application files
  #       should be deployed to.
  #
  if {![info exists device_directory]} then {
    set device_directory "\\Program Files\\testce\\"
  }

  #
  # NOTE: Load the managed assembly that allows us to communicate with the
  #       target device.  If this fails, the necessary SDK components are
  #       probably not available on this system.
  #
  # NOTE: As of Visual Studio 2013 Update 2 (?), this must specify the full
  #       (versioned) assembly name here in order to make sure the assembly
  #       associated with the Pocket PC 2003 platform(s) gets loaded.
  #
  object load [appendArgs \
      "Microsoft.Smartdevice.Connectivity, Version=9.0.0.0, " \
      "Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"]

  #
  # NOTE: Show the full list of available platforms.
  #
  showPlatforms $cultureInfo

  #
  # NOTE: Lookup the necessary device based on the platform and device Ids.
  #
  set device [getDevice $cultureInfo $platformId $deviceId]

  #
  # NOTE: Attempt to connect to the target device, which may be an emulator.
  #       By default, we attempt to connect to the "Pocket PC 2003 SE Emulator"
  #       device of the "Pocket PC 2003" platform (English).  If this fails,
  #       the target device is probably unavailable, either because it is not
  #       connected or some SDK components are missing.
  #
  $device Connect

  #
  # NOTE: Grab the file deployer instance for the target device.  This will
  #       be used to download packages and send files to the target device.
  #
  set fileDeployer [$device -alias GetFileDeployer]

  #
  # NOTE: If the list of packages related to the configured build year do not
  #       exist, skip this step.
  #
  if {[info exists packages($year)]} then {
    #
    # NOTE: Process each entry in the list of packages to be downloaded to the
    #       target device.  The package list must contain the package Id and
    #       the file name (relative to the target device), in that order, for
    #       each package to be downloaded to the target device.
    #
    foreach {packageId packageFileName} $packages($year) {
      qputs stdout [appendArgs \
          "downloading package \"" $packageId "\" to device..."]

      $fileDeployer DownloadPackage [object create \
          Microsoft.SmartDevice.Connectivity.ObjectId $packageId]

      qputs stdout [appendArgs \
          "installing package file \"" $packageFileName "\" on device..."]

      startRemoteProcess $device wceload.exe [appendArgs "/noui " \
          $packageFileName]
    }
  }

  #
  # NOTE: Process each application file to be sent to the target device.
  #
  foreach fileName $fileNames {
    qputs stdout [appendArgs \
        "sending file \"" $fileName "\" to device..."]

    #
    # NOTE: All the application files are sent to the same directory on the
    #       target device and the SendFile method requires a fully qualified
    #       file name; therefore, grab the file name only from the source file
    #       name and append that to the directory name on the target device.
    #       Using [file join] and/or [file normalize] should be avoided here
    #       because the directory name on the target device is not necessarily
    #       valid a file name on this system and vice versa.
    #
    $fileDeployer SendFile $fileName [appendArgs $device_directory \
        [file tail $fileName]] true false
  }

  #
  # NOTE: Run the test application on the target device in "automatic" mode
  #       (i.e. no user interaction is required) and capture the exit code.
  #       The exit code will be zero upon success (i.e. all tests passed) or
  #       non-zero otherwise.
  #
  set testFileName [file nativename [file join $device_directory testce.exe]]

  #
  # NOTE: The first (and only) argument passed to "testce.exe" on the device
  #       is the auto-close flag.  When non-zero, it will close automatically
  #       upon completion.  Setting this to zero is sometimes useful in order
  #       to more carefully examine the detailed results.
  #
  set exitCode [startRemoteProcess $device $testFileName true]

  #
  # NOTE: If an output file is generated by the remote process, where should
  #       it be saved locally?
  #
  set outputFileName [file join $managed_directory output.txt]

  #
  # NOTE: Attempt to obtain the output file produced by the remote process,
  #       if any.
  #
  catch {
    $fileDeployer ReceiveFile [appendArgs \
        $device_directory [file tail $outputFileName]] $outputFileName false
  }

  #
  # NOTE: Is the target device actually an emulator running on this system?
  #
  set isEmulator [$device IsEmulator]

  #
  # NOTE: We no longer need to be connected to the target device.
  #
  $device Disconnect

  #
  # NOTE: Also, if the device is an emulator, attempt to shutdown the process
  #       containing it now (since we probably caused it to start).
  #
  if {$isEmulator} then {
    #
    # NOTE: Try to find the top-level window for the device emulator process
    #       based on the "LCDDisplay" window class name.  Using this method
    #       of finding the target window is somewhat fragile and may not work
    #       reliably in the future.
    #
    set hWnd [lindex [lindex [info windows LCDDisplay] 0] 0]; # FIXME: ???

    #
    # NOTE: Make sure we found it before trying to lookup the parent process.
    #
    if {[string is integer -strict $hWnd] && $hWnd != 0} then {
      #
      # NOTE: Attempt to lookup the parent process for the target window.
      #
      qputs stdout [appendArgs "found device emulator window handle " $hWnd \
          ", looking up the process Id..."]

      set processId 0; set threadId 0; set error null

      if {[object invoke -flags +NonPublic \
          Eagle._Components.Private.WindowOps GetWindowThreadProcessId \
          [object create IntPtr $hWnd] processId threadId error] eq "Ok" && \
          [string is integer -strict $processId] && $processId != 0} then {
        #
        # NOTE: This is not ideal; however, if we simply try to close the
        #       target window, it will prompt to save state changes and that
        #       requires user interaction.  We never want to save the state;
        #       therefore, just forcibly kill the process containing the
        #       emulator.
        #
        qputs stdout [appendArgs "found device emulator process Id " \
            $processId ", killing..."]

        kill -force $processId
      }
    }
  }

  #
  # NOTE: Print the overall result of running the test application and exit
  #       using the exit code from the test application on the target device.
  #
  qputs stdout [expr {$exitCode == 0 ? "SUCCESS" : "FAILURE"}]
  exit $exitCode
} else {
  usage ""
}
