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

import gzip
import errno
import http.client
import mmap
import operator
import io
import os
import sys
import tempfile
import warnings
import zipfile
import re

from functools import reduce

import numpy as np

from .util import (isreadable, iswritable, isfile, fileobj_open, fileobj_name,
                   fileobj_closed, fileobj_mode, _array_from_file,
                   _array_to_file, _write_string)
from astropy.utils.data import download_file, _is_url
from astropy.utils.decorators import classproperty
from astropy.utils.exceptions import AstropyUserWarning
from astropy.utils.misc import NOT_OVERWRITING_MSG

# NOTE: Python can be built without bz2.
from astropy.utils.compat.optional_deps import HAS_BZ2
if HAS_BZ2:
    import bz2


# Maps astropy.io.fits-specific file mode names to the appropriate file
# modes to use for the underlying raw files.
IO_FITS_MODES = {
    'readonly': 'rb',
    'copyonwrite': 'rb',
    'update': 'rb+',
    'append': 'ab+',
    'ostream': 'wb',
    'denywrite': 'rb'}

# Maps OS-level file modes to the appropriate astropy.io.fits specific mode
# to use when given file objects but no mode specified; obviously in
# IO_FITS_MODES there are overlaps; for example 'readonly' and 'denywrite'
# both require the file to be opened in 'rb' mode.  But 'readonly' is the
# default behavior for such files if not otherwise specified.
# Note: 'ab' is only supported for 'ostream' which is output-only.
FILE_MODES = {
    'rb': 'readonly', 'rb+': 'update',
    'wb': 'ostream', 'wb+': 'update',
    'ab': 'ostream', 'ab+': 'append'}

# A match indicates the file was opened in text mode, which is not allowed
TEXT_RE = re.compile(r'^[rwa]((t?\+?)|(\+?t?))$')


# readonly actually uses copyonwrite for mmap so that readonly without mmap and
# with mmap still have to same behavior with regard to updating the array.  To
# get a truly readonly mmap use denywrite
# the name 'denywrite' comes from a deprecated flag to mmap() on Linux--it
# should be clarified that 'denywrite' mode is not directly analogous to the
# use of that flag; it was just taken, for lack of anything better, as a name
# that means something like "read only" but isn't readonly.
MEMMAP_MODES = {'readonly': mmap.ACCESS_COPY,
                'copyonwrite': mmap.ACCESS_COPY,
                'update': mmap.ACCESS_WRITE,
                'append': mmap.ACCESS_COPY,
                'denywrite': mmap.ACCESS_READ}

# TODO: Eventually raise a warning, and maybe even later disable the use of
# 'copyonwrite' and 'denywrite' modes unless memmap=True.  For now, however,
# that would generate too many warnings for too many users.  If nothing else,
# wait until the new logging system is in place.

GZIP_MAGIC = b'\x1f\x8b\x08'
PKZIP_MAGIC = b'\x50\x4b\x03\x04'
BZIP2_MAGIC = b'\x42\x5a'


def _is_bz2file(fileobj):
    if HAS_BZ2:
        return isinstance(fileobj, bz2.BZ2File)
    else:
        return False


def _normalize_fits_mode(mode):
    if mode is not None and mode not in IO_FITS_MODES:
        if TEXT_RE.match(mode):
            raise ValueError(
                "Text mode '{}' not supported: "
                "files must be opened in binary mode".format(mode))
        new_mode = FILE_MODES.get(mode)
        if new_mode not in IO_FITS_MODES:
            raise ValueError(f"Mode '{mode}' not recognized")
        mode = new_mode
    return mode


class _File:
    """
    Represents a FITS file on disk (or in some other file-like object).
    """

    def __init__(self, fileobj=None, mode=None, memmap=None, overwrite=False,
                 cache=True):
        self.strict_memmap = bool(memmap)
        memmap = True if memmap is None else memmap

        self._file = None
        self.closed = False
        self.binary = True
        self.mode = mode
        self.memmap = memmap