# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""An extensible ASCII table reader and writer.

daophot.py:
  Classes to read DAOphot table format

:Copyright: Smithsonian Astrophysical Observatory (2011)
:Author: Tom Aldcroft (aldcroft@head.cfa.harvard.edu)
"""

##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are met:
##     * Redistributions of source code must retain the above copyright
##       notice, this list of conditions and the following disclaimer.
##     * Redistributions in binary form must reproduce the above copyright
##       notice, this list of conditions and the following disclaimer in the
##       documentation and/or other materials provided with the distribution.
##     * Neither the name of the Smithsonian Astrophysical Observatory nor the
##       names of its contributors may be used to endorse or promote products
##       derived from this software without specific prior written permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
## ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
## DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
## DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import re
import numpy as np
from . import core
from . import basic
from . import fixedwidth
from ...utils import OrderedDict


class Daophot(core.BaseReader):
    """Read a DAOphot file.
    Example::

      #K MERGERAD   = INDEF                   scaleunit  %-23.7g
      #K IRAF = NOAO/IRAFV2.10EXPORT version %-23s
      #K USER = davis name %-23s
      #K HOST = tucana computer %-23s
      #
      #N ID    XCENTER   YCENTER   MAG         MERR          MSKY           NITER    \\
      #U ##    pixels    pixels    magnitudes  magnitudes    counts         ##       \\
      #F %-9d  %-10.3f   %-10.3f   %-12.3f     %-14.3f       %-15.7g        %-6d
      #
      #N         SHARPNESS   CHI         PIER  PERROR                                \\
      #U         ##          ##          ##    perrors                               \\
      #F         %-23.3f     %-12.3f     %-6d  %-13s
      #
      14       138.538     INDEF   15.461      0.003         34.85955       4        \\
                  -0.032      0.802       0     No_error

    The keywords defined in the #K records are available via the output table
    ``meta`` attribute::

      >>> import os
      >>> from astropy.io import ascii
      >>> filename = os.path.join(ascii.__path__[0], 'tests/t/daophot.dat')
      >>> data = ascii.read(filename)
      >>> for name, keyword in data.meta['keywords'].items():
      ...     print name, keyword['value'], keyword['units'], keyword['format']
      ...
      MERGERAD INDEF scaleunit %-23.7g
      IRAF NOAO/IRAFV2.10EXPORT version %-23s
      USER  name %-23s
      ...

    The unit and formats are available in the output table columns::

      >>> for colname in data.colnames:
      ...     col = data[colname]
      ...     print colname, col.unit, col.format
      ...
      ID None %-9d
      XCENTER pixels %-10.3f
      YCENTER pixels %-10.3f
      ...

    Any column values of INDEF are interpreted as a missing value and will be
    masked out in the resultant table.
    """
    _format_name = 'daophot'
    _io_registry_format_aliases = ['daophot']
    _io_registry_can_write = False
    _description = 'IRAF DAOphot format table'

    def __init__(self):
        core.BaseReader.__init__(self)
        self.header = DaophotHeader()
        self.inputter = core.ContinuationLinesInputter()
        self.inputter.no_continue = r'\s*#'
        self.data.splitter = fixedwidth.FixedWidthSplitter()
        self.data.start_line = 0
        self.data.comment = r'\s*#'

    def write(self, table=None):
        raise NotImplementedError


class DaophotHeader(core.BaseHeader):
    """Read the header from a file produced by the IRAF DAOphot routine."""
    def __init__(self):
        core.BaseHeader.__init__(self)
        self.comment = r'\s*#K'

    def update_meta(self, lines, meta):
        """
        Extract table-level keywords for DAOphot table.  These are indicated by
        a leading '#K ' prefix.
        """
        table_meta = meta['table']
        # Read keywords as a table embedded in the header comments
        comment_lines = [line for line in lines if line.startswith('#')]
        if len(comment_lines) > 0:
            re_header_keyword = re.compile(r'[#]K'
                                           r'\s+ (?P<name> \w+)'
                                           r'\s* = (?P<stuff> .+) $',
                                           re.VERBOSE)

            table_meta['keywords'] = OrderedDict()
            for line in comment_lines:
                m = re_header_keyword.match(line)
                if m:
                    vals = m.group('stuff').strip().rsplit(None, 2)
                    keyword_dict = {'units': vals[-2],
                                    'format': vals[-1]}
                    keyword_dict['value'] = (vals[0] if len(vals) > 2 else "")
                    table_meta['keywords'][m.group('name')] = keyword_dict

    def get_cols(self, lines):
        """Initialize the header Column objects from the table ``lines`` for a DAOphot
        header.  The DAOphot header is specialized so that we just copy the entire BaseHeader
        get_cols routine and modify as needed.

        :param lines: list of table lines
        :returns: list of table Columns
        """

        # Parse a series of column defintion lines like below.  There may be several
        # such blocks in a single file (where continuation characters have already been
        # stripped).
        # #N ID    XCENTER   YCENTER   MAG         MERR          MSKY           NITER
        # #U ##    pixels    pixels    magnitudes  magnitudes    counts         ##
        # #F %-9d  %-10.3f   %-10.3f   %-12.3f     %-14.3f       %-15.7g        %-6d
        coldef_lines = ['', '', '']
        starts = ('#N ', '#U ', '#F ')
        col_width = []
        col_len_def = re.compile(r'[0-9]+')
        re_colformat_def = re.compile(r'#F([^#]+)')
        for line in lines:
            if not line.startswith('#'):
                break  # End of header lines
            else:
                formatmatch = re_colformat_def.search(line)
                if formatmatch:
                    form = formatmatch.group(1).split()
                    width = ([int(col_len_def.search(s).group()) for s in form])
                    # original data format might be shorter than 80 characters
                    # and filled with spaces
                    width[-1] = 80 - sum(width[:-1])
                    col_width.extend(width)
                for i, start in enumerate(starts):
                    if line.startswith(start):
                        line_stripped = line[2:]
                        coldef_lines[i] = coldef_lines[i] + line_stripped
                        break

        # At this point colddef_lines has three lines corresponding to column
        # names, unit, and format.  Get the column names by splitting the
        # first line on whitespace.
        self.names = coldef_lines[0].split()
        if not self.names:
            raise core.InconsistentTableError('No column names found in DAOphot header')

        ends = np.cumsum(col_width)
        starts = ends - col_width

        # If there wasn't a #U defined (not sure of DAOphot specification), then
        # replace the empty line with the right number of ## indicators, which matches
        # the DAOphot "no unit" tag.
        for i, coldef_line in enumerate(coldef_lines):
            if not coldef_line:
                coldef_lines[i] = '## ' * len(self.names)

        # Read the three lines as a basic table.
        reader = core._get_reader(Reader=basic.Basic, comment=None)
        reader.header.comment = None
        coldefs = reader.read(coldef_lines)

        # Create the list of io.ascii column objects
        self._set_cols_from_names()

        # Set unit and format as needed.
        for col in self.cols:
            if coldefs[col.name][0] != '##':
                col.unit = coldefs[col.name][0]
            if coldefs[col.name][1] != '##':
                col.format = coldefs[col.name][1]

        # Set column start and end positions.
        for i, col in enumerate(self.cols):
            col.start = starts[i]
            col.end = ends[i]
            if hasattr(col, 'format'):
                if any(x in col.format for x in 'fg'):
                    col.type = core.FloatType
                elif 'd' in col.format:
                    col.type = core.IntType
                elif 's' in col.format:
                    col.type = core.StrType

        # INDEF is the missing value marker
        self.data.fill_values.append(('INDEF', '0'))
