Source code for

# Licensed under a 3-clause BSD style license - see PYFITS.rst

import collections
import copy
import itertools
import re
import warnings

from .card import Card, _pad, KEYWORD_LENGTH
from .file import _File
from .util import encode_ascii, decode_ascii, fileobj_closed, fileobj_is_binary

from astropy.utils import isiterable
from astropy.utils.exceptions import AstropyUserWarning
from astropy.utils.decorators import deprecated_renamed_argument

BLOCK_SIZE = 2880  # the FITS block size

# This regular expression can match a *valid* END card which just consists of
# the string 'END' followed by all spaces, or an *invalid* end card which
# consists of END, followed by any character that is *not* a valid character
# for a valid FITS keyword (that is, this is not a keyword like 'ENDER' which
# starts with 'END' but is not 'END'), followed by any arbitrary bytes.  An
# invalid end card may also consist of just 'END' with no trailing bytes.
HEADER_END_RE = re.compile(encode_ascii(
    r'(?:(?P<valid>END {77}) *)|(?P<invalid>END$|END {0,76}[^A-Z0-9_-])'))

# According to the FITS standard the only characters that may appear in a
# header record are the restricted ASCII chars from 0x20 through 0x7E.
VALID_HEADER_CHARS = set(map(chr, range(0x20, 0x7F)))
END_CARD = 'END' + ' ' * 77

__doctest_skip__ = ['Header', 'Header.*']

class _CardAccessor:
    This is a generic class for wrapping a Header in such a way that you can
    use the header's slice/filtering capabilities to return a subset of cards
    and do something with them.

    This is sort of the opposite notion of the old CardList class--whereas
    Header used to use CardList to get lists of cards, this uses Header to get
    lists of cards.

    # TODO: Consider giving this dict/list methods like Header itself
    def __init__(self, header):
        self._header = header

    def __repr__(self):
        return '\n'.join(repr(c) for c in self._header._cards)

    def __len__(self):
        return len(self._header._cards)

    def __iter__(self):
        return iter(self._header._cards)

    def __eq__(self, other):
        # If the `other` item is a scalar we will still treat it as equal if
        # this _CardAccessor only contains one item
        if not isiterable(other) or isinstance(other, str):
            if len(self) == 1:
                other = [other]
                return False

        for a, b in itertools.zip_longest(self, other):
            if a != b:
                return False
            return True

    def __ne__(self, other):
        return not (self == other)

    def __getitem__(self, item):
        if isinstance(item, slice) or self._header._haswildcard(item):
            return self.__class__(self._header[item])

        idx = self._header._cardindex(item)
        return self._header._cards[idx]

    def _setslice(self, item, value):
        Helper for implementing __setitem__ on _CardAccessor subclasses; slices
        should always be handled in this same way.

        if isinstance(item, slice) or self._header._haswildcard(item):
            if isinstance(item, slice):
                indices = range(*item.indices(len(self)))
                indices = self._header._wildcardmatch(item)
            if isinstance(value, str) or not isiterable(value):
                value = itertools.repeat(value, len(indices))
            for idx, val in zip(indices, value):
                self[idx] = val
            return True
        return False

class _HeaderComments(_CardAccessor):
    A class used internally by the Header class for the Header.comments
    attribute access.

    This object can be used to display all the keyword comments in the Header,
    or look up the comments on specific keywords.  It allows all the same forms
    of keyword lookup as the Header class itself, but returns comments instead
    of values.

    def __iter__(self):
        for card in self._header._cards:
            yield card.comment

    def __repr__(self):
        """Returns a simple list of all keywords and their comments."""

        keyword_length = KEYWORD_LENGTH
        for card in self._header._cards:
            keyword_length = max(keyword_length, len(card.keyword))
        return '\n'.join('{:>{len}}  {}'.format(c.keyword, c.comment,
                         for c in self._header._cards)

    def __getitem__(self, item):
        Slices and filter strings return a new _HeaderComments containing the
        returned cards.  Otherwise the comment of a single card is returned.

        item = super().__getitem__(item)
        if isinstance(item, _HeaderComments):
            # The item key was a slice
            return item
        return item.comment

    def __setitem__(self, item, comment):
        Set/update the comment on specified card or cards.

        Slice/filter updates work similarly to how Header.__setitem__ works.

        if self._header._set_slice(item, comment, self):

        # In this case, key/index errors should be raised; don't update
        # comments of nonexistent cards
        idx = self._header._cardindex(item)
        value = self._header[idx]
        self._header[idx] = (value, comment)

class _HeaderCommentaryCards(_CardAccessor):
    This is used to return a list-like sequence over all the values in the
    header for a given commentary keyword, such as HISTORY.

    def __init__(self, header, keyword=''):
        self._keyword = keyword
        self._count = self._header.count(self._keyword)
        self._indices = slice(self._count).indices(self._count)

    # __len__ and __iter__ need to be overridden from the base class due to the
    # different approach this class has to take for slicing
    def __len__(self):
        return len(range(*self._indices))

    def __iter__(self):
        for idx in range(*self._indices):
            yield self._header[(self._keyword, idx)]

    def __repr__(self):
        return '\n'.join(self)

    def __getitem__(self, idx):
        if isinstance(idx, slice):
            n = self.__class__(self._header, self._keyword)
            n._indices = idx.indices(self._count)
            return n
        elif not isinstance(idx, int):
            raise ValueError('{} index must be an integer'.format(self._keyword))

        idx = list(range(*self._indices))[idx]
        return self._header[(self._keyword, idx)]

    def __setitem__(self, item, value):
        Set the value of a specified commentary card or cards.

        Slice/filter updates work similarly to how Header.__setitem__ works.

        if self._header._set_slice(item, value, self):

        # In this case, key/index errors should be raised; don't update
        # comments of nonexistent cards
        self._header[(self._keyword, item)] = value

def _block_size(sep):
    Determine the size of a FITS header block if a non-blank separator is used
    between cards.

    return BLOCK_SIZE + (len(sep) * (BLOCK_SIZE // Card.length - 1))

def _pad_length(stringlen):
    """Bytes needed to pad the input stringlen to the next FITS block."""

    return (BLOCK_SIZE - (stringlen % BLOCK_SIZE)) % BLOCK_SIZE