; docformat = 'rst'

;+
;
; Main interface to work with the NASA Ames PAH IR Spectroscopic
; Database's XML file.
;
; Updated versions of the NASA Ames PAH IR Spectroscopic Database and
; more information can be found at: `www.astrochemistry.org/pahdb <https://www.astrochemistry.org/pahdb>`.
;
; :Examples:
;   Obtaining and plotting the experimental spectrum of the PAH species
;   with UID = 100::
;
;     IDL> dbi = OBJ_NEW('AmesPAHdbIDLSuite', Filename='experimental.xml')
;     IDL> pahs = db->GetTransitionsByUID(100)
;     IDL> pahs->Plot
;     IDL> OBJ_DESTROY,[pahs, dbi]
;
; :Properties:
;   Filename: in, optional, type=string
;     Database XML-file
;   Check: in, optional, type=int
;     Check the consistency of the XML-file (defaults to 1)
;   Cache: in, optional, type=int
;     Use the cache (defaults to 1)
;   UpdateCheck: in, optional, type=int
;     Check for updates to the Suite (defaults to 1)
;
; :Uses:
;    AmesPAHdbIDLSuite_XMLParser,AmesPAHdbIDLSuite_Species,AmesPAHdbIDLSuite_Transitions,AmesPAHdbIDLSuite_Laboratory_Spectrum,AmesPAHdbIDLSuite_Geometry
;
; :Author:
;   Dr. Christiaan Boersma
;
; :Copyright:
;   BSD licensed
;
; :History:
;   Changes::
;
;     07-03-2024
;     Add search logic for n_ch and n_ch3 fields added in v4.00. Christiaan
;     Boersma.
;     05-09-2024
;     Add Windows support for MD5 hashing. Christiaan Boersma.
;     10-10-2023
;     Generalize caching by adding CACHE_DIR and CACHE methods. Christiaan
;     Boersma.
;     05-15-2023
;     Remove invalid search key 'atoms' in TOKENIZEWORDS. Christiaan Boersma.
;     04-11-2023
;     Initialize array with appropriate size in GETTAGBYUID. Christiaan
;     Boersma.
;     12-05-2022
;     Speed up GETTAGBYUID by using HISTOGRAM. Christiaan
;     Boersma.
;     10-05-2020
;     Fix typo in ExtendDatabase. Christiaan Boersma.
;     11-15-2019
;     Check the state of parsing an XML-file in PARSEFILE, which
;     avoids caching invalid data. Christiaan Boersma.
;     02-12-2019
;     Fixed hitting the maximum value that can be held in a LONG in
;     GETTAGBYUID by changing data type to ULONG. Ensuring returned
;     UIDs reflect those found in GETTAGBYUID. Christiaan Boersma.
;     07-17-2018
;     Added EXTENDDATABASE
;     10-10-2017
;     Fixed GETUIDSCOMPLETECHARGESET to set list to !NULL when last
;     last anion/neutral has been matched. Christiaan Boersma.
;     10-06-2017
;     Added GETHASH to allow retrieval of the calculated md5-sum.
;     Christiaan Boersma.
;     08-11-2016
;     If md5 cannot be calculated, the basename of the database XML-file
;     is used instead. Christiaan Boersma.
;     07-06-2016
;     Using now TEMPORARY to move pahdb and joined to properties of
;     AmesPAHdbIDLSuite in READFILE and SEARCH, respectively. Christiaan
;     Boersma.
;     07-02-2016
;     Added file_md5 as a property, now calling method FILE_MD5 only
;     once in method READFILE, which sets file_md5. Affected routines
;     have been updated. File_md5 is added as a tag to the pahdb
;     structure. SEARCH and READFILE now print a message when saving to
;     cache and both routines have been refactored to store the location
;     of the cache file into a variable. READFILE now only prints the
;     database header when restoring from cache. Christiaan Boersma.
;     06-29-2016
;     Remove scale-property from species. Removed extra comma from
;     GETVERSION. Christiaan Boersma.
;     03-15-2016
;     Provide argument to ROUTINE_FILEPATH in INIT or compatibility with
;     older version of IDL. Christiaan Boersma.
;     09-09-2015
;     Added File_WGET procedure and updated INIT to allow for update
;     check (enabled with the UpdateCheck-keyword). Christiaan Boersma.
;     08-26-2015
;     Added File_MD5 procedure and updated READFILE and SEARCH
;     procedures. Updated Init to read VERSION-file from current
;     distribution. Christiaan Boersma.
;     04-21-2015
;     Moved stop condition to beginning of SEARCH-method. Removed
;     duplicate charge tokens from TOKENIZEWORDS. Christiaan Boersma.
;     04-07-2015
;     Ensured UIDs in GetXXXXByUID are arrays, even when when called
;     with scalar. Christiaan Boersma.
;     02-01-2015
;     First version of the file. Christiaan Boersma.
;-

;+
; Retrieves the contents of the file pointed to by Url.
;
; :Params:
;   Url: in, required, type=string
;     URL of file
;   File: out, optional, type=string
;     Contents of the file pointed to by Url
;
; :Categories:
;   INTERNET
;
; :Private:
;-
PRO AmesPAHdbIDLSuite::File_WGET,Url,File

  COMPILE_OPT IDL2

  ON_ERROR,2

  urlC = PARSE_URL(Url)

  netUrl = OBJ_NEW('IDLnetUrl')

  netUrl->SetProperty,URL_SCHEME=urlC.scheme

  netUrl->SetProperty,URL_HOSTNAME=urlC.host

  netUrl->SetProperty,URL_PORT=urlC.port

  netUrl->SetProperty,URL_PATH=urlC.path

  netUrl->SetProperty,URL_QUERY=urlC.query

  netUrl->SetProperty,HEADER='User-Agent: ' +  "IDL/" + !VERSION.RELEASE + ' (' + !VERSION.OS + '; ' + !VERSION.ARCH + ' like ' + !VERSION.OS_FAMILY + ')'

  File = STRING(netUrl->Get(/BUFFER))

  netUrl->CloseConnections

  OBJ_DESTROY,netUrl
END

;+
; Calculates the MD5-hash of File.
;
; :Params:
;   File: in, required, type=string
;     File to calculate MD5 of
;   Hash: out, optional, type=string
;     MD5-hash of File
;   Err: out, optional, type=string
;     Error string
;
; :Categories:
;   HASHING
;
; :Private:
;-
PRO AmesPAHdbIDLSuite::File_MD5,File,Hash,Err

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF !VERSION.OS_FAMILY EQ "Windows" THEN BEGIN

     SPAWN,['certUtil', '-hashfile', File, 'MD5'],Hash,Err,/NOSHELL

     Hash = (STRSPLIT(Hash, ':', /EXTRACT))[1]

     RETURN
  ENDIF

  md5 = FILE_WHICH(GETENV('PATH'), 'md5')

  IF MD5 NE '' THEN BEGIN

     SPAWN,[md5,'-q', File],Hash,Err,/NOSHELL

     RETURN
  ENDIF

  md5sum = FILE_WHICH(GETENV('PATH'), 'md5sum')

  IF md5sum NE '' THEN BEGIN

     SPAWN,[md5sum, File],Hash,Err,/NOSHELL

     Hash = (STRSPLIT(Hash, /EXTRACT))[0]

     RETURN
  ENDIF

  Err = "       UNABLE TO CALCULATE MD5: COMMAND NOT FOUND        "

  PRINT
  PRINT,"========================================================="
  PRINT,Err
  PRINT,"========================================================="
  PRINT
