require 'logger'
require 'yaml'
require 'pathname'
require 'ftools'

# stp libs
require 'session-2.1.3' # 2.1.3
require 'envi-0.0.0'    # 0.0.0


module StpJob
  VERSION = '0.0.0'
  EXIT_SUCCESS = 0
  EXIT_FAILURE = 1

  class MetaError < StandardError
#{{{
    attr :error
    def initialize a = nil, b = nil 
      @error = (Exception === a ? a : b)
      msg = ((not (Exception === a)) ? a : b)
      msg ? super(msg) : super('')
      self
    end
#}}}
  end
  class ConfigurationError < MetaError; end
  class ExecutionError < MetaError; end
  class InterpreterError < MetaError; end
  class CompilationError < MetaError; end

  module Util
#{{{
    def pathname path
#{{{
      test(?e, path) ?
        Pathname.new(File.expand_path(path)).realpath.to_s :
        File.expand_path(path)
#}}}
    end
    module_function :pathname
    public :pathname
    def extension path
#{{{
      pat = %r|\.([^#{ File::SEPARATOR }]+)\s*$|o
      m = pat.match(path)
      m[1]
#}}}
    end
    module_function :extension
    public :extension
    def ext path
#{{{
      pat = %r|\.([^\.]+)\s*$|o
      m = pat.match(path)
      m[1]
#}}}
    end
    module_function :ext
    public :ext
    def emsg e
#{{{
      "%s(%s)" % [e.class, e.message]
#}}}
    end
    module_function :emsg
    public :emsg
    def btrace e
#{{{
      "\n%s\n" % [e.backtrace.join("\n")]
#}}}
    end
    module_function :btrace
    public :btrace
    def errmsg e
#{{{
      emsg(e) << btrace(e)
#}}}
    end
    module_function :errmsg
    public :errmsg
    def find_ols list, cur_dir = false, ols_types = nil 
#{{{
      return [] if list.empty?

      list = list.dup
      ols_types ||= %w(OIS OLS OIF OLF FLS FIS ois ols oif fls fis) 
      
# glob to find only specific file extensions
      glob = '*.{%s}' % ols_types.join(',')

# look in the current directory if _NO_ files or dirs specified
      #list << '.' if list.empty?

# weed out non-existent entries 
      exist, noexist = list.partition{|e| File.exist?(e)}

# separate into dirs/notdirs
      dirs, notdirs = exist.partition{|e| File.directory?(e)}

# add any ols typed files found in named dirs
      dirs.each{|dir| notdirs.push(*(Dir[File.join(dir, glob)]))}

# weed out non-regular (device, etc) files
      files, nonfiles = notdirs.partition{|e| File.file?(e)}

# map into absolute pathname space (no links) so uniq! works correctly
      files.map!{|e| pathname(e)}
      files.uniq!
      files.sort!

# now from these files select only those of ols type
      ols_pat = Regexp.new(ols_types.map{|t| "#{ t }\\s*$"}.join('|'))
      olsfiles, nonolsfiles =
        files.partition{|e| ols_pat.match e}

      olsfiles
#}}}
    end
    module_function :find_ols
    public :find_ols
    def klass
#{{{
      Class === self ? self : self.class
#}}}
    end
    module_function :klass
    public :klass
    def extract_time f
#{{{
      pat = %r|F(\d\d)(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)[^/]+$|o
      m = pat.match f
      raise ArgumentError, "invalid file <#{ f }>" unless 
        m and (yyyy=m[2]) and (mm=m[3]) and (dd=m[4]) and
              (h=m[5]) and (m=m[6])

      # kill leading zeros
      yyyy.gsub! %r/^\s*0+/o, ''
      mm.gsub! %r/^\s*0+/o, ''
      dd.gsub! %r/^\s*0+/o, ''
      h.gsub! %r/^\s*0+/o, ''
      m.gsub! %r/^\s*0+/o, ''
      Time.gm(Integer(yyyy), Integer(mm), Integer(dd), Integer(h), Integer(m))
#}}}
    end
    module_function :extract_time
    public :extract_time
    def extract_sat f
#{{{
      pat = %r|(F\d\d)[^/]+$|o
      m = pat.match f
      raise ArgumentError, "invalid file <#{ f }>" unless 
        m and (sat=m[1])

      sat
#}}}
    end
    module_function :extract_sat
    public :extract_sat
#}}}
  end # module Util


  module Hashify
#{{{
    def hashify(*args)
#{{{
      args.inject({}){|h,a| h.update a}
#}}}
    end
    module_function :hashify
    public :hashify
#}}}
  end


  module Logging
#{{{
    DIV = ("=" * 79) << "\n"
    LINE = ("-" * 79) << "\n"
    LOGGER = Logger.new STDOUT

    def logger= logger
#{{{
      @logger = logger
#}}}
    end
    def logger
#{{{
      ((defined?(@logger)) ? (@logger || Logging::LOGGER) : Logging::LOGGER)
#}}}
    end
    def debug(*args, &block); logger.debug(*args, &block); end
    def info(*args, &block);  logger.info(*args, &block) ; end
    def warn(*args, &block);  logger.warn(*args, &block) ; end
    def error(*args, &block); logger.error(*args, &block); end
    def fatal(*args, &block); logger.fatal(*args, &block); end
    def log_append(*lines); lines.each{|line| logger << line}; end
    def log_div; log_append DIV; end
    def log_line; log_append LINE; end
#}}}
  end # module Logging


  module Inheritable
    def inherited subclass
#{{{
      subclass.class_init
#}}}
    end
  end

  class StringSymbolHash < Hash
#{{{
    class << self
      def symbolize h
#{{{
        h.each{|k, v| symbolize v if Hash === v}
        class << h
          def [] k
            if Symbol === k
              super(k) || super(k.to_s)
            elsif String === k
              super(k) || super(k.intern)
            else
              super
            end
          end
          def fetch k
            if Symbol === k
              super(k) || super(k.to_s)
            elsif String === k
            p k
              super(k) || super(k.intern)
            else
              super
            end
          end
          def values_at(*ks)
            ret = ks.map{|k| self[k]}
            ret << nil if ret.empty?
            ret
          end
        end
#}}}
      end
      def new(*args, &block)
#{{{
        ret = super
        symbolize ret
        ret
#}}}
      end
      alias [] new
    end
    def initialize(*args, &block)
#{{{
      ret = super
      self.class.symbolize(self)
      ret
#}}}
    end
#}}}
  end

  class Config < StringSymbolHash 
#{{{

  # class methods 
    class << self
      include Inheritable
      def class_init
#{{{
        @configurable = nil
#}}}
      end
      def configurable
#{{{
        if @configurable
          @configurable
        elsif defined?(self::CONFIGURABLE)
          self::CONFIGURABLE
        else
          nil
        end
#}}}
      end
      def configurable= allowed 
#{{{
        @configurable = (Array === allowed ? allowed : [allowed])
#}}}
      end
    end
    class_init

  # instance methods
    attr :path, true
    def initialize(*args, &block)
#{{{
      @path = nil 
      @configurable = nil
      arg = args.shift
      case arg
        when Config
          @path = arg.path
          update arg
        when Hash
          update arg
        when String
          @path = arg
          update YAML::load(munge(open(@path).read))
        else
          if arg.respond_to? :read # IO or StringIO
            @path = arg.path if arg.respond_to? :path
            update YAML::load(munge(arg.read))
          else
            super
          end
      end
      validate
      self.class.symbolize(self)
      self
#}}}
    end
    def munge buf
#{{{
      buf.gsub(%r/\t/o,'  ')
#}}}
    end
    def configurable
#{{{
      allowed = [@configurable, self.class.configurable].flatten.compact.uniq
      return((allowed.empty? ? nil : allowed))
#}}}
    end
    def configurable= allowed 
#{{{
      @configurable = (Array === allowed ? allowed : [allowed])
#}}}
    end
    def configure obj
#{{{
      each do |prop, value|
        meth = "#{ prop }=".intern
        begin
          obj.send meth, value 
        rescue StandardError => e
          msg = "configuration of <#{ prop.inspect }> => <#{ value.inspect }> failed"
          raise ConfigurationError.new(e, msg)
        end
      end
#}}}
    end
    def validate
#{{{
      allowed = configurable
      return self if allowed.nil?
      keys.each do |key|
        included = false
        ks = [key]
        ks << key.to_s if Symbol === key
        ks << key.intern if String === key
        ks.each{|k| (included = true and break) if allowed.include?(k)}
        unless included
          msg = "configuration of <#{ self.class }>[<#{ key.inspect }>] not permitted"
          raise ConfigurationError, msg
        end
      end
      self
#}}}
    end
#}}}
  end # class Config

  module Configurable
#{{{
    attr :config
    attr :configured
    alias configured? configured
    def configure(*args)
#{{{
      klass = (Class === self ?  self : self.class)
      conf =
        if defined?(klass::Config)
          klass::Config.new(StpJob::Hashify::hashify(*args))
        else
          StpJob::Config.new(StpJob::Hashify::hashify(*args))
        end
      @config = conf
      @config.configure self
      @configured = true
      self
#}}}
    end
#}}}
  end


  class AbstractJob
#{{{
    include Util
    include Logging
    include Hashify

    class Config < StpJob::Config; end

    # class methods 
    class << self
#{{{
      include Inheritable
      attr :name, true
      attr :n_name, true
      def class_init
#{{{
      #puts "================================================"
      #puts "class_init called on #{ self }"
      #puts "================================================"
      #puts
        @name = self.to_s.split(%r/[:][:]?/o).last
        @config = nil 
        @n_name = 0
#}}}
      end
      def gen_name
#{{{
        ret = "#{ self.name.downcase }_#{ self.n_name }"
        @n_name += 1
        ret
#}}}
      end
      def gen_program
#{{{
        defined?(self::PROGRAM) ? self::PROGRAM : self.name.downcase
#}}}
      end
#}}}
    end
    class_init

    attr :opts
    attr :name, true
    attr :program, true
    attr :quiet, true
    alias quiet? quiet
    attr :noop, true
    alias noop? noop

    def initialize(*options)
#{{{
      #@opts = StringSymbolHash.new
      #@opts.update(hashify(*options))
      @opts = klass::Config.new(hashify(*options))
      @name = opts[:name] || klass.gen_name.downcase
      @program = opts[:program] || klass.gen_program
      @logger = opts[:logger]
      @quiet = opts[:quiet]
      @noop = opts[:noop]
      self
#}}}
    end
    def strerr errno
#{{{
      if errno == 0
        label('exit_success')
      else
        msg = nil
        if defined?(STRERR) and Hash === STRERR
          msg = STRERR[errno]
        end
        msg ||= label("unknown errno <#{ errno }>")
      end
      label msg
#}}}
    end
    def quietly
#{{{
      was_already_quiet = quiet? 
      if logger.debug?
        begin
          @quiet = false
          yield
        ensure
          @quiet = true if was_already_quiet
        end
      else
        begin
          @quiet = true
          yield
        ensure
          @quiet = false unless was_already_quiet
        end
      end
#}}}
    end
    def announce
#{{{
      ret = nil
      begin
        info{ label('starting...') }
        ret = yield
        info{ label('finished') }
      rescue Exception
        info{ label('failed') }
        raise
      end
      ret
#}}}
    end
    def label msg
#{{{
      "#{ name } - #{ msg }"
#}}}
    end
    def success?
#{{{
      if self.respond_to? :expected
        self.expected.each do |e|
          unless test ?s, e 
            raise ExecutionError, "expected file <#{ e }> was not created"
          end
        end
      else
        true
      end
#}}}
    end

    # these must be implemented!
    def run; raise NotImplementedError; end
#}}}
  end # class AbstractJob


  class AbstractInterpreterJob < AbstractJob
#{{{

  # class methods
    class << self
#{{{
      def class_init
#{{{
        super
        @interpreter = nil
        @path = nil
#}}}
      end
      def interpreter
#{{{
        if @interpreter
          @interpreter
        else
          if defined? self::INTERPRETER
            self::INTERPRETER
          else
            raise "no interpreter defined for <#{ self }>"
          end
        end
#}}}
      end
      attr :path
      def path= value
#{{{
        @path = munge_path(value) 
#}}}
      end
      def munge_path value
#{{{
        case value
          when String
            value.strip.split %r/:/io
          when Array
            value
          else
            value.to_s.split %r/:/io
        end.uniq
#}}}
      end
#}}}
    end
 
  # instance methods
     attr :interpreter
     attr :path
     attr :status, true
     def initialize(*opts)
 #{{{
       super
       @interpreter = nil 
       @in_session = false
       @status = -1
       @path = @opts[:path]
 #}}}
     end
     def session(&block)
 #{{{
       begin
         @in_session = true
         @interpreter = self.class.interpreter.new
         set_path(self.class.path + get_path) if(self.class.path)
         set_path(@path + get_path) if(@path)
         debug{ "path <#{ get_path.join(':') }>" } unless quiet?
         yield(@interpreter)
       ensure
         @in_session = false 
         @interpreter.close! if defined? @interpreter and @interpreter
         @interpreter = nil
       end
 #}}}
     end
     def execute command
 #{{{
       raise ExecutionError, "not in session" unless @in_session
       stdout = stderr = nil
       unless quiet?
         info{ "cmd <#{ command }>" }
         log_line
         stdout, stderr = @interpreter.execute(command) do |o, e|
           log_append o if o
           log_append e if e
         end
         log_line
       else
         stdout, stderr = @interpreter.execute(command)
       end
       return stdout, stderr
 #}}}
     end
     def path= value
 #{{{
       @path = self.class.munge_path(value) 
 #}}}
     end
     def set_path value
 #{{{
       value = self.class.munge_path(value)
       @interpreter.path = (value + get_path).uniq 
 #}}}
     end
     private :set_path
     def get_path
 #{{{
       @interpreter.path.uniq
 #}}}
     end
     private :get_path
#}}}
  end # class AbstractInterpreterJob


  class AbstractIDLJob < AbstractInterpreterJob
#{{{
    INTERPRETER = Session::IDL
    alias idl_path path
    alias idl_path= path=
    attr :errno
    def initialize(*opts)
#{{{
      super
      @path ||= @opts[:idl_path]
#}}}
    end
    def set_errno
#{{{
      quietly{ execute('errno = -1') }
      errno = -1
#}}}
    end
    def get_errno
#{{{
      stdout, stderr = quietly{ execute('print, errno') }
      errno = stdout
      unless errno =~ /^\s*-?\d+\s*$/o
        raise InterpreterError, "errno <#{ errno.inspect }>"
      end
      errno = Integer errno
      case errno
        when 0 
          # success
        when -1
          raise CompilationError, "<#{ name }>" 
        else
          raise "errno <#{ errno }> - msg <#{ strerr errno }>"
      end
      @errno = errno
#}}}
    end
    def to_struct hash
#{{{
      '{' + hash.map{|kv| kv.join(':')}.join(',') + '}'
#}}}
    end
    def to_array array
#{{{
      '[' + array.map{|a| "'#{ a }'"}.join(',') + ']'
#}}}
    end
#}}}
  end # class AbstractIDLJob 


  class AbstractShellJob < AbstractInterpreterJob
#{{{
   INTERPRETER = Session::Shell
#}}}
  end # class AbstractIDLJob 


# TODO - conver *parms to StringSymbolHash's and validate
# use strerr
  class Write_Flags < AbstractIDLJob
 #{{{
    STRERR = {
#{{{
      -1 => 'write_flags did not compile',
      1  => 'file not found',
      2  => 'file extension not an allowed file type',
      3  => 'unable to open file for writing',
#}}}
    }

    OPTIONS =
#{{{
      :path, :idl_path, :program,
      :flagfile, :hdrfile,         
      :solar_elev_file, :lunar_illum_file,
      :daytime_solar_elev_cutoff, :glare_solar_elev_cutoff, :lunar_illum_cutoff,
      :bsl_nlights, :bsl_nzeros,
      :glare_params, :glare_trim,
      :lightfilt_params1, :rm_light_size1,
      :lightfilt_params2, :rm_light_size2
#}}}

    OPTIONS.each{|kw| attr kw}
  
    attr :olsfile
    attr :expected

    def initialize(olsfile, *options)
#{{{
      self.olsfile = olsfile
      
      super(*options)

      illegal = 
        opts.keys.map{|k| k.to_s} - OPTIONS.map{|k| k.to_s}
      unless illegal.empty?
        raise ArgumentError, "illegal options <#{ illegal.join ',' }>"
      end

      OPTIONS.each do |opt|
        value = opts[opt]
        self.send "#{ opt }=".intern, value if value
      end

      self.flagfile ||= gen_flagfile
      self.hdrfile ||= gen_hdrfile
      self.expected = [self.flagfile, self.hdrfile]
      self
#}}}
    end
    def olsfile= olsfile
#{{{
      raise ArgumentError, "olsfile <#{ olsfile }>" unless test(?r, olsfile)
      @olsfile = pathname(olsfile)
#}}}
    end
    def expected= expected
#{{{
      raise ArgumentError, expected.class.to_s unless Array === expected
      @expected = expected
#}}}
    end
    def flagfile= flagfile
#{{{
      @flagfile = pathname flagfile
#}}}
    end
    def hdrfile= hdrfile
#{{{
      @hdrfile = pathname hdrfile
#}}}
    end
    def solar_elev_file= value
 #{{{
      @solar_elev_file =
        case value 
          when String, TrueClass, FalseClass, NilClass
            value
          when Fixnum
            unless [0,1].include? value
              raise ArgumentError, "solar_elev_file <#{ value.inspect }>"
            end
            value
          else
            raise TypeError, "solar_elev_file <#{ value.class }>"
        end
 #}}}
    end
    def lunar_illum_file= value
 #{{{
      @lunar_illum_file =
        case value 
          when String, TrueClass, FalseClass, NilClass
            value
          when Fixnum
            unless [0,1].include? value
              raise ArgumentError, "lunar_illum_file <#{ value.inspect }>"
            end
            value
          else
            raise TypeError, "lunar_illum_file <#{ value.class }>"
        end
 #}}}
    end
    def daytime_solar_elev_cutoff= value
#{{{
      @daytime_solar_elev_cutoff = (value ? Float(value) : value)
#}}}
    end
    def glare_solar_elev_cutoff= value
#{{{
      @glare_solar_elev_cutoff = (value ? Float(value) : value)
#}}}
    end
    def lunar_illum_cutoff= value
#{{{
      @lunar_illum_cutoff = (value ? Float(value) : value)
#}}}
    end
    def bsl_nlights= value
#{{{
      @bsl_nlights = (value ? Integer(value) : value)
#}}}
    end
    def bsl_nzeros= value
#{{{
      @bsl_nzeros = (value ? Integer(value) : value)
#}}}
    end
    def glare_params= value
#{{{
      raise ArgumentError unless Hash === value
      @glare_params = value
#}}}
    end
    def glare_trim= value
#{{{
      @glare_trim = (value ? Integer(value) : value)
#}}}
    end
    def lightfilt_params1= value
#{{{
      raise ArgumentError unless Hash === value
      @lightfilt_params1 = value
#}}}
    end
    def rm_light_size1= value
 #{{{
      unless Fixnum === value
        raise TypeError, "rm_light_size1 <#{ value.class }>"
      end
      @rm_light_size1 = value
 #}}}
    end
    def lightfilt_params2= value
#{{{
      raise ArgumentError unless Hash === value
      @lightfilt_params2 = value
#}}}
    end
    def rm_light_size2= value
 #{{{
      unless Fixnum === value
        raise TypeError, "rm_light_size2 <#{ value.class }>"
      end
      @rm_light_size2 = value
 #}}}
    end
    def flagfilehdr
#{{{
      "#{ flagfile }.hdr"
#}}}
    end
    def gen_flagfile
 #{{{
      raise TypeError, "olsfile nil!" unless olsfile
      pathname "#{ olsfile }flag"
 #}}}
    end
    def gen_hdrfile
 #{{{
      raise TypeError, "olsfile nil!" unless olsfile
      pathname "#{ olsfile }flag.hdr"
 #}}}
    end
    def gen_command
 #{{{
      command = "%s,'%s','%s'" % [program, olsfile, flagfile] 
  
      if solar_elev_file
        case solar_elev_file
          when String
            command << ",solar_elev_file='#{ solar_elev_file }'"
          when TrueClass
            command << ",solar_elev_file=1"
          when FalseClass
            command << ",solar_elev_file=0"
          when Fixnum
            command << ",solar_elev_file=#{ solar_elev_file }"
        end
      end
  
      if lunar_illum_file
        case lunar_illum_file
          when String
            command << ",lunar_illum_file='#{ lunar_illum_file }'"
          when TrueClass
            command << ",lunar_illum_file=1"
          when FalseClass
            command << ",lunar_illum_file=0"
          when Fixnum
            command << ",lunar_illum_file=#{ lunar_illum_file }"
        end
      end
      if daytime_solar_elev_cutoff
        command << ",daytime_solar_elev_cutoff=#{ daytime_solar_elev_cutoff }"
      end
      if glare_solar_elev_cutoff
        command << ",glare_solar_elev_cutoff=#{ glare_solar_elev_cutoff }"
      end
      if lunar_illum_cutoff
        command << ",lunar_illum_cutoff=#{ lunar_illum_cutoff }"
      end
      if bsl_nlights
        command << ",bsl_nlights=#{ bsl_nlights }"
      end
      if bsl_nzeros
        command << ",bsl_nzeros=#{ bsl_nzeros }"
      end
      if glare_params
        command << ",glare_params=#{ to_struct glare_params }"
      end
      if glare_trim
        command << ",glare_trim=#{ glare_trim }"
      end
      if lightfilt_params1
        command << ",lightfilt_params1=#{ to_struct lightfilt_params1 }"
      end
      if rm_light_size1
        command << ",rm_light_size1=#{ rm_light_size1 }"
      end
      if lightfilt_params2
        command << ",lightfilt_params2=#{ to_struct lightfilt_params2 }"
      end
      if rm_light_size2
        command << ",rm_light_size2=#{ rm_light_size2 }"
      end
  
      command << ",exit_code=errno"
      command
 #}}}
    end
    def run
 #{{{
      debug{ "olsfile <#{ olsfile }>" }
      debug{ "flagfile <#{ flagfile }>" }
      debug{ "hdrfile <#{ hdrfile }>" }

      announce do 
        session do
          unless noop?
            set_errno
            execute(gen_command)
            status = get_errno
          else
            debug{ "cmd (#{ gen_command }>" }
          end
        end
        success?
      end

      return self
 #}}}
    end
 #}}}
  end # class Write_Flags


  class GLL < AbstractShellJob
#{{{
    # error codes
#{{{
    DMSPNL_USAGE_ERROR          = 2
    DMSPNL_FILE_IO_ERROR        = 3
    DMSPNL_MEMORY_ALLOC_ERROR   = 4
    DMSPNL_NO_DATA              = 5
    DMSPNL_PROCESSING_ERROR     = 6
    DMSPNL_CONTEXT_ERROR        = 7
    DMSPNL_COMPRESS_ERROR       = 8
    DMSPNL_UNCOMPRESS_ERROR     = 9
    DMSPNL_DEM_ERROR            = 10
    DMSPNL_NOT_IMPLEMENTED      = 11
    DMSPNL_UNSUPPORTED_PLATFORM = 12
#}}}

    STRERR = { 
#{{{
      EXIT_FAILURE                => 'EXIT_FAILURE',
      DMSPNL_USAGE_ERROR          => 'DMSPNL_USAGE_ERROR',
      DMSPNL_FILE_IO_ERROR        => 'DMSPNL_FILE_IO_ERROR',
      DMSPNL_MEMORY_ALLOC_ERROR   => 'DMSPNL_MEMORY_ALLOC_ERROR',
      DMSPNL_NO_DATA              => 'DMSPNL_NO_DATA',
      DMSPNL_PROCESSING_ERROR     => 'DMSPNL_PROCESSING_ERROR',
      DMSPNL_CONTEXT_ERROR        => 'DMSPNL_CONTEXT_ERROR',
      DMSPNL_COMPRESS_ERROR       => 'DMSPNL_COMPRESS_ERROR',
      DMSPNL_UNCOMPRESS_ERROR     => 'DMSPNL_UNCOMPRESS_ERROR',
      DMSPNL_DEM_ERROR            => 'DMSPNL_DEM_ERROR',
      DMSPNL_NOT_IMPLEMENTED      => 'DMSPNL_NOT_IMPLEMENTED',
      DMSPNL_UNSUPPORTED_PLATFORM => 'DMSPNL_UNSUPPORTED_PLATFORM',
      127                         => 'GLL COMMAND NOT FOUND',
#}}}
    }

    # attributes
#{{{
    attr :olsfile, true
    attr :olsflagfile, true
    attr :prefix, true
    attr :visfile, true
    attr :visfilehdr, true
    attr :tirfile, true
    attr :tirfilehdr, true
    attr :flagfile, true
    attr :flagfilehdr, true
    attr :linesfile, true
    attr :linesfilehdr, true
    attr :samplesfile, true
    attr :samplesfilehdr, true
    attr :verbose, true
    attr :rm_edge, true
    attr :dem_path, true
    attr :latitude_limits, true
    attr :limits, true
    attr :direction, true
    attr :ols_flag_definitions, true
#}}}

    def initialize(olsfile, *options)
#{{{
      super(*options)
      opts.configure self

      self.olsfile = olsfile
      self.olsflagfile = opts[:olsflagfile] || 'none' 
      self.prefix = opts[:prefix] || gen_prefix 
      self.visfile = "#{ prefix }.vis"
      self.visfilehdr = "#{ prefix }.vis.hdr"
      self.tirfile = "#{ prefix }.tir"
      self.tirfilehdr = "#{ prefix }.tir.hdr"
      self.flagfile = "#{ prefix }.flag"
      self.flagfilehdr = "#{ prefix }.flag.hdr"
      self.linesfile = "#{ prefix }.lines"
      self.linesfilehdr = "#{ prefix }.lines.hdr"
      self.samplesfile = "#{ prefix }.samples"
      self.samplesfilehdr = "#{ prefix }.samples.hdr"
      self
#}}}
    end
    def run
#{{{
      announce do
        debug {"olsfile = <#{ olsfile }>" }
        debug {"olsflagfile = <#{ olsflagfile }>" }
        debug {"prefix = <#{ prefix }>" }

return self if noop?
        session do |sh|
          execute(gen_command)
          unless sh.status == EXIT_SUCCESS
            raise ExecutionError, "failed with <#{ $?.exitstatus }>"
          end
        end
      end

      success?

      self
#}}}
    end
    def gen_command
#{{{
    # note that we pick up gll from $PATH
      command = '%s ' % [program]

    # add options
      if verbose 
        command << ('%s ' % ['-verbose']) 
      end
      if rm_edge
        command << ('%s ' % ['-rmEdge']) 
      end
      if latitude_limits 
        command << ('-latitudeLimits %s %s ' % latitude_limits) 
      end
      if limits 
        command << ('-limits %s %s %s %s ' % limits) 
      end
      if direction
        command << ('-direction %s ' % [direction]) 
      end
      if dem_path
        command << ('-DEMPath %s ' % [dem_path]) 
      end
      if ols_flag_definitions
        command << ('-OLSFlagDefinitions %s ' % [ols_flag_definitions]) 
      end

    # files to work on
      command << 
        ('%s %s %s ' % [olsfile, olsflagfile, prefix])
#}}}
    end
    def expected
#{{{
      [
        visfile, visfilehdr, tirfile, tirfilehdr, flagfile, flagfilehdr,
        linesfile, linesfilehdr, samplesfile, samplesfilehdr
      ]
#}}}
    end
    def success?
#{{{
      expected.each do |f|
        raise ExecutionError, "#{ f } was not produced" unless test(?s, f)
      end
#}}}
    end
    def gen_prefix
#{{{
      raise TypeError, "olsfile is nil!" unless olsfile
      pfx = olsfile.gsub %r|\.[^/.]*$|, ''
      case direction.to_s 
        when %r/^\s*asc/io
          pfx + '.asc'
        when %r/^\sdes/io
          pfx + '.des'
        else
          pfx
      end
#}}}
    end
#    def rm_edge= value
##{{{
#      raise TypeError, "rm_edge must be 'true' or 'false'" unless TrueClass === value or
#                                                     FalseClass === value
#      @rm_edge = value
##}}}
#    end
#    def dem_path= value
##{{{
#      value = value.to_s
#
#      raise ArgumentError, "dem_path #{ value } does not exist as a readable directory" unless 
#        test ?d, value and test ?r, value
#
#      @dem_path = value
##}}}
#    end
#    def latitude_limits= value
##{{{
#      raise ArgumentError, "cannot specify both " +
#          "limits #{ @limits.inspect } and " +
#          "latitude_limits #{ value.inspect }" if @limits
#
#      raise TypeError, "latitude_limits must be an array of two numbers" unless 
#        Array === value
#
#      raise ArgumentError "latitude_limits must be an array of two numbers" unless 
#        value.size == 2
#
#      raise TypeError "latitudes must be numeric" unless 
#        Numeric === value[0] and Numeric === value[1]
#
#      @latitude_limits = value
##}}}
#    end
#    def limits= value
##{{{
#      raise ArgumentError, "cannot specify both " +
#          "limits #{ value.inspect } and " +
#          "latitude_limits #{ @latitude_limits.inspect }" if @latitude_limits
#
#      raise TypeError, "limits must be an array of four numbers not #{ value.inspect }" unless 
#        Array === value and value.size == 4 and
#        Numeric === value[0] and Numeric === value[1] and
#        Numeric === value[2] and Numeric === value[3]
#
#      die "latitudes must be -90 <= and <= +90" unless
#        value[0] >= -90 and value[0] <= +90 and
#        value[2] >= -90 and value[2] <= +90
#
#      die "longitudes must be -180 <= and <= +180" unless
#        value[1] >= -180 and value[1] <= +180 and
#        value[3] >= -180 and value[3] <= +180
#
#      @limits = value
##}}}
#    end
#    def direction= value
##{{{
#      die "direction must be ascending | descending" unless 
#        value =~ %r/ascending|descending/io
#
#      @direction = value
##}}}
#    end
#    def ols_flag_definitions= value
##{{{
#      value = value.to_s
#
#      die "ols_flag_definitions #{ value } does not exist as a readable file" unless 
#        File.file? value and File.readable? value
#
#      @ols_flag_definitions = value
##}}}
#    end
#}}}
  end # class GLL


  class Copy_Envi_Headers < AbstractJob
#{{{
    attr :src, true
    attr :dest, true
    attr :copy, true
    attr :delete, true
    def initialize(src, dest, *options) 
#{{{
      super(*options)
      opts.configure self
      self.src = src
      self.dest = dest
#}}}
    end
    
    def run
#{{{
      announce do
        debug{ "src <#{ src }>" }
        debug{ "dest <#{ dest }>" }

        srchdr = ENVI::Header.new :path => src
        desthdr = ENVI::Header.new :path => dest 

        debug{ "copy <#{ copy.inspect }>" }
        copy.each do |key|
          pat = gen_pat key
          if srchdr.has_key? pat
            if desthdr.has_key? pat
              debug{ "deleting <#{ key }>" }
              desthdr.delete pat
            end
            debug{ "copying <#{ key }>" }
            desthdr[key] = srchdr[pat]
          end
        end

        debug{ "delete <#{ delete.inspect }>" }
        delete.each do |key|
          pat = gen_pat key
          if desthdr.has_key? pat
            debug{ "deleting <#{ key }>" }
            desthdr.delete pat
          end
        end

        debug{ "srchdr <\n#{ srchdr.to_header }\n>" }
        debug{ "desthdr <\n#{ desthdr.to_header }\n>" }

  return self if noop?
        open(dest,'w'){|f| f.puts desthdr.to_header}
        debug{ "updated <#{ dest }>" }
      end
      self
#}}}
    end

    def gen_pat key
#{{{
      k = "#{ key }"
      k.strip!
      k.gsub! %r/\s+/, '\s+'
      %r/^\s*#{ k }\s*$/i
#}}}
    end
#}}}
  end # Copy_Envi_Headers


  class Apply_Clouds < AbstractIDLJob
#{{{
    STRERR = {
      1 => 'unknown error',
      2 => 'file not found',
      3 => 'input file size does not match dimensions in envi header',
    }

    attr :flagfile, true

    def initialize(flagfile, *options)
#{{{
      super(*options)
      opts.configure self
      self.flagfile = flagfile
#}}}
    end
    def run
#{{{

      announce do
  return self if noop?
        session do
          set_errno
          execute(gen_command)
          @status = get_errno
        end

        success?
      end

      self
#}}}
    end
    def success?
#{{{
      test ?s, flagfile
#}}}
    end
    def gen_command
#{{{
      command = "%s,'%s'" % [program, flagfile] 
      command << ",exit_code=errno"
      command
#}}}
    end
#}}}
  end # class Apply_Clouds 


  class Mosaic_Geo_Files < AbstractIDLJob
#{{{
    ORBIT_PERIOD = 105 * 60 * 60 # 105 minutes
    attr :files, true
    attr :type, true
    attr :outlimits, true
    attr :no_data_val, true
    attr :orbit_period, true
    attr :mosaic_files, true
    attr :mosaic_hdr_files, true
    attr :prefixes, true
    def initialize(files, type, *options)
#{{{
      super(*options)
      opts.configure self
      self.files = files
      case type.to_s
        when /vis/
          self.no_data_val = 255
          self.type = 'vis'
        when /tir/
          self.no_data_val = 255
          self.type = 'tir'
        when /flag/
          self.no_data_val = 2 ** 15
          self.type = 'flag'
        when /lines/
          self.no_data_val = 0
          self.type = 'lines'
        when /samples/
          self.no_data_val = 0
          self.type = 'samples'
        else
          raise ArgumentError, "unsupported type <#{ type.inspect }>"
      end
      self.orbit_period = ORBIT_PERIOD
      self.mosaic_files = []
      self.mosaic_hdr_files = []
      self.prefixes = []
#}}}
    end
    def run
#{{{
      announce do
        debug{ "files <#{ files.inspect }>" }
        debug{ "orbit_period <#{ orbit_period }>" }
        groups = Groups.new files, orbit_period
        groups.each do |group|
          debug{ "group files <#{ group.files.inspect }>" }
          debug{ "group prefix <#{ group.prefix }>" }
          session do
            #set_errno
            command = gen_command group.files, group.prefix
  unless self.noop?
            execute(command)
  end
            #@status = get_errno
          end

          mosaic_file, mosaic_hdr_file = gen_expected group.prefix

          raise ExecutionError, group.prefix unless
            test(?s, mosaic_file) and test(?s, mosaic_hdr_file)

          self.mosaic_files << mosaic_file
          self.mosaic_hdr_files << mosaic_hdr_file
          self.prefixes << group.prefix
        end

        debug{ "mosaic_files <#{ mosaic_files.inspect }>" }
        debug{ "mosaic_hdr_files <#{ mosaic_hdr_files.inspect }>" }
      end

      self
#}}}
    end
    def gen_expected prefix
#{{{
      ["#{ prefix }", "#{ prefix }.hdr"]
#}}}
    end
    def gen_command list, prefix
#{{{
      command = "#{ program } "
      command << ",#{ to_array(list) } "
      command << ",'#{ prefix }' "
      command << ",outlimits=[#{ outlimits.join(',')  }] "
      command << ",no_data_val=#{ no_data_val } "
      #command << ",exit_code=errno"
      command
#}}}
    end

    class Groups
#{{{
      include Util
      attr :chunks, true
      def initialize files, orbit_period
#{{{
        self.chunks = [ Chunk.new ]

        files.map!{|f| pathname f}
        files.uniq!
        files.sort!{|a,b| extract_time(a) <=> extract_time(b)} 

        files.each do |f|
          chunk = chunks[-1]
          if chunk.files.empty?
            chunk.files << f
          else
            last = chunk.files[-1]
            if delta_t(last, f) <= orbit_period
              chunk.files << f
            else
              #chunk.finish
              chunk = Chunk.new
              chunk.files << f
              chunks << chunk
            end
          end
        end

        chunks.each{|c| c.finish}
#}}}
      end
      def delta_t a, b
#{{{
        (extract_time(b).to_i - extract_time(a).to_i).abs
#}}}
      end
      def each
#{{{
        chunks.each{|c| yield c}
#}}}
      end
      class Chunk
#{{{
        include Util
        attr :files
        attr :prefix
        def initialize
#{{{
          @files = []
          @prefix = nil
#}}}
        end
        def finish
#{{{
# TODO - name based on first day
# TODO - naming based on multiple satelites???
          t0 = nil
          e0 = nil
          s0 = nil
          of = nil
          files.each do |f|
            e0 ||= extension f
            t0 ||= extract_time f
            s0 ||= extract_sat f
            t1 = extract_time f
            e1 = extension f
            s1 = extract_sat f
            if of
# TODO - this is redundant
#              if t0.year != t1.year and 
#                 t0.month != t1.month and 
#                 t0.day != t1.day 
#                raise ExecutionError, "<#{ f }> not same period as <#{ of }>"
#              end
              if e0 != e1
                raise ExecutionError, "<#{ f }> does not have same ext as <#{ of }>"
              end
# TODO - make this configurable
#              if s0 != s1
#                raise ExecutionError, "<#{ f }> does not have same sat as <#{ of }>"
#              end
            end
            of = f
          end

          sat = s0
          yyyy = "%04.4d" % t0.year 
          mm = "%02.2d" % t0.month 
          dd = "%02.2d" % t0.day
          ext = e0
          @prefix = "#{ sat }#{ yyyy }#{ mm }#{ dd }.#{ ext }"
#}}}
        end
#}}}
      end # class Chunk
#}}}
    end # class Groups
#}}}
  end # class Mosaic_Geo_Files


  class Fire_Product < AbstractIDLJob
#{{{
    attr :prefix, true

    def initialize(prefix, *options)
#{{{
      super(*options)
      opts.configure self
      self.prefix = prefix 
#}}}
    end
    def run
#{{{
      announce do

  return self if noop?
        session do
          #set_errno
          execute(gen_command)
          #@status = get_errno
        end

        #success?
      end

      self
#}}}
    end
    def success?
#{{{
      #test ?s, flagfile
#}}}
    end
    def gen_command
#{{{
      command = "%s,'%s'" % [program, prefix] 
      #command << ",exit_code=errno"
      command
#}}}
    end
#}}}
  end # class Fire_Product


# TODO - make this StpFire
  class Fires < AbstractJob
#{{{
    attr :olsfiles, true
    attr :path, true
    attr :idl_path, true
    attr :configs, true

    def initialize(olsfiles, *options) 
#{{{
      super(*options)
      self.olsfiles = olsfiles
      self.path = opts[:path]
      self.idl_path = opts[:idl_path]
      self.configs = {}
      [:write_flags, :gll, :copy_envi_headers, :apply_clouds, :mosaic_geo_files].each do |key|
        configs[key] = opts[key]
      end
#}}}
    end
    def set_class_path c
#{{{
      raise ArgumentError, c.class unless Class === c

      if AbstractIDLJob === c
        c.idl_path = self.idl_path if self.idl_path
      elsif AbstractShellJob
        c.path = self.path if self.path
      end
#}}}
    end
    def run
#{{{
      announce do
        visfiles = [] 
        tirfiles = [] 
        flagfiles = [] 

        [Write_Flags,Apply_Clouds,Mosaic_Geo_Files,Fire_Product].each do |c|
          set_class_path c
        end
        [GLL].each do |c|
          set_class_path c
        end

        write_flags= gll= copy= apply= mosaic= fire_product= nil


        debug{ "olsfiles <#{ olsfiles.inspect }>" }

        olsfiles.each do |olsfile|

        # write_flags
          write_flags = Write_Flags.new olsfile, 
                                        opts[:write_flags]
          write_flags.logger = logger
#write_flags.noop = true
          write_flags.run

        # gll
          gll = GLL.new olsfile, 
                        opts[:gll], 
                        :olsflagfile => write_flags.flagfile
          gll.logger = logger
#gll.noop = true
          gll.run

          visfiles << gll.visfile
          tirfiles << gll.tirfile
          flagfiles << gll.flagfile

        # copy_envi_headers
          src, dest = write_flags.flagfilehdr, gll.flagfilehdr 
          copy = Copy_Envi_Headers.new src, dest, 
                                       opts[:copy_envi_headers] 
          copy.logger = logger
#copy.noop = true
          copy.run

        # apply_clouds
          apply = Apply_Clouds.new gll.flagfile, 
                                   opts[:apply_clouds]
#apply.noop = true
          apply.logger = logger
          apply.run
        end


      # mosaic_geo_files
        debug{ "visfiles <#{ visfiles.inspect }>" }
        debug{ "tirfiles <#{ tirfiles.inspect }>" }
        debug{ "flagfiles <#{ flagfiles.inspect }>" }

        types = {
          :vis => visfiles, :tir => tirfiles, :flag => flagfiles,
        }
        types.each do |type, files|
          mosaic = Mosaic_Geo_Files.new files, type,
                                        opts[:mosaic_geo_files]
#mosaic.noop = true
          mosaic.logger = logger
          mosaic.run
        end

# TODO - create better way of determining this?
        prefixes = mosaic.prefixes.map{|pfx| pfx.gsub(%r/[\.][^\.]+\s*$/,'')}.uniq
        debug{ "prefixes <#{ prefixes.inspect }>" }

        prefixes.each do |prefix|
          fire_product = Fire_Product.new prefix,
                                          opts[:fire_product]
#fire_product.noop = true
          fire_product.logger = logger
          fire_product.run
        end
      end
#}}}
    end
#}}}
  end
end # module StpJob
