
"""
This module helps you working with different kinds of text based
star catalogs.  To adapt the mechanism to your own catlog, create a
class extending Catalog() and overwrite the fields and functions as
needed.
"""

import numpy

class ParserError(ValueError):
    pass

class Catalog():
    def __init__(self, catfile):
        """
          Class with basic When extending the class, change the default settings
          by overwriting following variables:

            intro: Number of lines to skip in the beginning of the catalog file.
            delimiter: Column delimiter
            strict: If True, the Parser will exit when column descriptions
                don't match the data.
        """
        super().__init__()
        self.catfile = catfile
        self.intro = 0
        self.delimiter = ','
        self.strict = True
        # [{'label': str,
        #   'type': type str|int|float,
        #   'units': str,
        #   'explanation': str}]
        self.columndescriptions = []

    def read_column_descriptions(self, descr=None):
        columns = []
        for line in descr.splitlines():
            if line.startswith('#'):
                continue
            fields = [s.strip() for s in line.split(",")]
            assert fields[1] in ['str', 'int', 'float']
            columns.append({
                'label': fields[0],
                'type': eval(fields[1]),
                'units': fields[2],
                'explanation': fields[3]
                })
        return columns

    def parse_record(self, linum, line):
        fields = [s.strip() for s in line.split(self.delimiter)]
        if self.strict and len(fields) != self._colnum:
            raise ParserError(
                "Wrong number of columns in line %d (%d, expected %d)"
                ""%(linum, len(fields), self._colnum))
        parsed = []
        for i, (field, type) in enumerate(zip(fields, self._coltypes)):
            try:
                if len(field) == 0:
                    if type == str: parsed.append("")
                    else: parsed.append(numpy.nan)
                else:
                    parsed.append(type(field))
            except ValueError as e:
                if not self.strict:
                    parsed.append(field)
                else:
                    raise ParserError(
                        "In line %d, field %s: %s"
                        ""%(linum, self.columndescriptions[i]['label'], str(e)))
        return parsed

    def records(self, columns=None, filters=None):
        """
          Generator function iterating over the catalog entries.

          Arguments:
            columns:
                list of column labels
            filters:
                dict with column labels as keys and these values:
                  if column has type int / float: (min, max)
                  if column has type str: regexp as string or compiled object
                you can filter for all columns, not only the ones in `columns`
        """
        # save intermediate data for parse_record
        self._colnum = len(self.columndescriptions)
        self._coltypes = [col['type'] for col in self.columndescriptions]

        labels = [col['label'] for col in self.columndescriptions]
        # convert column labels to indices
        if columns is None:
            # list all columns
            colidxs = list(range(len(self.columndescriptions)))
        else:
            colidxs = [labels.index(label) for label in columns]
        # convert filters to list and compile regexes
        filterlist = []
        if filters is not None:
            filterlist = []
            for label, rule in filters.items():
                colidx = labels.index(label)
                if self._coltypes[colidx] == str and type(rule) == str:
                    rule = re.compile(rule)
                filterlist.append( (colidx, rule) )

        with open(self.catfile, 'r') as f:
            for i in range(self.intro):
                next(f) # skip intro lines
            for linum, line in enumerate(f):
                fields = self.parse_record(linum, line)
                for colidx, rule in filterlist:
                    if self._coltypes[colidx] == str:
                        if not rule.fullmatch(fields[colidx]):
                            break
                    else:
                        if not (rule[0] <= fields[colidx] <= rule[1]):
                            break
                else:
                    yield [fields[i] for i in colidxs]


class Hipparcos(Catalog):
    def __init__(self, catfile):
        super().__init__(catfile)
        self.intro = 0
        self.delimiter = '|'
        self.columndescriptions = \
          self.read_column_descriptions(DESCRIPTION_HIPPARCOS)

    def read_column_descriptions(self, descr):
        columns = []
        for line in descr.splitlines()[1:]:
            fmtidentifier = line[10:15].strip()[0]
            fmt = {'A': str, 'I': int, 'F': float}[fmtidentifier]
            columns.append({
                'label': line[23:33].strip(),
                'type': fmt,
                'units': line[16:22].strip(),
                'explanation': line[34:75].strip()
                })
        return columns


class HYG(Catalog):
    def __init__(self, catfile):
        super().__init__(catfile)
        self.intro = 1
        self.delimiter = ','
        self.columndescriptions = self.read_column_descriptions(DESCRIPTION_HYG)

