# Skycrane package, initiated 18.08.2012

# Inspired August 05, 2012 JPL Time, by the upper stage called Skycrane that delivered Curiosity onto
# the surface of Mars.

package.checkclib('skycrane');  # has C dynamic link library been found ?

skycrane.initstring :=
   'Skycrane package v1.2.4 as of October 21, 2014\n';

create table skycrane.aux;

skycrane.aux.fileop := proc(fn :: procedure, a :: string, b :: string, verbose) is  # Agena 1.7.6
   local aattribs, battribs, p, topp, r;
   if verbose <> true then verbose := false fi;
   a := skycrane.trimpath(a);
   b := skycrane.trimpath(b);
   p := a split '/';
   topp := top(p);
   if topp <> null and ('*' in topp or '?' in topp or topp = '.') then
      # is there a joker or dot in the end of the path ?
      pop top from p;
      p := join(p, '/');
      for i in os.list(a, 'files') do
         if skycrane.aux.fileop(fn, p & '/' & i, b) = fail then
            return fail, i
         fi;
         if verbose then print(p & '/' & i) fi
      od;
      return true
   fi;
   aattribs := os.fstat(a);
   if aattribs = fail then
      return fail, a
   fi;
   if aattribs.mode = 'DIR' then
      for i in os.list(a, 'files') do
         if skycrane.aux.fileop(fn, a & '/' & i, b) = fail then
            return fail, i
         fi
         if verbose then print(a & '/' & i) fi
      od;
      return true
   fi;
   battribs := os.fstat(b);
   if battribs :: table and battribs.mode = 'DIR' then
      local prep := a split '/';
      b := b & '/' & is prep = seq(a) then a else top(prep) si;  # condition: no slash in path = true ? -> copy/move dir into new dir
      if fn = os.move and os.exists(b) then os.remove(b) fi;  # file already exists in target path ? -> remove it
   fi;
   # copy or move file
   r := fn(a, b);
   if verbose and r <> fail then print(a) fi;
   return r
end;

# Copies one or more files, accepts both source and target directories
skycrane.fcopy := proc(a :: string, b :: string, verbose) is  # Agena 1.8.10
   return skycrane.aux.fileop(os.fcopy, a, b, verbose)
end;

# Moves one or more files, accepts both source and target directories
skycrane.move := proc(a :: string, b :: string, verbose) is  # Agena 1.8.10
   return skycrane.aux.fileop(os.move, a, b, verbose)
end;


skycrane.getlocales := proc() is  # Agena 1.7.7, 26.08.2012
   local curlocale, locale, lstr, entry;
   create local table r, sequence t;
   curlocale := os.setlocale();
   r['C'] := os.setlocale('C');
   for i from abs('A') to abs('Z') do
      for j from abs('A') to abs('Z') do
         locale := char(i) & char(j);
         insert locale into t;
         entry := os.setlocale(locale)
         if entry then
            r[locale] := entry
         fi
      od
   od;
   for i in << x -> lower(x) >> -> t do
      for j in t do
         lstr := i & '_' & j;
         entry := os.setlocale(lstr)
         if entry then
            r[lstr] := entry
         fi
      od
   od;
   os.setlocale(curlocale);
   return r
end;


# creates a table of bags with its keys determined by the values in the sequence indices. 1.7.8 u2, 30.08.2012
skycrane.bagtable := proc(indices :: sequence) is
   create local table r(size indices);
   if size indices = 0 then
      error('Error in `skycrane.bagtable`: expected a sequence with at least one entry.')
   fi;
   if unassigned(bags) or bags :- table then
      import bags
   fi;
   for i in indices do
      r[i] := bags.bag()
   od;
   return r
end;


skycrane.readcsv := proc(fn, ?) is  # Agena 1.8.0 u1
   if fn :- string then
      argerror(fn, 'skycrane.readcsv', 'expected a file name')
   fi;
   for i in varargs do
      if not(i :: pair or i :: procedure) then
         argerror(i, 'skycrane.readcsv', 'option must be a pair or procedure')
      fi
   od;
   return utils.readcsv(fn,
      convert=false, ignorespaces=false, remove='quotes', remove='doublequotes', unpack(varargs))
end;


skycrane.aux.scribe := proc(fd, x) is
   if fd then io.write(fd, x) else io.write(x) fi
end;