END

;+
; Returns the MD5-hash of the parsed database file.
;
; :Returns:
;   string
;
; :Categories:
;   HASHING
;-
FUNCTION AmesPAHdbIDLSuite::GetHash

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,self.file_md5
END

;+
; Return the PAHdb cache dir.
;
; :Returns:
;   string
;
; :Categories:
;   CACHE
;
; :Private:
;-
FUNCTION AmesPAHdbIDLSuite::Cache_DIR

    COMPILE_OPT IDL2, STATIC

    ON_ERROR,2

    DEFSYSV,'!AMESPAHCACHDIR',EXISTS=defsys

    IF defsys EQ 1 THEN dir_cache = !AMESPAHCACHEDIR $
    ELSE dir_cache = GETENV("AMESPAHCACHEDIR")

    IF dir_cache EQ "" THEN RETURN,GETENV('IDL_TMPDIR')

    IF NOT FILE_TEST(dir_cache, /DIRECTORY) THEN FILE_MKDIR,dir_cache

    RETURN,dir_cache + PATH_SEP()
END

;+
; Manage the PAHdb cache.
;
; :Params:
;   Hash: in, optional, type=string
;     Hash to clear
;
; :Keywords:
;   Clear: in, optional, type=string
;     Clear all or selected hash(es)
;
; :Categories:
;   CACHE
;-
PRO AmesPAHdbIDLSuite::Cache,Hash,Clear=Clear

  COMPILE_OPT IDL2, STATIC

  ON_ERROR,2

  PRINT
  PRINT,"========================================================="

  IF GETENV("AMESPAHCACHEDIR") EQ "" THEN BEGIN
    PRINT,"                   CACHING IS TEMPORARY                  "
    PRINT,"========================================================="
    PRINT
    RETURN
  ENDIF

  dir_cache = AmesPAHdbIDLSuite->Cache_DIR()

  PRINT,FORMAT='("CACHE DIR:",X,A0)',dir_cache
  PRINT,"---------------------------------------------------------"
  PRINT,FORMAT='(A-32,X,A-7,8X,A0)',"HASH","SIZE","CLEARED"
  PRINT,"---------------------------------------------------------"

  files = FILE_SEARCH(dir_cache + PATH_SEP() + "*.sav", COUNT=nfiles)
  FOR i = 0L, nfiles - 1L DO BEGIN
    info = FILE_INFO(files[i])
    basename = STRUPCASE(FILE_BASENAME(files[i],".sav"))
    cleared = 0
    IF KEYWORD_SET(Clear) THEN BEGIN
        IF N_PARAMS() EQ 0 OR (N_PARAMS() EQ 1 AND $
           (basename EQ Hash OR $
            basename EQ Hash + "_JOINED")) THEN BEGIN
            FILE_DELETE,files[i]
            cleared = 1
        ENDIF
    ENDIF
    PRINT,FORMAT='(A-32,X,F7.2,X,"MiB",4X,I1)', basename, $
          info.size / 1024.0 / 1024.0,cleared
  ENDFOR

  PRINT,"========================================================="
  PRINT
END

;+
; Returns the UIDs of complementary charge sets.
;
; :Returns:
;   long array (2D)
;
; :Params:
;   UIDs: in, required, type="long array (1D)"
;     UIDs to consider; -1 for all
;   Count: out, optional, type=long
;     Number of charge sets found
;
; :Keywords:
;   Two: in, optional, type=int
;      Consider only two charge states
;
; :Categories:
;   SEARCH
;
; :Bugs:
;  There are issues when isomers are present.
;-
FUNCTION AmesPAHdbIDLSuite::GetUIDsCompleteChargeSet,UIDs,Count,Two=Two

  COMPILE_OPT IDL2

  ON_ERROR,2

  pahs = self->GetTagByUID('species', UIDs)

  tags = STRLOWCASE(TAG_NAMES(pahs))

  tags = tags[WHERE(tags NE 'uid' AND tags NE 'charge' AND tags NE 'energy' AND tags NE 'formula' AND tags NE 'symm' AND tags NE 'method' AND tags NE 'zeropoint' AND tags NE 'formula')]

  anions = WHERE(pahs.charge EQ -1, nanions)

  cations = WHERE(pahs.charge EQ 1, ncations)

  neutrals = WHERE(pahs.charge EQ 0)

  IF NOT KEYWORD_SET(Two) THEN BEGIN

     set = [0L, 0L, 0L]

     FOR i = 0, nanions - 1 DO BEGIN

        cmd = "sel1 = WHERE(" + STRJOIN("pahs[cations]." + tags + " EQ pahs[anions[i]]." + tags, " AND ") + ", ncation)"

        ret = EXECUTE(cmd)

        IF ncation EQ 0 THEN CONTINUE

        geometries = self->getGeometryByUID([pahs[anions[i]].uid, pahs[cations[sel1]].uid])

        ;rings = geometries->rings()

        ;nrings = N_TAGS(rings[0])

        ;sel2 = WHERE(rings.uid EQ pahs[anions[i]].uid)

        ;same = BYTARR(ncation) + 1B

        ;FOR j = 0, ncation - 1 DO BEGIN

           ;sel3 = WHERE(rings.uid EQ pahs[cations[sel1[j]]].uid)

           ;FOR k = 1L, nrings - 1 DO BEGIN
           ;   IF rings[sel2].(k) NE rings[sel3].(k) THEN BEGIN

           ;      same[j] = 0B

           ;      CONTINUE
           ;   ENDIF
           ;ENDFOR
        ;ENDFOR

        ;keep = WHERE(same, ncation)

        ;sel1 = sel1[keep]

        ;IF ncation GT 1 THEN BEGIN

           geometries->Diagonalize,Full=0

           geo = geometries->get()

           norms = DBLARR(ncation)

           sel2 = WHERE(geo.data.uid EQ pahs[anions[i]].uid)

           x = geo.data[sel2].x & y = geo.data[sel2].y & z = geo.data[sel2].z

           FOR j = 0, ncation - 1 DO BEGIN

              sel2 = WHERE(geo.data.uid EQ pahs[cations[sel1[j]]].uid)

              norms[j] = NORM((x - geo.data[sel2].x)^2 + (y - geo.data[sel2].y)^2 + (z - geo.data[sel2].z)^2)
           ENDFOR

           min = MIN(norms, imin)

           IF min LT 1 THEN BEGIN

              sel1 = sel1[imin]

              ncation = 1L
           ENDIF ELSE ncation = 0L
        ;ENDIF

        OBJ_DESTROY,geometries

        IF ncation EQ 1 THEN BEGIN

           ret = EXECUTE("sel2 = WHERE(" + STRJOIN("pahs[neutrals]." + tags + " EQ pahs[anions[i]]." + tags, " AND ") + ", nneutral)")

           IF nneutral EQ 0 THEN CONTINUE

           geometries = self->getGeometryByUID([pahs[anions[i]].uid, pahs[neutrals[sel2]].uid])

           ;rings = geometries->rings()

           ;nrings = N_TAGS(rings[0])

           ;sel3 = WHERE(rings.uid EQ pahs[anions[i]].uid)

           ;same = BYTARR(nneutral) + 1B

           ;FOR j = 0, nneutral - 1 DO BEGIN

           ;   sel4 = WHERE(rings.uid EQ pahs[neutrals[sel2[j]]].uid)

           ;   FOR k = 1L, nrings - 1 DO BEGIN

           ;      IF rings[sel3].(k) NE rings[sel4].(k) THEN BEGIN

           ;         same[j] = 0B

           ;         CONTINUE
           ;      ENDIF
           ;   ENDFOR
           ;ENDFOR

           ;keep = WHERE(same, nneutral)

           ;sel2 = sel2[keep]

           ;IF nneutral GT 1 THEN BEGIN

              geometries->Diagonalize

              geo = geometries->get()

              norms = DBLARR(nneutral)

              sel3 = WHERE(geo.data.uid EQ pahs[anions[i]].uid)

              x = geo.data[sel3].x & y = geo.data[sel3].y & z = geo.data[sel3].z

              FOR j = 0, nneutral - 1 DO BEGIN

                 sel3 = WHERE(geo.data.uid EQ pahs[neutrals[sel2[j]]].uid)

                 norms[j] = SQRT(TOTAL((x - geo.data[sel3].x)^2 + (y - geo.data[sel3].y)^2 + (z - geo.data[sel3].z)^2))
              ENDFOR

              min = MIN(norms, imin)

              IF min LT 1 THEN BEGIN

                 sel2 = sel2[imin]

                 nneutral = 1
              ENDIF ELSE nneutral = 0
           ;ENDIF

           OBJ_DESTROY,geometries

           IF nneutral EQ 1 THEN BEGIN

              set = [[set], [pahs[anions[i]].uid, pahs[neutrals[sel2]].uid, pahs[cations[sel1]].uid]]

              keep = WHERE(cations NE cations[sel1[0]], nkeep)

              cations = nkeep GT 0 ? cations[keep] : !NULL

              keep = WHERE(neutrals NE neutrals[sel2[0]], nkeep)

              neutrals = nkeep GT 0 ? neutrals[keep] : !NULL
           ENDIF
        ENDIF
     ENDFOR
  ENDIF ELSE BEGIN

     set = [0L, 0L]

     FOR i = 0, ncations - 1 DO BEGIN

        cmd = "sel1 = WHERE(" + STRJOIN("pahs[neutrals]." + tags + " EQ pahs[cations[i]]." + tags, " AND ") + ", nneutral)"

        ret = EXECUTE(cmd)

        IF nneutral EQ 0 THEN CONTINUE

        geometries = self->getGeometryByUID([pahs[cations[i]].uid, pahs[neutrals[sel1]].uid])

        ;rings = geometries->rings()

        ;nrings = N_TAGS(rings[0])

        ;sel2 = WHERE(rings.uid EQ pahs[cations[i]].uid)

        ;same = BYTARR(nneutral) + 1B

        ;FOR j = 0, nneutral - 1 DO BEGIN

        ;   sel3 = WHERE(rings.uid EQ pahs[neutrals[sel1[j]]].uid)

        ;   FOR k = 1L, nrings - 1 DO BEGIN

        ;      IF rings[sel2].(k) NE rings[sel3].(k) THEN BEGIN

        ;         same[j] = 0B

        ;         CONTINUE
        ;      ENDIF
        ;   ENDFOR
        ;ENDFOR

        ;keep = WHERE(same, nneutral)

        ;sel1 = sel1[keep]

        ;IF nneutral GT 1 THEN BEGIN

           geometries->Diagonalize,Full=0

           geo = geometries->get()

           norms = DBLARR(nneutral)

           sel2 = WHERE(geo.data.uid EQ pahs[cations[i]].uid)

           x = geo.data[sel2].x & y = geo.data[sel2].y & z = geo.data[sel2].z

           FOR j = 0, nneutral - 1 DO BEGIN

              sel2 = WHERE(geo.data.uid EQ pahs[neutrals[sel1[j]]].uid)

              norms[j] = SQRT(TOTAL((x - geo.data[sel2].x)^2 + (y - geo.data[sel2].y)^2 + (z - geo.data[sel2].z)^2))
           ENDFOR

           min = MIN(norms, imin)
