# -*- coding: utf-8 -*-
"""
idi.py
======

Abstract classes for python Header-Data unit object. This is similar
to the HDU in FITS. Each HDU has a header dictionary and a data
dictionary. The data dictionary can be converted into a pandas
DataFrame object, and there are a few view / verify items also.

"""

import numpy as np
import six
from astropy.table import Table, Column, MaskedColumn
from astropy.nddata import NDData
from collections import OrderedDict


class VerificationError(Exception):
    """ Custom data verification exception """
    pass


class IdiHeader(OrderedDict):
    """
    Header unit for storing header information

    This object stores a header dictionary. For FITS files, order
    is important (particularly for HISTORY cards); but HDF5 does
    not assign any ordering to attributes. As such, order may be lost
    in translation between the two formats.

    Comments should be passed to this object as dictionary entries with
    keys key_COMMENT, e.g. CARD1: 1.20, CARD1_COMMENT: 'Example entry'

    Parameters
    ----------
    values: dict
        Dictionary of header keyword : value pairs.
    """
    def __init__(self, values=None):

        if values is not None:
            super(IdiHeader, self).__init__(values)
        else:
            super(IdiHeader, self).__init__()

    def __repr__(self):
        to_print = ''
        for key, val in self.items():
            if not key.endswith('_COMMENT'):
                if len(key) < 8:
                    key = "%8s" % key

                if type(val) in [bool, float, int]:
                    val = "%32s" % val
                elif isinstance(val, six.text_type):
                    if len(val) < 32:
                        val = "%32s" % val
                comment_key = key + '_COMMENT'
                comment_val = self.get(comment_key)

                if comment_val is None:
                    comment_val = ''
                to_print += "%s %s   / %s\n" % (key, val, comment_val)
        return to_print

class IdiComment(list):
    """
    Class for storing comments within a HDU

    This stores comments as a list of strings. The FITS 'COMMENT'
    keyword should be stripped, and only the actual comment should
    be passed.

    Parameters
    ----------
    comment: list, string, or None
        Comment values to be used in initialization (more can be added
        later by using the append method / other list methods).
    """
    def __init__(self, comment=None):
        new_comment = comment
        if isinstance(comment, type(None)):
            new_comment = []
        elif isinstance(comment, six.text_type):
            new_comment = [comment]

        super(IdiComment, self).__init__(new_comment)

    def __repr__(self):
        to_print = 'COMMENTS\n--------\n'
        for item in self:
            to_print += item + '\n'
        return to_print

class IdiHistory(IdiComment):
    """
    Class for storing history within a HDU

    This stores history log notes as a list of strings. The FITS 'HISTORY'
    keyword should be stripped and only actual history log should be passed.

    Parameters
    ----------
    history: list, string, or None
        Comment values to be used in initialization (more can be added
        later by using the append method / other list methods).
    """
    def __init__(self, history):
        super(IdiHistory, self).__init__(history)

    def __repr__(self):
        to_print = 'HISTORY\n--------\n'
        for item in self:
            to_print += item + '\n'
        return to_print


class IdiPrimaryHdu(OrderedDict):
    """
    Header-data unit for storing PRIMARY metadata

    This is used for storing the FITS / HDFITS PRIMARY HDU, where
    there is NO data payload. Otherwise, the IdiImageHdu should be
    used.

    Parameters
    ----------
    name : string
        Name of HDU. Required.
    comment : list
        List of comments. Optional
    history : list
        List of history entries. Optional
    header: dict
        Header dictionary of keyword:value pairs. Optional
    """
    def __init__(self, name, header=None, history=None, comment=None):
        self.name   = name
        self.header  = IdiHeader(header)
        self.comment = IdiComment(comment)
        self.history = IdiHistory(history)

    def __repr__(self):
        return "IdiPrimary: %s" % self.name


class IdiImageHdu(NDData):
    """ Header-data unit for storing table data

    stores header dictionary and data dictionary

    Parameters
    ----------
    name : string
        Name of HDU. Required.
    comment : list
        List of comments. Optional
    history : list
        List of history entries. Optional
    header: dict
        Header dictionary of keyword:value pairs. Optional
    data: dict
        dictionary of key:value pairs, where data are stored
        as numpy arrays
    """

    def __init__(self, *args, **kwargs):
        self.name = args[0]
        try:
            self.comment = IdiComment(kwargs.pop("comment"))
        except KeyError:
            self.comment = None
        try:
            self.history = IdiHistory(kwargs.pop("history"))
        except KeyError:
            self.history = None
        try:
            self.header = IdiHeader(kwargs.pop("header"))
        except KeyError:
            self.header = None
        super(IdiImageHdu, self).__init__(*args[1:], **kwargs)


