; docformat = 'rst'
;+
; Logger object to control logging.
; 
; :Properties:
;    name : type=string
;       name of the logger
;    parent : private
;       parent logger
;    level : type=long
;       current level of logging: 0 (none), 1 (critial), 2 (error), 
;       3 (warning), 4 (info), or 5 (debug); can be set to an array of levels 
;       which will be cascaded up to the parents of the logger with the 
;       logger taking the last level and passing the previous ones up to its 
;       parent; only messages with levels lower or equal to than the logger 
;       level will be logged
;    time_format : type=string
;       Fortran style format code to specify the format of the time in the 
;       `FORMAT` property; the default value formats the time/date like 
;       "2003-07-08 16:49:45.891"
;    format : type=string
;       format string for messages, default value for format is:: 
;
;         '%(time)s %(levelname)s: %(routine)s: %(message)s'
;
;       where the possible names to include are: "time", "levelname", 
;       "routine", "stacktrace", "name", and "message".
;
;       Note that the time argument will first be formatted using the 
;       `TIME_FORMAT` specification
;    filename : type=string
;       filename to send append output to; set to empty string to send output 
;       to `stderr`
;    clobber : type=boolean
;       set, along with filename, to clobber pre-existing file
;    output : type=strarr
;        output sent to the logger already
;    _extra : type=keywords
;       any keyword accepted by `MGffLogger::setProperty`
;-
;+
; Get the minimum level value of this logger and all its parents.
;
; :Private:
;
; :Returns:
;    long
;-
function mgfflogger::_getLevel
  compile_opt strictarr
  return, obj_valid(self.parent) $
            ? (self.parent->_getLevel() < self.level) $
            : self.level
end
;+
; Finds the name of an object, even if it does not have a `NAME` property. 
; Returns the empty string if the object does not have a `NAME` property.
;
; :Private:
;
; :Returns:
;    string
; 
; :Params:
;    obj : in, required, type=object
;       object to find name of
;-
function mgfflogger::_askName, obj
  compile_opt strictarr
  catch, error
  if (error ne 0L) then begin
    catch, /cancel
    return, ''
  endif
  
  obj->getProperty, name=name
  return, name
end
;+
; Returns an immediate child of a container by name.
;
; :Private:
;
; :Returns:
;    object
;
; :Params:
;    name : in, required, type=string
;       name of immediate child
;    container : in, required, type=object
;       container to search children of
;-
function mgfflogger::_getChildByName, name, container
  compile_opt strictarr
  
  for i = 0L, container.children->count() - 1L do begin
    child = container.children->get(position=i)
    childName = self->_askName(child)
    if (childName eq name) then return, child
  endfor
  
  return, obj_new()
end
;+
; Traverses a hierarchy of named objects using a path of names delimited with
; /'s.
; 
; :Returns:
;    object
;
; :Params:
;    name : in, required, type=string
;       path of names to the desired object; names are delimited with /'s
;-
function mgfflogger::getByName, name
  compile_opt strictarr
  
  tokens = strsplit(name, '/', /extract, count=ntokens)
  child = self
  for depth = 0L, ntokens - 1L do begin
    newChild = self->_getChildByName(tokens[depth], child)
    if (~obj_valid(newChild)) then begin
      newChild = obj_new('MGffLogger', name=tokens[depth], parent=child)
      child.children->add, newChild
    endif
    child = newChild
  endfor
  
  return, child
end
;+
; Set properties.
;-
pro mgfflogger::getProperty, level=level, $
                             format=format, time_format=time_format, $
                             name=name, $
                             filename=filename, $
                             output=output
  compile_opt strictarr
  
  if (arg_present(level)) then level = self.level
  if (arg_present(format)) then format = self.format
  if (arg_present(time_format)) then time_format = self.time_format
  if (arg_present(name)) then name = self.name
  if (arg_present(filename)) then filename = self.filename  
  if (arg_present(output)) then begin
    if (self.filename ne '') then begin
      output = strarr(file_lines(self.filename))
      openr, lun, self.filename, /get_lun
      readf, lun, output
    endif
  endif
end
;+
; Get properties.
;-
pro mgfflogger::setProperty, level=level, $
                             format=format, time_format=time_format, $
                             filename=filename, clobber=clobber
  compile_opt strictarr
  
  case n_elements(level) of
    0: 
    1: self.level = level
    else: begin
        self.level = level[n_elements(level) - 1L]
        if (obj_valid(self.parent)) then begin
          self.parent->setProperty, level=level[0:n_elements(level) - 2L]
        endif
      end
  endcase
  
  if (n_elements(format) gt 0L) then self.format = format
  if (n_elements(time_format) gt 0L) then self.time_format = time_format
  if (n_elements(filename) gt 0L) then self.filename = filename
  if (keyword_set(clobber) && n_elements(filename) gt 0L) then begin
    if (file_test(filename)) then file_delete, filename
  endif