print,[pahs[neutrals[sel1[imin]]].uid, pahs[cations[i]].uid],min
           IF min LT 1 THEN BEGIN

              sel1 = sel1[imin]

              nneutral = 1
           ENDIF ELSE nneutral = 0
        ;ENDIF

        OBJ_DESTROY,geometries

        IF nneutral EQ 1 THEN BEGIN

           set = [[set], [pahs[neutrals[sel1]].uid, pahs[cations[i]].uid]]

           keep = WHERE(neutrals NE neutrals[sel1[0]], nkeep)

           neutrals = nkeep GT 0 ? neutrals[keep] : !NULL
        ENDIF
     ENDFOR
  ENDELSE

  set = set[*, 1:*]

  Count = N_ELEMENTS(set[0, *])

  RETURN,set
END

;+
; Returns AmesPAHdbIDLSuite_Species-instance containing the data on
; the species with UIDs.
;
; :Returns:
;   AmesPAHdbIDLSuite_Species-instance
;
; :Params:
;   UIDs: in, required, type="long array (1D)"
;     UIDs to consider; -1 for all
;   Count: out, optional, type=long
;     Number of species found
;
; :Categories:
;   RETRIEVAL
;
; :Bugs:
;   Not all species have references/comments and UIDs is updated to
;   only those that do
;
;-
FUNCTION AmesPAHdbIDLSuite::GetSpeciesByUID,UIDs,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,OBJ_NEW('AmesPAHdbIDLSuite_Species', $
                 Type=(*self.pahdb).type ,$
                 Version=(*self.pahdb).version, $
                 Data=self->GetTagByUID('species', UIDs, Count), $
                 PAHdb=self.pahdb, $
                 Uids=[UIDs]);;, $
                 ;;References=self->GetTagByUID('references', UIDs, Count), $
                 ;;Comments=self->GetTagByUID('comments', UIDs, Count))
END

;+
; Returns AmesPAHdbIDLSuite_Transitions-instance containing the data
; on the species with UIDs.
;
; :Returns:
;   AmesPAHdbIDLSuite_Transitions-instance
;
; :Params:
;   UIDs: in, required, type="long array (1D)"
;     UIDs to consider; -1 for all
;   Count: out, optional, type=long
;     Number of species found
;
; :Categories:
;   RETRIEVAL
;-
FUNCTION AmesPAHdbIDLSuite::GetTransitionsByUID,UIDs,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,OBJ_NEW('AmesPAHdbIDLSuite_Transitions', $
                 Type=(*self.pahdb).type ,$
                 Version=(*self.pahdb).version, $
                 Data=self->GetTagByUID('transitions', UIDs, Count), $
                 PAHdb=self.pahdb, $
                 Uids=[UIDs], $
                 Model={type:'AMESPAHDBIDLSUITE_MODEL_ZEROKELVIN_S',$
                        Temperature:0D, $
                        description:STRING(FORMAT='(A-12,":",X,A-0)', "model", "ZeroKelvin")}, $
                 Units={AmesPAHdb_Data_Units_S, $
                 abscissa:{AmesPAHdb_Unit_S, $
                           unit:1, $
                           str:'frequency [cm!U-1!N]'}, $
                 ordinate:{AmesPAHdb_Unit_S, $
                           unit:2, $
                           str:'integrated cross-section [km/mol]'}})
END