class IdiTableHdu(Table):
    """ Header-data unit for storing table data

    This subclasses the astropy.table Table() class.
    It attaches comments, history and a header to make
    it a "HDU", instead of just a Table

    Parameters
    ----------
     name : string
        Name of HDU. Required.
    comment : list
        List of comments. Optional
    history : list
        List of history entries. Optional
    header: dict
        Header dictionary of keyword:value pairs. Optional
    data : numpy ndarray, dict, list, or Table, optional
        Data to initialize table.
    mask : numpy ndarray, dict, list, optional
        The mask to initialize the table
    names : list, optional
        Specify column names
    dtypes : list, optional
        Specify column data types
    meta : dict, optional
        Metadata associated with the table
    copy : boolean, optional
        Copy the input data (default=True).
    """
    def __init__(self, *args, **kwargs):
        self.name = args[0]

        try:
            self.comment = IdiComment(kwargs.pop("comment"))
        except KeyError:
            self.comment = None
        try:
            self.history = IdiHistory(kwargs.pop("history"))
        except KeyError:
            self.history = None
        try:
            self.header = IdiHeader(kwargs.pop("header"))
        except KeyError:
            self.header = IdiHeader()
        super(IdiTableHdu, self).__init__(*args[1:], **kwargs)

        # # Add self.data item, which is missing in Table()
        # self.data = None


class IdiColumn(Column):
    """ IDI version of astropy.table Column()

    This subclasses the astropy.table Column() class, to provide an equivalent
    comment object for IDI data conversion. This subclass adds the ability to
    name the column

    Parameters
    ----------
    name: string
        name of column. This is a required argument for IdiColumn, and must be
        the first argument.
        Column name and key for reference within Table
    data : list, ndarray or None
        Column data values
    dtype : numpy.dtype compatible value
        Data type for column
    shape : tuple or ()
        Dimensions of a single row element in the column data
    length : int or 0
        Number of row elements in column data
    description : str or None
        Full description of column
    unit : str or None
        Physical unit
    format : str or None or function or callable
        Format string for outputting column values. This can be an “old-style”
        (format % value) or “new-style” (str.format) format specification
        string or a function or any callable object that accepts a single value
        and returns a string.
    meta : dict-like or None
        Meta-data associated with the column
    """
    # def __init__(self, *args, **kwargs):
    #     self.name = args[0]
    #     args = args[1:]
    #     super(IdiColumn, self).__init__(*args, **kwargs)

    def __new__(cls, name, data=None,
                dtype=None, shape=(), length=0,
                description=None, unit=None, format=None, meta=None, copy=False):

        if isinstance(data, MaskedColumn) and np.any(data.mask):
            raise TypeError("Cannot convert a MaskedColumn with masked value to a Column")

        self = super(IdiColumn, cls).__new__(cls, data=data, name=name, dtype=dtype,
                                             shape=shape, length=length, description=description,
                                             unit=unit, format=format, meta=meta)
        return self


class IdiHdulist(OrderedDict):
    """OrderedDict subclass for a dictionary of Header-data units (HDU).

    This is used as a container equivalent to the FITS HDUList. This can
    be initialized with no arguments, then HDUs may be appended to it
    using regular ordered dict methods

    Parameters
    ----------
    dict_data: dict
        This class can be initialized with zero arguments, or you can pass
        a python-style dictionary.
    """

    def __getitem__(self, item):
        """Get items from a TableColumns object."""
        if isinstance(item, six.string_types):
            try:
                return OrderedDict.__getitem__(self, item)
            except KeyError:
                try:
                    return OrderedDict.__getitem__(self, item.lower())
                except KeyError:
                    return OrderedDict.__getitem__(self, item.upper())

        elif isinstance(item, int):
            return self.values()[item]
        elif isinstance(item, tuple):
            return self.__class__([self[x] for x in item])
        elif isinstance(item, slice):
            return self.__class__([self[x] for x in list(self)[item]])
        else:
            raise IndexError('Illegal key or index value for {} object'
                             .format(self.__class__.__name__))

    def __repr__(self):
        names = ("'{0}'".format(x) for x in six.iterkeys(self))
        return "<{1} names=({0})>".format(",".join(names), self.__class__.__name__)

    # Define keys and values for Python 2 and 3 source compatibility
    def keys(self):
        return list(OrderedDict.keys(self))

    def values(self):
        return list(OrderedDict.values(self))

    def add_table_hdu(self, name, header=None, data=None, history=None, comment=None):
        """
        Add a Table HDU to HDU list

        Parameters
        ----------
        name: str
            Name for table HDU
        header=None: dict
            Header keyword:value pairs dictionary. optional
        data=None: IdiTableHdu
            IdiTableHdu that contains the data
        history=None: list
            list of history data
        comment=None: list
            list of comments
        """
        self[name] = IdiTableHdu(name, header=header, data=data,
                              history=history, comment=comment)

    def add_image_hdu(self, name, header=None, data=None, history=None, comment=None):
        """
        Add a Image HDU to HDU list

        Parameters
        ----------
        name: str
            Name for table HDU
        header=None: dict
            Header keyword:value pairs dictionary. optional
        data=None: np.ndarray or equivalent
            Array that contains the image data
        history=None: list
            list of history data
        comment=None: list
            list of comments
        """
        self[name] = IdiImageHdu(name, header=header, data=data,
                              history=history, comment=comment)

    def add_primary_hdu(self, name, header=None, history=None, comment=None):
        """
        Add a Primary HDU to HDU list. This should not have a data payload.

        Parameters
        ----------
        name: str
            Name for table HDU
        header=None: dict
            Header keyword:value pairs dictionary. optional
        history=None: list
            list of history data
        comment=None: list
            list of comments
        """
        self[name] = IdiPrimaryHdu(name, header=header,
                                history=history, comment=comment)