DESCRIPTION_HIPPARCOS = \
"""Bytes Format Units   Label     Explanations
       1  A1    ---     Catalog   [H] Catalogue (H=Hipparcos)               (H0)
   9- 14  I6    ---     HIP       Identifier (HIP number)                   (H1)
      16  A1    ---     Proxy    *[HT] Proximity flag                       (H2)
  18- 28  A11   ---     RAhms     Right ascension in h m s, ICRS (J1991.25) (H3)
  30- 40  A11   ---     DEdms     Declination in deg ' ", ICRS (J1991.25)   (H4)
  42- 46  F5.2  mag     Vmag      ? Magnitude in Johnson V                  (H5)
      48  I1    ---     VarFlag  *[1,3]? Coarse variability flag            (H6)
      50  A1    ---     r_Vmag   *[GHT] Source of magnitude                 (H7)
  52- 63  F12.8 deg     RAdeg    *? alpha, degrees (ICRS, Epoch=J1991.25)   (H8)
  65- 76  F12.8 deg     DEdeg    *? delta, degrees (ICRS, Epoch=J1991.25)   (H9)
      78  A1    ---     AstroRef *[*+A-Z] Reference flag for astrometry    (H10)
  80- 86  F7.2  mas     Plx       ? Trigonometric parallax                 (H11)
  88- 95  F8.2  mas/yr  pmRA     *? Proper motion mu_alpha.cos(delta), ICRS(H12)
  97-104  F8.2  mas/yr  pmDE     *? Proper motion mu_delta, ICRS           (H13)
 106-111  F6.2  mas     e_RAdeg  *? Standard error in RA*cos(DEdeg)        (H14)
 113-118  F6.2  mas     e_DEdeg  *? Standard error in DE                   (H15)
 120-125  F6.2  mas     e_Plx     ? Standard error in Plx                  (H16)
 127-132  F6.2  mas/yr  e_pmRA    ? Standard error in pmRA                 (H17)
 134-139  F6.2  mas/yr  e_pmDE    ? Standard error in pmDE                 (H18)
 141-145  F5.2  ---     DE:RA     [-1/1]? Correlation, DE/RA*cos(delta)    (H19)
 147-151  F5.2  ---     Plx:RA    [-1/1]? Correlation, Plx/RA*cos(delta)   (H20)
 153-157  F5.2  ---     Plx:DE    [-1/1]? Correlation, Plx/DE              (H21)
 159-163  F5.2  ---     pmRA:RA   [-1/1]? Correlation, pmRA/RA*cos(delta)  (H22)
 165-169  F5.2  ---     pmRA:DE   [-1/1]? Correlation, pmRA/DE             (H23)
 171-175  F5.2  ---     pmRA:Plx  [-1/1]? Correlation, pmRA/Plx            (H24)
 177-181  F5.2  ---     pmDE:RA   [-1/1]? Correlation, pmDE/RA*cos(delta)  (H25)
 183-187  F5.2  ---     pmDE:DE   [-1/1]? Correlation, pmDE/DE             (H26)
 189-193  F5.2  ---     pmDE:Plx  [-1/1]? Correlation, pmDE/Plx            (H27)
 195-199  F5.2  ---    pmDE:pmRA  [-1/1]? Correlation, pmDE/pmRA           (H28)
 201-203  I3    %       F1        ? Percentage of rejected data            (H29)
 205-209  F5.2  ---     F2       *? Goodness-of-fit parameter              (H30)
 211-216  I6    ---     ---       HIP number (repetition)                  (H31)
 218-223  F6.3  mag     BTmag     ? Mean BT magnitude                      (H32)
 225-229  F5.3  mag     e_BTmag   ? Standard error on BTmag                (H33)
 231-236  F6.3  mag     VTmag     ? Mean VT magnitude                      (H34)
 238-242  F5.3  mag     e_VTmag   ? Standard error on VTmag                (H35)
     244  A1    ---     m_BTmag  *[A-Z*-] Reference flag for BT and VTmag  (H36)
 246-251  F6.3  mag     B-V       ? Johnson B-V colour                     (H37)
 253-257  F5.3  mag     e_B-V     ? Standard error on B-V                  (H38)
     259  A1    ---     r_B-V     [GT] Source of B-V from Ground or Tycho  (H39)
 261-264  F4.2  mag     V-I       ? Colour index in Cousins' system        (H40)
 266-269  F4.2  mag     e_V-I     ? Standard error on V-I                  (H41)
     271  A1    ---     r_V-I    *[A-T] Source of V-I                      (H42)
     273  A1    ---     CombMag   [*] Flag for combined Vmag, B-V, V-I     (H43)
 275-281  F7.4  mag     Hpmag    *? Median magnitude in Hipparcos system   (H44)
 283-288  F6.4  mag     e_Hpmag  *? Standard error on Hpmag                (H45)
 290-294  F5.3  mag     Hpscat    ? Scatter on Hpmag                       (H46)
 296-298  I3    ---     o_Hpmag   ? Number of observations for Hpmag       (H47)
     300  A1    ---     m_Hpmag  *[A-Z*-] Reference flag for Hpmag         (H48)
 302-306  F5.2  mag     Hpmax     ? Hpmag at maximum (5th percentile)      (H49)
 308-312  F5.2  mag     HPmin     ? Hpmag at minimum (95th percentile)     (H50)
 314-320  F7.2  d       Period    ? Variability period (days)              (H51)
     322  A1    ---     HvarType *[CDMPRU]? variability type               (H52)
     324  A1    ---     moreVar  *[12] Additional data about variability   (H53)
     326  A1    ---    morePhoto  [ABC] Light curve Annex                  (H54)
 328-337  A10   ---     CCDM      CCDM identifier                          (H55)
     339  A1    ---     n_CCDM   *[HIM] Historical status flag             (H56)
 341-342  I2    ---     Nsys      ? Number of entries with same CCDM       (H57)
 344-345  I2    ---     Ncomp     ? Number of components in this entry     (H58)
     347  A1    ---     MultFlag *[CGOVX] Double/Multiple Systems flag     (H59)
     349  A1    ---     Source   *[PFILS] Astrometric source flag          (H60)
     351  A1    ---     Qual     *[ABCDS] Solution quality                 (H61)
 353-354  A2    ---     m_HIP     Component identifiers                    (H62)
 356-358  I3    deg     theta     ? Position angle between components      (H63)
 360-366  F7.3  arcsec  rho       ? Angular separation between components  (H64)
 368-372  F5.3  arcsec  e_rho     ? Standard error on rho                  (H65)
 374-378  F5.2  mag     dHp       ? Magnitude difference of components     (H66)
 380-383  F4.2  mag     e_dHp     ? Standard error on dHp                  (H67)
     385  A1    ---     Survey    [S] Flag indicating a Survey Star        (H68)
     387  A1    ---     Chart    *[DG] Identification Chart                (H69)
     389  A1    ---     Notes    *[DGPWXYZ] Existence of notes             (H70)
 391-396  I6    ---     HD        [1/359083]? HD number III/135            (H71)
 398-407  A10   ---     BD        Bonner DM I/119, I/122                   (H72)
 409-418  A10   ---     CoD       Cordoba Durchmusterung (DM) I/114        (H73)
 420-429  A10   ---     CPD       Cape Photographic DM I/108               (H74)
 431-434  F4.2  mag     (V-I)red  V-I used for reductions                  (H75)
 436-447  A12   ---     SpType    Spectral type                            (H76)
     449  A1    ---     r_SpType *[1234GKSX]? Source of spectral type      (H77)
"""