skycrane.scribe := proc(?) is  # Agena 1.8.5 u1, improved 2.3.0 RC 3; rewritten 2.3.3
   local fd, i, sizes, start, stop, delim, nl, option, name, value;
   if io.isfdesc(varargs[1]) then  # 2.0.0 RC 5
      fd := varargs[1];
      start := 2
   else
      start := 1
   fi;
   stop := nargs;
   delim := ';';  # default delimitor
   nl := true;    # add a newline at end of line
   for option, value in checkoptions('skycrane.scribe', varargs[start to nargs], 
                                     delim=string, newline=boolean, nl=boolean) do
      case option
         of 'delim' then
            delim := value;
            dec stop
         of 'newline', 'nl' then
            nl := value;
            dec stop
         else
            error('Error in `skycrane.scribe`: unknown option ' & option & '.')
      esac
   od;
   for x from start to stop do
      i := varargs[x];
      if isstructure(i) then
         sizes := if i :: register then registers.gettop(i) else size i fi;
         for j to sizes do
            skycrane.aux.scribe(fd, tostring(i[j]))
            if j < sizes then skycrane.aux.scribe(fd, delim) fi
         od
      else
         skycrane.aux.scribe(fd, i);
      fi;
      if x < stop then skycrane.aux.scribe(fd, delim) fi
   od;
   if nl then skycrane.aux.scribe(fd, '\n') fi
end;


skycrane.sorted := proc(s :: {table, sequence, register}, f) is  # Agena 1.8.9, 13.11.2012, optimised 2.3.0 RC 3
   local tnumbers, tstrings, tothers;
   if assigned(f) and f :- procedure then
      argerror('sorted', 'second argument must be a procedure')
   fi;
   tnumbers, tothers := selectremove(<< x -> x :: number >>, s, newarray=true);
   sort(tnumbers, f);  # applying `filled` is slower
   tstrings, tothers := selectremove(<< x -> x :: string >>, tothers, newarray=true);
   sort(tstrings, f);  # applying `filled` is slower
   return tnumbers union tstrings union tothers
end;


skycrane.iterate := proc(s) is  # Agena 1.8.11 u1, 11.12.2012; very primitive and very slow
   local key, sizes, indices := 0;
   case type(s)
      of table then
         indices := sorted(tables.indices(s))
      of set then
         indices := sorted(totable(s))
      of sequence then
         sizes := size s
      of register then  # 2.3.0 RC 3
         sizes := registers.gettop(s)
      else
         argerror(s, 'skycrane.iterate', 'expected a table, set, register, or sequence')
   esac;
   return proc() is
      inc key;
      case type(s)
         of table then
            return s[indices[key]]
         of set then
            return indices[key]
         else
            if key > sizes then
               return null
            else
               return s[key]
            fi
      esac
   end
end;


# returns random integers in the range [1 .. 6].
skycrane.dice := << () -> math.random (6) >>;


skycrane.stopwatch := proc() is  # 24.07.2012 or earlier
   local t, flag;
   flag := true;
   while true do
      if flag then
         write('Press any key to start the stop watch ...\n');
         io.getkey();
      fi;
      writeline('Press any key to stop the stop watch ...\n')
      t := time();
      write('Elapsed time: ' & strings.repeat(' ', 11) & 'seconds' & strings.repeat('\b', 18));
      while io.anykey() = false do
         write(strings.format('%10.2f', time(t) - t));
         write(strings.repeat('\b', 10))
      od;
      writeline('\n');
      writeline('Press `q` to quit, any other key to start again.\n');
      if char(io.getkey()) = 'q' then break else flag := false; fi
   od
end;


skycrane.tee := proc(fd, ?) is  # patched 2.2.0 RC 2
   local delim, nops, tobewritten, options;
   if not io.isopen(fd) then
      error('Error in `skycrane.tee`: file is not open or not existing.')
   fi;
   delim := '\t';
   nops := nargs - 1;
   options := checkoptions('skycrane.tee', [varargs[nops]], delim = string, format = string);
   if options.delim then dec nops fi;    # 1.12.4 u2
   if options.format then  # 1.12.4 u2
      dec nops;  # 2.2.0 RC 2
      tobewritten := strings.format(options.format, unpack(varargs[1 to nops]));
      io.write(tobewritten);
      io.write(fd, tobewritten)
   else
      delim := alternate(delim, options.delim);
      tobewritten := join(tostring -> varargs[1 to nops], delim);
      io.writeline(tobewritten);
      io.writeline(fd, tobewritten)
   fi;
end;


#/

Returns the calendar date and time represented by the number x, which should hold the number of seconds
(and optionally milliseconds) elapsed since the start of the given epoch. The return is a string of the
format `YYYY/MM/DD hh:mm:ss`.

If no argument is given, the current system date and time is returned. You may pass an optional format
string if you prefer another representation of the date and time. See also: strings.format.

See also: os.now, os.time.

/#

skycrane.todate := proc(s, format) is  # 2.0.0 RC 5
   if assigned s then
      if s :: string then
         format := s
      elif s :- number then
         argerror(s, 'skycrane.todate', 'expected a number as first argument')
      fi
   fi;
   if unassigned(s) or s :: string then  # get current time
      local a, b := os.time();
      s := a + b/1k
   fi
   if unassigned format then
      format := '%4d/%02d/%02d %02d:%02d:%02d'
   fi;
   return strings.format(format, unpack(os.secstodate(s)))
end;

# Backward compatibility to 12.1 and earlier
skycrane.semaphore := skycrane.count;