;+
; Returns AmesPAHdbIDLSuite_Laboratory_Spectrum-instance containing
; the data on the species with UIDs.
;
; :Returns:
;   AmesPAHdbIDLSuite_Laboratory_Spectrum-instance
;
; :Params:
;   UIDs: in, required, type="long array (1D)"
;     UIDs to consider; -1 for all
;   Count: out, optional, type=long
;     Number of species found
;
; :Categories:
;   RETRIEVAL
;-
FUNCTION AmesPAHdbIDLSuite::GetLaboratoryByUID,UIDs,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF (*self.pahdb).type NE 'experimental' THEN BEGIN
     PRINT
     PRINT,"========================================================="
     PRINT,"            EXPERIMENTAL DATABASE REQUIRED               "
     PRINT,"========================================================="
     PRINT
     RETURN, OBJ_NEW()
  ENDIF

  RETURN,OBJ_NEW('AmesPAHdbIDLSuite_Laboratory_Spectrum', $
                 Type=(*self.pahdb).type ,$
                 Version=(*self.pahdb).version, $
                 Data=self->GetTagByUID('laboratory', UIDs, Count), $
                 PAHdb=self.pahdb, $
                 Model={type:'AMESPAHDBIDLSUITE_RAW_LABORATORY_S',$
                        Temperature:0D, $
                        description:STRING(FORMAT='(A-12,":",X,A-0)', "model", "Laboratory")}, $
                 Uids=[UIDs], $
                 Units={AmesPAHdb_Data_Units_S, $
                        abscissa:{AmesPAHdb_Unit_S, $
                                  unit:1, $
                                  str:'frequency [cm!U-1!N]'}, $
                        ordinate:{AmesPAHdb_Unit_S, $
                                  unit:1, $
                                  str:'absorbance [-log(I/I!L0!N)]'}})
END

;+
; Returns AmesPAHdbIDLSuite_Geometry-instance containing the data on
; the species with UIDs.
;
; :Returns:
;   AmesPAHdbIDLSuite_Geometry-instance
;
; :Params:
;   UIDs: in, required, type="long array (1D)"
;     UIDs to consider; -1 for all
;   Count: out, optional, type=long
;     Number of species found
;
; :Categories:
;   RETRIEVAL
;-
FUNCTION AmesPAHdbIDLSuite::GetGeometryByUID,UIDs,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,OBJ_NEW('AmesPAHdbIDLSuite_Geometry', $
                 Data=self->GetTagByUID('geometries', UIDs, Count), $
                 Uids=[UIDs])
END

;+
; Returns the data on associated with the given Tag
;
; :Returns:
;  struct (variable)
;
; :Params:
;   Tag: in, required, type=string
;    Name of the Tag
;   UIDs: in, required, type="long array (1D)"
;     UIDs to consider; -1 for all
;   Count: out, optional, type=long
;     Number of species found
;
; :Categories:
;   RETRIEVAL
;
; :Private:
;-
FUNCTION AmesPAHdbIDLSuite::GetTagByUID,Tag,UIDs,Count

  COMPILE_OPT IDL2

  ON_ERROR,2

  Count = 0L

  itag = WHERE(STRLOWCASE(TAG_NAMES((*self.pahdb).data)) EQ Tag, ntag)

  IF ntag NE 1 THEN RETURN,-1

  IF SIZE(UIDs, /DIMENSIONS) EQ 0 AND UIDs EQ -1 THEN BEGIN

     Count = N_ELEMENTS((*self.pahdb).data.(itag))

     UIDs = (*self.pahdb).data.(itag)[UNIQ((*self.pahdb).data.(itag).uid)].uid

     RETURN,(*self.pahdb).data.(itag)
  ENDIF

  UIDs = UIDs[SORT(UIDs)]

  nuids = N_ELEMENTS(UIDs)

  h = HISTOGRAM((*self.pahdb).data.(itag).uid, MIN=0, REVERSE_INDICES=ri)

  FOR i = 0L, nuids - 1L DO BEGIN

    n = h[UIDs[i]]

    IF n GT 0 THEN BEGIN

      IF Count EQ 0 THEN BEGIN

        nbuf = 2L * n

        select = LONARR(nbuf, /NOZERO)
      ENDIF

      s = ri[ri[UIDs[i]]:ri[UIDS[i]+1]-1]

      IF Count + n GT nbuf THEN BEGIN

        WHILE Count + n GE nbuf DO nbuf *= 2L

        new = LONARR(nbuf, /NOZERO)

        new[0:Count-1L] = select[0:Count-1L]

        select = new
      ENDIF

      select[Count:Count+n-1L] = s

      Count += n
    ENDIF
  ENDFOR

  IF Count EQ 0 THEN RETURN,-1

  select = select[0:Count-1L]

  UIDs = (*self.pahdb).data.(itag)[select[UNIQ((*self.pahdb).data.(itag)[select].uid)]].uid

  RETURN,(*self.pahdb).data.(itag)[select]
END

;+
; Returns a pointer to the parsed database.
;
; :Returns:
;   pointer
;
; :Categories:
;   RETRIEVAL
;-
FUNCTION AmesPAHdbIDLSuite::Pointer

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,self.pahdb
END