end
;+
; Insert the stack trace for the last error message into the log. Since stack
; traces are from run-time crashes they are considered to be at the CRITICAL 
; level.
;
; :Keywords:
;    back_levels : in, optional, private, type=boolean
;       number of levels to go back in the stack trace beyond the normal ones;
;       should be set to 1 if calling this routine from `MG_LOG` for example
;-
pro mgfflogger::insertLastError, back_levels=back_levels
  compile_opt strictarr
  _back_levels = n_elements(back_levels) eq 0L ? 0 : back_levels
  
  help, /last_message, output=helpOutput
  if (n_elements(helpOutput) eq 1L && helpOutput[0] eq '') then return
  
  self->print, 'Stack trace for error', level=1, back_levels=_back_levels + 1L
  if (self.filename eq '') then begin
    lun = -2L
  endif else begin
    if (file_test(self.filename)) then begin
      openu, lun, self.filename, /get_lun, /append 
    endif else begin
      openw, lun, self.filename, /get_lun
    endelse
  endelse
  
  printf, lun, transpose(helpOutput)
  
  if (lun ge 0L) then free_lun, lun
end
;+
; Log message to given level.
; 
; :Params:
;    msg : in, required, type=string
;       message to print
;
; :Keywords:
;    level : in, optional, type=long
;       level of message
;    back_levels : in, optional, private, type=boolean
;       number of levels to go back in the stack trace beyond the normal ones;
;       should be set to 1 if calling this routine from `MG_LOG` for example
;-
pro mgfflogger::print, msg, level=level, back_levels=back_levels
  compile_opt strictarr
  _back_levels = n_elements(back_levels) eq 0L ? 0 : back_levels
  if (self.filename eq '') then begin
    lun = -2L
  endif else begin
    if (file_test(self.filename)) then begin
      openu, lun, self.filename, /get_lun, /append 
    endif else begin
      openw, lun, self.filename, /get_lun
    endelse
  endelse
  if (level le self->_getLevel()) then begin
    stack = scope_traceback(/structure, /system)
    vars = { time: string(systime(/julian), format='(' + self.time_format + ')'), $
             levelname: strupcase(self.levelNames[level - 1L]), $
             routine: stack[n_elements(stack) - 2L - _back_levels].routine, $
             stacktrace: strjoin(stack[0:n_elements(stack) - 2L - _back_levels].routine, '->'), $
             name: self.name, $
             message: msg $
           }
    s = mg_subs(self.format, vars)
    printf, lun, s
  endif
  
  if (lun ge 0L) then free_lun, lun
end
;+
; Free resources.
;-
pro mgfflogger::cleanup
  compile_opt strictarr
  
  if (obj_valid(self.parent)) then begin
    (self.parent).children->remove, self
  endif
  
  obj_destroy, self.children
end
;+
; Create logger object.
;
; :Returns:
;    1 for success, 0 for failure
;-
function mgfflogger::init, parent=parent, name=name, _extra=e
  compile_opt strictarr
  self.parent = n_elements(parent) eq 0L ? obj_new() : parent
  self.name = n_elements(name) eq 0L ? '' : name
  self.children = obj_new('IDL_Container')
  
  self.time_format = 'C(CYI4.4, "-", CMOI2.2, "-", CDI2.2, " ", CHI2.2, ":", CMI2.2, ":", CSF06.3)'
  self.format = '%(time)s %(levelname)s: %(routine)s: %(message)s'
  
  self.level = 0L
  self.levelNames = ['Critical', 'Error', 'Warning',  'Informational', 'Debug']
  
  self->setProperty, _extra=e
  
  return, 1
end
;+
; Define instance variables.
; 
; :Fields:
;    parent
;       parent `MGffLoffer` object
;    name
;       name of the loffer
;    children
;       `IDL_Container` of children loggers
;    level
;       current level of logging: 0=none, 1=critical, 2=error, 3=warning, 
;       4=informational, or 5=debug; only messages with a level lower or equal
;       to this this value will be logged
;    levelNames
;       names for the different levels
;    filename
;       filename to send output to
;    time_format
;       Fortran format codes for calendar output
;    format
;       format code to send output to
;-
pro mgfflogger__define
  compile_opt strictarr
  
  define = { MGffLogger, $
             parent: obj_new(), $
             name: '', $
             children: obj_new(), $
             level: 0L, $
             levelNames: strarr(5), $
             filename: '', $
             time_format: '', $
             format: '' $
           }
end