DESCRIPTION_HYG = """
# https://github.com/astronexus/HYG-Database
# Label,Type,Units,Explanation
id,int,,database primary key
hip,int,,ID in Hipparcos catalog
hd,int,,ID in Henry Draper catalog
hr,int,,ID in Harvard Revised catalog and Yale Bright Star Catalog
gl,str,,ID in third edition of Gliese Catalog of Nearby Stars
bf,str,,Bayer / Flamsteed designation
proper,str,,Common name
ra,float,deg,J2000.0
dec,float,deg,J2000.0
dist,float,pc,
pmra,float,milliarcseconds per year,proper motion
pmdec,float,milliarcseconds per year,proper motion
rv,float,mag,radial velocity
mag,float,mag,apparent visual magnitude
absmag,float,mag,absolute visual magnitude
spect,str,,spectral type
ci,float,,color index
x,float,pc,
y,float,pc,
z,float,pc,
vx,float,pc/yr,
vy,float,pc/yr,
vz,float,pc/yr,
rarad,float,rad,
decrad,float,rad,
pmrarad,float,rad/year,proper motion
pmdecrad,float,rad/year,proper motion
bayer,str,,Bayer designation
flam,str,,Flamsteed number
con,str,,standard constellation abbreviation
comp,int,,ID of companion star
comp_primary,int,,ID of primary star
base,str,,ID or name for this multi-star system (Gliese stars only)
lum,float,Lsol,luminosity
var,str,,standard variable star designation
var_min,float,mag,
var_max,float,mag,
"""