;+
; Returns the tokinazation of Word
;
; :Returns:
;   array of structures
;
; :Params:
;   Word: in, required, type=string
;      String to tokenize
;
; :Categories:
;   SEARCH
;
; :Private:
;-
FUNCTION AmesPAHdbIDLSuite::TokenizeWords,Word

  COMPILE_OPT IDL2

  ON_ERROR,2

  token = {word:Word, $
           translation:'', $
           type:'None', $
           valid:1}

  word = STRUPCASE(Word)

  charge = [{word:'anion', $
             translation:'(*self.joined).charge LT 0'}, $
            {word:'cation', $
             translation:'(*self.joined).charge GT 0'}, $
            {word:'neutral', $
             translation:'(*self.joined).charge EQ 0'}, $
            {word:'positive', $
             translation:'(*self.joined).charge GT 0'}, $
            {word:'negative', $
             translation:'(*self.joined).charge LT 0'}, $
            {word:'-', $
             translation:'(*self.joined).charge EQ -1'}, $
            {word:'+', $
             translation:'(*self.joined).charge EQ 1'}, $
            {word:'++', $
             translation:'(*self.joined).charge EQ 2'}, $
            {word:'+++', $
             translation:'(*self.joined).charge EQ 3'}, $
            {word:'---', $
             translation:'(*self.joined).charge EQ -3'}]

  icharge = WHERE(STRCMP(charge.word, Word, /FOLD_CASE), ncharge)

  identities = [{word:'uid', $
                 translation:'(*self.joined).uid'}, $
                {word:'identifier', $
                 translation:'(*self.joined).uid'}, $
                {word:'carbon', $
                 translation:'(*self.joined).nc'}, $
                {word:'hydrogen', $
                 translation:'(*self.joined).nh'}, $
                {word:'nitrogen', $
                 translation:'(*self.joined).nn'}, $
                {word:'oxygen', $
                 translation:'(*self.joined).no'}, $
                {word:'magnesium', $
                 translation:'(*self.joined).nmg'}, $
                {word:'silicium', $
                 translation:'(*self.joined).nsi'}, $
                {word:'iron', $
                 translation:'(*self.joined).nfe'}, $
                {word:'c', $
                 translation:'(*self.joined).nc'}, $
                {word:'h', $
                 translation:'(*self.joined).nh'}, $
                {word:'n', $
                 translation:'(*self.joined).nn'}, $
                {word:'o', $
                 translation:'(*self.joined).no'}, $
                {word:'mg', $
                 translation:'(*self.joined).nmg'}, $
                {word:'si', $
                 translation:'(*self.joined).nsi'}, $
                {word:'fe', $
                 translation:'(*self.joined).nfe'}, $
                {word:'wavenumber', $
                 translation:'(*self.joined).frequency'}, $
                {word:'absorbance', $
                 translation:'(*self.joined).intensity'}, $
                {word:'frequency', $
                 translation:'(*self.joined).frequency'}, $
                {word:'scale', $
                 translation:'(*self.joined).scale'}, $
                {word:'intensity', $
                 translation:'(*self.joined).intensity'}, $
                {word:'ch', $
                 translation:'(*self.joined).nch'}, $
                {word:'ch2', $
                 translation:'(*self.joined).nch2'}, $
                {word:'ch3', $
                 translation:'(*self.joined).nch3'}, $
                {word:'chx', $
                 translation:'(*self.joined).nchx'}, $
                {word:'solo', $
                 translation:'(*self.joined).nsolo'}, $
                {word:'duo', $
                 translation:'(*self.joined).nduo'}, $
                {word:'trio', $
                 translation:'(*self.joined).ntrio'}, $
                {word:'quartet', $
                 translation:'(*self.joined).nquartet'}, $
                {word:'quintet', $
                 translation:'(*self.joined).nquintet'}, $
                {word:'charge', $
                 translation:'(*self.joined).charge'}, $
                {word:'symmetry', $
                 translation:'(*self.joined).symmetry'}, $
                {word:'weight', $
                 translation:'(*self.joined).weight'}, $
                {word:'energy', $
                 translation:'(*self.joined).total_e'}, $
                {word:'zeropoint', $
                 translation:'(*self.joined).vib_e'}, $
                {word:'experiment', $
                 translation:'(*self.joined).exp'}]

  iidentity = WHERE(STRCMP(identities.word, Word, /FOLD_CASE), nidentity)

  logical = [{word:'and', $
              translation:'AND'}, $
             {word:'or', $
              translation:'OR'}, $
             {word:'|', $
              translation: 'OR'}, $
             {word:'&', $
              translation: 'AND'}]

  ilogical = WHERE(STRCMP(logical.word, Word, /FOLD_CASE), nlogical)

  comparison = [{word:'<', $
                 translation:'LT'}, $
                {word:'lt', $
                 translation:'LT'}, $
                {word:'>', $
                 translation:'GT'}, $
                {word:'gt', $
                 translation:'GT'}, $
                {word:'=', $
                 translation:'EQ'}, $
                {word:'eq', $
                 translation:'EQ'}, $
                {word:'<=', $
                 translation:'LE'}, $
                {word:'le', $
                 translation:'LE'}, $
                {word:'>=', $
                 translation:'GE'}, $
                {word:'ge', $
                 translation:'GE'}, $
                {word:'with', $
                 translation:'AND'}, $
                {word:'ne', $
                 translation:'ne'}, $
                {word:'!=', $
                 translation:'ne'}]

  icomparison = WHERE(STRCMP(comparison.word, Word, /FOLD_CASE), ncomparison)

  transfer = [{word:'(', $
               translation:'('}, $
              {word:')', $
               translation:')'}]

  itransfer = WHERE(STRCMP(transfer.word, Word, /FOLD_CASE), ntransfer)

  IF STREGEX(Word, '(^[+-]?[0-9]+)|([A-Z][0-9][A-Z])', /FOLD_CASE, /BOOLEAN) THEN BEGIN

     token.type = "NUMERIC"

     token.translation = Word
  ENDIF ELSE IF ncharge THEN BEGIN

     token.type = "CHARGE"

     token.translation = charge[icharge].translation
  ENDIF ELSE IF nidentity THEN BEGIN

     token.type = "IDENTITY"

     token.translation = identities[iidentity].translation
  ENDIF ELSE IF nlogical THEN BEGIN

     token.type = "LOGICAL"

     token.translation = logical[ilogical].translation
  ENDIF ELSE IF ncomparison THEN BEGIN

     token.type = "COMPARISON"

     token.translation = comparison[icomparison].translation
  ENDIF ELSE IF ntransfer THEN BEGIN

     token.type = "TRANSFER"

     token.translation = transfer[itransfer].translation
  ENDIF ELSE IF STREGEX(Word,'(MG+|SI+|FE+|[CHNO]+)([0-9]*)(MG+|SI+|FE+|[CHNO]+)([0-9]*)(MG+|SI+|FE+|[CHNO]*)([0-9]*)', /FOLD_CASE, /BOOLEAN) THEN BEGIN

     token.type = "FORMULA"

     token.translation = Word
  ENDIF ELSE BEGIN

     token.type = "IGNORE"      ; SHOULD BE NAME

     token.translation = Word
  ENDELSE

  RETURN,token
END

;+
; Parses Tokens into a search string
;
; :Returns:
;   string
;
; :Params:
;   Tokens: in, required, type="array of tokens"
;      Tokens to parse
;
; :Categories:
;   SEARCH
;
; :Private:
;-
FUNCTION AmesPAHdbIDLSuite::ParseTokens,Tokens

  COMPILE_OPT IDL2

  ON_ERROR,2

  ntokens = N_ELEMENTS(Tokens)

  prev = -1 & current = 0

  IF ntokens GT 1 THEN next = 1 ELSE next = -1

  parsed = ''
  WHILE current NE -1 DO BEGIN
     CASE Tokens[current].type OF

        'FORMULA': BEGIN
           IF prev NE -1 THEN BEGIN

              IF NOT (Tokens[prev].type NE 'LOGICAL' AND Tokens[prev].valid EQ 1) THEN parsed += ' OR '
           ENDIF

           parsed += ' STRMATCH(joined.formula, "' + Tokens[current].translation + '*", /FOLD_CASE)'
        END
        'IDENTITY': BEGIN

           IF prev NE -1 THEN BEGIN

              IF NOT (Tokens[prev].type EQ 'LOGICAL' OR Tokens[prev].type EQ 'TRANSFER' OR Tokens[prev].type EQ 'TRANSFER' AND Tokens[prev].valid EQ 1) THEN parsed += ' AND '
           ENDIF

           IF next NE -1 THEN BEGIN

              IF Tokens[next].type EQ 'COMPARISON' THEN parsed += ' ' + Tokens[current].translation ELSE parsed += ' ' + Tokens[current].translation + ' GT 0'
           ENDIF
        END

        'NUMERIC': BEGIN

           IF prev NE -1 THEN BEGIN

              IF Tokens[prev].type EQ 'COMPARISON' AND Tokens[prev].valid EQ 1 THEN parsed += ' ' + Tokens[current].translation ELSE Tokens[current].valid = -1
           ENDIF
        END

        'LOGICAL': BEGIN

           IF prev NE -1 THEN BEGIN

              IF (Tokens[prev].type EQ 'IDENTITY' OR Tokens[prev].type EQ 'NUMERIC' OR Tokens[prev].type EQ 'FORMULA' OR Tokens[prev].type EQ 'CHARGE' AND Tokens[prev].valid EQ 1) THEN BEGIN

                 IF next NE -1 THEN BEGIN

                    IF (Tokens[next].type EQ 'TRANSFER') THEN parsed += Tokens[current].translation $
                    ELSE IF (Tokens[next].type EQ 'IDENTITY' OR Tokens[next].type EQ 'NUMERIC' OR Tokens[next].type EQ 'FORMULA' OR Tokens[next].type EQ 'CHARGE') THEN parsed += ' ' + Tokens[current].translation ELSE Tokens[current].valid = -1
                 ENDIF
              ENDIF
           ENDIF
        END

        'COMPARISON': BEGIN

           IF prev NE -1 THEN BEGIN

              IF Tokens[prev].type EQ 'IDENTITY' AND Tokens[prev].valid EQ 1 THEN BEGIN

                 IF next NE -1 THEN BEGIN

                    IF Tokens[next].type EQ 'NUMERIC' THEN parsed +=  ' ' + Tokens[current].translation ELSE Tokens[current].valid = -1
                 ENDIF
              ENDIF
           ENDIF
        END

        'CHARGE': BEGIN

           IF prev NE -1 THEN BEGIN

              IF NOT (Tokens[prev].type EQ 'LOGICAL' AND Tokens[prev].valid EQ 1) THEN parsed += ' AND '
           ENDIF

           parsed += ' ' + Tokens[current].translation
        END

        'TRANSFER': BEGIN

           parsed += Tokens[current].translation
        END

        'NAME': BEGIN

           IF prev NE -1 THEN BEGIN

              IF NOT (Tokens[prev].type EQ 'LOGICAL' AND Tokens[prev].valid EQ 1) THEN parsed += ' AND '
           ENDIF

           ; THIS IS NOT CORRECTLY IMPLEMENTED
           ;parsed += ' STRMATCH(pahdb.data.comments.str, "' + Tokens[current].translation + '*", /FOLD_CASE)'
        END

        'IGNORE': BEGIN

           MESSAGE,Tokens[current].word+' NOT UNDERSTOOD',/INFORMATIONAL

           RETURN, ''
        END
     ENDCASE

     prev = current & current = next

     IF next++ GE ntokens - 1 THEN next = -1
  ENDWHILE

  RETURN,parsed
END

;+
; Returns the UIDs that match the given search Str.
;
; :Returns:
;   long array (1d)
;
; :Params:
;   Str: in, required, type=string
;     Search string
;   Count: out, optional, type=long
;     Number of results
;
; :Keywords:
;   Query: in, optional, type=string
;     The generated query-string that can be used with the WHERE-function
;
; :Categories:
;   SEARCH
;-
FUNCTION AmesPAHdbIDLSuite::Search,Str,Count,Query=Query

  COMPILE_OPT IDL2

  ON_ERROR,2

  Count = 0

  IF STRLEN(Str) EQ 0 THEN RETURN,-1

  IF NOT PTR_VALID(self.joined) THEN BEGIN

     file_cache = self->Cache_DIR()+self.file_md5+'_joined.sav'

     IF FILE_TEST(file_cache, /READ) THEN BEGIN
        PRINT
        PRINT,"========================================================="
        PRINT,"              RESTORING INDEX FROM CACHE                 "
        PRINT,"========================================================="
        PRINT
        RESTORE,FILENAME=file_cache,/RELAXED_STRUCTURE_ASSIGNMENT
     ENDIF ELSE BEGIN

        PRINT
        PRINT,"========================================================="
        PRINT,"       BUILDING INDEX: THIS MAY TAKE A FEW MINUTES       "
        PRINT,"========================================================="
        PRINT

        tags = TAG_NAMES((*self.pahdb).data.species)

        ntags = N_TAGS((*self.pahdb).data.species)

        command = 'template = CREATE_STRUCT((*self.pahdb).data.transitions[0]'

        FOR i = 0,  ntags - 1 DO BEGIN

           IF tags[i] EQ 'UID' THEN CONTINUE

           CASE SIZE((*self.pahdb).data.species.(i), /TYPE) OF
              2: command += ",'"+tags[i]+"'"+',0'
              3: command += ",'"+tags[i]+"'"+',0L'
              4: command += ",'"+tags[i]+"'"+',0E'
              5: command += ",'"+tags[i]+"'"+',0D'
              7: command += ",'"+tags[i]+"'"+',""'
              ELSE:
           ENDCASE
        ENDFOR

        command += ')'

        result = EXECUTE(command)

        ntransitions = N_ELEMENTS((*self.pahdb).data.transitions)

        joined = REPLICATE(template, ntransitions)

        STRUCT_ASSIGN,(*self.pahdb).data.transitions,joined,/NOZERO

        nids = N_ELEMENTS((*self.pahdb).data.species)

        jtags = TAG_NAMES(joined)

        FOR i = 0, nids - 1 DO BEGIN

           select = WHERE(joined.uid EQ (*self.pahdb).data.species[i].uid, nselect)

           FOR j = 0, ntags - 1 DO BEGIN

              k = WHERE(jtags EQ tags[j])

              IF tags[j] EQ 'UID' THEN CONTINUE

              joined[select].(k) = (*self.pahdb).data.species[i].(j)
           ENDFOR
        ENDFOR

        PRINT
        PRINT,"========================================================="
        PRINT,"STORING INDEX IN CACHE        : "+file_cache
        PRINT,"========================================================="
        PRINT

        SAVE,joined,FILENAME=file_cache
     ENDELSE

     self.joined = PTR_NEW(TEMPORARY(joined))
  ENDIF

  words = ''

  len = STRLEN(Str)

  i = 0

  WHILE 1 DO BEGIN

     IF i EQ len THEN BREAK

     WHILE STRMID(Str, i, 1) EQ " " DO ++i

     token = STRMID(Str, i, 1)

     IF SIZE(WHERE(["=", "<", ">", "(", ")"] EQ token), /DIMENSIONS) NE 0 THEN BEGIN

        IF ++i LT len AND STRMID(Str, i, 1) EQ "=" THEN token += STRMID(Str, i++, 1)
     ENDIF ELSE IF token EQ "&" THEN BEGIN

        IF ++i LT len AND STRMID(Str, i, 1) EQ "&" THEN token += STRMID(Str, i++, 1)
     ENDIF ELSE IF token EQ "|" THEN BEGIN

        IF ++i LT len AND STRMID(Str, i, 1) EQ "|" THEN token += STRMID(Str, i++, 1)
     ENDIF ELSE IF token EQ "!" THEN BEGIN

        IF ++i LT len AND STRMID(Str, i, 1) EQ "=" THEN token += STRMID(Str, i++, 1)
     ENDIF ELSE BEGIN

        ++i
        WHILE i LT len AND SIZE(WHERE([" ", "=", "<", ">", "&", "|", "(", ")", "!"] EQ STRMID(Str, i, 1)), /DIMENSIONS) EQ 0 DO token += STRMID(Str, i++, 1)
     ENDELSE

     words = [words, token]
  ENDWHILE

  words = words[1:*]

  nwords = N_ELEMENTS(words)

  tokens = {word:'', $
            translation:'', $
            type:'', $
            valid:0}

  FOR i = 0, nwords - 1 DO tokens = [tokens, self->TokenizeWords(words[i])]

  tokens = tokens[1:*]

  QUERY = self->ParseTokens(tokens)

  IF QUERY EQ '' THEN RETURN,-1

  res = EXECUTE('select = WHERE(' + QUERY + ', Count)')

  IF Count EQ 0 THEN RETURN,-1

  u = (*self.joined)[select[UNIQ((*self.joined)[select].uid)]].uid

  Count = N_ELEMENTS(u)

  RETURN,u
END

;+
; Checks whether the given Version matched the version of the parsed
; database.
;
; :Returns:
;   long
;
; :Params:
;   Version: in, required, type=string
;     Version string
;
; :Categories:
;   VERSIONING
;-
FUNCTION AmesPAHdbIDLSuite::CheckVersion,Version

  COMPILE_OPT IDL2

  ON_ERROR,2

  RETURN,STRCMP((*self.pahdb).version, Version)
END

;+
; Returns versioning information.
;
; :Returns:
;   structure or string
;
; :Keywords:
;   String: out, optional, type=string
;     Formatted version string
;
; :Categories:
;   VERSIONING
;-
FUNCTION AmesPAHdbIDLSuite::GetVersion,String=String

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF KEYWORD_SET(String) THEN RETURN,STRING(FORMAT='("PAHdb,",X,A0,", version",X,A0,X,"(",A0,",",X,A0,")")', (*self.pahdb).type, (*self.pahdb).version, (*self.pahdb).date, (*self.pahdb).full ? "complete" : "partial")

  RETURN,{AmesPAHdbIDLSuite_Version_S, $
          type:(*self.pahdb).type, $
          number:(*self.pahdb).version, $
          date:(*self.pahdb).date, $
          full:(*self.pahdb).full}
END

;+
; Extends the database with outside-data
;
; :Keywords:
;   Species: in, optional, type=AmesPAHdb_Property_S
;     Species data
;   Comments: in, optional, type=AmesPAHdb_Comment_S
;     Comment data
;   References: in, optional, type=AmesPAHdb_Reference_S
;     Reference data
;   Transitions: in, optional, type=AmesPAHdb_Transition_S
;     Transition data
;   Geometries: in, optional, type=AmesPAHdb_Geometry_S
;     Geometry data
;   Laboratory: in, optional, type=AmesPAHdb_Laboratory_S
;     Laboratory data
;
; :Categories:
;   EXTENDING
;
; :Private:
;-
PRO AmesPAHdbIDLSuite::ExtendDatabase,Species=Species,Comments=Comments,References=References,Transitions=Transitions,Geometries=Geometries,Laboratory=Laboratory

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF NOT PTR_VALID(self.pahdb) THEN BEGIN
     PRINT
     PRINT,"========================================================="
     PRINT,"                NO VALID DATABASE                        "
     PRINT,"========================================================="
     PRINT
     RETURN
  ENDIF

  s = (*self.pahdb).data.species
  IF KEYWORD_SET(Species) THEN  s = [s, Species]
  c = (*self.pahdb).data.comments
  IF KEYWORD_SET(Comments) THEN  c = [c, Comments]
  r = (*self.pahdb).data.references
  IF KEYWORD_SET(References) THEN  r = [r, References]
  t = (*self.pahdb).data.transitions
  IF KEYWORD_SET(Transitions) THEN  t = [t, Transitions]
  g = (*self.pahdb).data.geometries
  IF KEYWORD_SET(Geometries) THEN  g = [g, Geometries]
  l = (*self.pahdb).data.laboratory
  IF KEYWORD_SET(Laboratory) THEN  l = [l, Laboratory]

  (*self.pahdb) = {filename:(*self.pahdb).filename, $
                   type:(*self.pahdb).type, $
                   version:(*self.pahdb).version, $
                   date:(*self.pahdb).date,$
                   full:(*self.pahdb).full, $
                   comment:(*self.pahdb).comment, $
                   sizes:(*self.pahdb).sizes, $
                   data:{species:s, $
                         comments:c, $
                         references:r, $
                         transitions:t, $
                         geometries:g, $
                         laboratory:l}}

	(*self.pahdb).sizes = {nspecies:N_ELEMENTS(s), ncomments:N_ELEMENTS(c), nreferences:N_ELEMENTS(r), ntransitions:N_ELEMENTS(t), ngeometries:N_ELEMENTS(g), nlaboratory:N_ELEMENTS(l)}
END

;+
; Reads in a database XML-file
;
; :Params:
;   File: in, required, type=string
;     Database XML-file
;
; :Keywords:
;   Check: in, optional, type=int
;     Check the consistency of the XML-file (defaults to 1)
;   Cache: in, optional, type=int
;     Use the cache (defaults to 1)
;
; :Categories:
;   PARSING
;-
PRO AmesPAHdbIDLSuite::ReadFile,File,Check=Check,Cache=Cache

  COMPILE_OPT IDL2

  ON_ERROR,2

  self->File_MD5,File,hash,err

  IF err NE '' THEN hash = FILE_BASENAME(File, '.xml')

  self.file_md5 = hash[0]

  file_cache = self->Cache_DIR()+self.file_md5+'.sav'

  IF SIZE(Cache, /TYPE) EQ 0 THEN Cache = 1

  IF NOT Cache THEN FILE_DELETE,file_cache,/ALLOW_NONEXISTENT

  IF Cache AND FILE_TEST(file_cache, /READ) THEN BEGIN

     PRINT
     PRINT,"========================================================="
     PRINT,"         NASA Ames PAH IR SPECTROSCOPIC DATABASE         "
     PRINT,"========================================================="
     PRINT

     PRINT
     PRINT,"========================================================="
     PRINT,"             RESTORING DATABASE FROM CACHE               "
     PRINT,"========================================================="
     PRINT

     timer = SYSTIME(/SECONDS)
     RESTORE,FILENAME=file_cache,/RELAXED_STRUCTURE_ASSIGNMENT
     PRINT,"========================================================="
     PRINT,FORMAT='(A-30, ": ", A0)',"FILENAME",""+file_cache
     PRINT,FORMAT='(A-30, ": ", A0)',"ORIGINAL FILE",""+pahdb.filename
     PRINT,FORMAT='(A-30, ": ", A0)',"DATABASE",""+pahdb.type+" ("+(pahdb.full ? "complete" : "partial")+")"
     timer = SYSTIME(/SECONDS) - timer
     IF timer LT 1 THEN PRINT,FORMAT='(A-30, ": ", A0)',"RESTORE TIME",STRING(FORMAT='(I-3)', timer*1D3)+" MILLISECONDS" $
     ELSE IF timer LT 60 THEN PRINT,FORMAT='(A-30, ": ", A0)',"RESTORE TIME",STRING(FORMAT='(I02)',timer)+" SECONDS" $
     ELSE IF timer LT 3600 THEN PRINT,FORMAT='(A-30, ": ", A0)',"RESTORE TIME",STRING(FORMAT='(I02,":",I02)',timer/60,timer MOD 60)+" MINUTES" $
     ELSE IF timer LT 86400 THEN PRINT,FORMAT='(A-30, ": ", A0)',"RESTORE TIME",STRING(FORMAT='(I02,":",I02,":",I02)',timer/3600,(timer MOD 3600)/60,(timer MOD 3600) MOD 60)+" HOURS" $
     ELSE PRINT,FORMAT='(A-30, ": ", A0)',"RESTORE TIME",STRING(FORMAT='(I03)',timer/86400E)+" DAYS"
     PRINT,FORMAT='(A-30, ": ", A0)',"VERSION (DATE)",""+pahdb.version+" ("+pahdb.date+")"
     PRINT,FORMAT='(A-30, ": ", I-0)',"TOTAL NUMBER OF SPECIES",N_ELEMENTS(pahdb.data.species)
     PRINT,FORMAT='(A-30, ": ", I-0)',"TOTAL NUMBER OF TRANSITIONS",N_ELEMENTS(pahdb.data.transitions)
     PRINT,FORMAT='(A-30,": ", A0)',"COMMENT",pahdb.comment
     PRINT,"========================================================="
     PRINT
  ENDIF ELSE BEGIN
     PRINT
     PRINT,"========================================================="
     PRINT,"      PARSING DATABASE: THIS MAY TAKE A FEW MINUTES      "
     PRINT,"========================================================="
     PRINT

     IF SIZE(Check, /TYPE) EQ 0 THEN Check = 2

     XML = OBJ_NEW('AmesPAHdbIDLSuite_XMLParser', SCHEMA_CHECKING=Check)
     XML->ParseFile,File

     IF XML->Status() EQ 0 THEN BEGIN

        pahdb = CREATE_STRUCT('file_md5', self.file_md5, XML->GetDatabase())

        PRINT
        PRINT,"========================================================="
        PRINT,"STORING DATABASE IN CACHE     : "+file_cache
        PRINT,"========================================================="
        PRINT

        SAVE,pahdb,FILENAME=file_cache
     ENDIF
  ENDELSE

  self.pahdb = PTR_NEW(TEMPORARY(pahdb))
END

;+
; Clean-up an AmesPAHdbIDLSuite-instance
;
; :Categories:
;   CLASS
;
; :Private:
;-
PRO AmesPAHdbIDLSuite::Cleanup

  COMPILE_OPT IDL2

  ON_ERROR,2

  IF PTR_VALID(self.pahdb) THEN PTR_FREE,self.pahdb

  IF PTR_VALID(self.joined) THEN PTR_FREE,self.joined
END

;+
; Create an AmesPAHdbIDLSuite-instance.
;
; :Returns:
;   AmesPAHdbIDLSuite-instance
;
; :Keywords:
;   Filename: in, optional, type=string
;     Database XML-file
;   Check: in, optional, type=int
;     Check the consistency of the XML-file (defaults to 1)
;   Cache: in, optional, type=int
;     Use the cache (defaults to 1)
;   UpdateCheck: in, optional, type=int
;     Check for updates to the Suite (defaults to 1)
;
; :Categories:
;   CLASS
;-
FUNCTION AmesPAHdbIDLSuite::Init,Filename=Filename,Check=Check,Cache=Cache,UpdateCheck=UpdateCheck

  COMPILE_OPT IDL2

  ON_ERROR,2

  PRINT
  PRINT,"========================================================="
  PRINT
  PRINT,"                    AmesPAHdbIDLSuite                    "
  PRINT
  PRINT,"                           by                            "
  PRINT
  PRINT,"                  Dr. Christiaan Boersma                 "
  PRINT
  PRINT,"========================================================="
  PRINT

  version_file = FILE_DIRNAME(ROUTINE_FILEPATH("amespahdbidlsuite__define", /EITHER)) + '/VERSION'

  IF FILE_TEST(version_file, /READ) THEN BEGIN

     OPENR,funit,version_file,/GET_LUN

     v = ''

     READF,funit,v

     self.version = v

     CLOSE,funit

     FREE_LUN,funit

     PRINT
     PRINT,"========================================================="
     PRINT,"               SUITE DATED: "+STRUPCASE(self.version)
     PRINT,"========================================================="
     PRINT

     IF KEYWORD_SET(UpdateCheck) THEN BEGIN

        self->File_WGET,'https://www.astrochemistry.org/pahdb/theoretical/2.00/ajax/amespahdbidlsuite/versioncheck',remote_version

        IF remote_version EQ '' THEN BEGIN

           PRINT
           PRINT,"========================================================="
           PRINT,"               CHECKING FOR UPDATES FAILED               "
           PRINT,"========================================================="
           PRINT
        ENDIF ELSE BEGIN

           IF self.version EQ remote_version THEN BEGIN

              PRINT
              PRINT,"========================================================="
              PRINT,"                   NO UPDATES AVAILABLE                  "
              PRINT,"========================================================="
              PRINT
           ENDIF ELSE BEGIN

              PRINT
              PRINT,"========================================================="
              PRINT,"           UPDATE AVAILABLE: "+STRUPCASE(remote_version)
              PRINT,"========================================================="
              PRINT
           ENDELSE
        ENDELSE
     ENDIF
  ENDIF

  PRINT
  PRINT,"========================================================="
  PRINT,"      WEBSITE: HTTP://WWW.ASTROCHEMISTRY.ORG/PAHDB/      "
  PRINT,"========================================================="
  PRINT

  PRINT
  PRINT,"========================================================="
  PRINT,"          CONTACT: CHRISTIAAN.BOERSMA@NASA.GOV           "
  PRINT,"========================================================="
  PRINT

  DEFSYSV,'!GDL',EXISTS=isgdl

  IF isgdl THEN BEGIN
     PRINT
     PRINT,"========================================================="
     PRINT,"    THE AMESPAHDBIDLSUITE IS NOT COMPATIBLE WITH GDL     "
     PRINT,"========================================================="
     PRINT
     self.state = 0
     RETURN,0
  ENDIF

  IF NOT KEYWORD_SET(Filename) THEN BEGIN

     DEFSYSV,'!AMESPAHDEFAULTDB',EXISTS=defsys

     IF defsys EQ 1 THEN Filename = !AMESPAHDEFAULTDB $
     ELSE IF GETENV('AMESPAHDEFAULTDB') NE '' THEN Filename = GETENV('AMESPAHDEFAULTDB') $
     ELSE BEGIN
        PRINT
        PRINT,"========================================================="
        PRINT," DATABASE NOT FOUND: SET IDL OR SYSTEM AMESPAHDEFAULTDB ENVIRONMENT VARIABLE"
        PRINT,"========================================================="
        PRINT
        self.state = 0
        RETURN,self.state
     ENDELSE
  ENDIF

  IF NOT FILE_TEST(Filename, /READ) THEN BEGIN
     IF (alt = FILE_WHICH(FILE_BASENAME(Filename), /INCLUDE_CURRENT_DIR))  NE '' THEN Filename = alt $
     ELSE BEGIN
       PRINT
       PRINT,"========================================================="
       PRINT,"      UNABLE TO READ: "+Filename
       PRINT,"========================================================="
       PRINT
       RETURN,0
    ENDELSE
  ENDIF

  self->ReadFile,Filename,Check=Check,Cache=Cache

  RETURN,1
END

;+
; Defines the AmesPAHdbIDLSuite Class.
;
; :Fields:
;   state: type=int
;     Internal state
;   file_md5: type=string
;     MD5-hash of the parsed database file
;   version: type=sting
;     Versioning information
;   pahdb: type=pointer
;     Database pointer
;   joined: type=pointer
;     Pointer to the joined database tables
;
; :Categories:
;   CLASS
;
; :Private:
;-
PRO AmesPAHdbIDLSuite__DEFINE

  COMPILE_OPT IDL2

  ON_ERROR,2

  void = {AmesPAHdbIDLSuite, $
          state:0, $
          file_md5:'', $
          version:'', $
          pahdb:PTR_NEW(), $
          joined:PTR_NEW()}
END

; END OF AmesPAHdbIDLSuite__define.pro
