import sys
from numpy import integer

if sys.version_info < (3,):
    integer_types = (int, long, integer)
else:
    integer_types = (int, integer)

class ASCFITS:
    """        
    Class consolidating the knowledge of the ASC-FITS history format.
    Provides methods for:
      + verifying compatibility
      + interpreting content
      + generating compliant strings

    All methods act on a single line.
    All methods except is_compliant() assume compliance.

    This class follows the format specified by the ASC-FITS-2.0.0 standard.
    With the following exceptions:
    1) PARM: There is no enforcement of PARM content.
       The standard states "one or more keyword=value pairs", we do no 
       interpretation of the body content.
    2) STCK: Additional label commonly seen in CXC HISTORY records 
       providing file names for expanded stacks.

    For details see Appendix 3 of:
        http://cxc.harvard.edu/ciao/data_products_guide/ascfits.ps

    In summary the format is:
               1         2         3         4         5         6         7         8
      12345678901234567890123456789012345678901234567890123456789012345678901234567890
      HISTORY  llll  :bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbASCnnnnn
    
         llll = label == TOOL|PARM|CONT|STCK
         b..b = body of card
         n..n = sequence number
    
    """

    def __init__(self):
        self.__tag = "HISTORY"
        self.__cardsize = 80

        self.TOOL_TOKEN = "TOOL"
        self.PARM_TOKEN = "PARM"
        self.CONT_TOKEN = "CONT"
        self.STCK_TOKEN = "STCK"
        self.__tokens = [self.TOOL_TOKEN, self.PARM_TOKEN, self.CONT_TOKEN, self.STCK_TOKEN]

    def __is_numeric(self, s ):
        try:
            int(s)
            return True
        except ValueError:
            return False

    def __get_tag(self, s ):
        return s[0:7]

    def __get_label(self, s ):
        return s[9:13]

    def __get_body(self, s ):
        return s[16:72]

    def __get_sequence(self, s ):
        return s[75:80]

    def __increment_seqno(self, seqno ):
        seqno += 1
        if seqno > 99999:
            seqno = 1
        return seqno

    def __make_paramstack_cards(self, token, body, seqnum, with_tag=False  ):
        # Genenerates 1 or more cards from the input body
        # wrapping into CONT cards as needed.
        result = []

        while body is not None:
            # store content too big for this card
            remainder = None
            if len(body) > 56:
                remainder = body[56:]

            # write this card
            card = self.as_card( token, body, seqnum, with_tag )
            result.append( card )
                
            if remainder is not None:
                # change token for continue lines
                token = self.CONT_TOKEN
                seqnum = self.__increment_seqno( seqnum )

            body = remainder

        return (result,seqnum)


    def __split_tool_version(self, s ):
        # split body string to extract tool and version
        # "string contains tool name and version, separated by one or more blanks."
        tool = ""
        version = ""
        tstr = s.strip()
        if len(tstr) > 0:
            try:
                (tool, version) = tstr.split(None,1)
            except ValueError:
                # failed to split.. put single word on tool
                tool = tstr
                pass

        return (tool,version)



    def as_card(self, label, body, seqno, with_tag=False ):
        """
        Generate ASC-FITS compliant card from input components.

        Parameters
        ----------
          label        : string
                         card 'label' == TOOL|PARM|CONT|STCK

          body         : string
                         card 'body' (content >56 chars is truncated)

          seqno        : int
                         sequence number (1 <= seqno <= 99999)

          with_tag     : boolean, default = False
                         Flag indicating if 'HISTORY' tag should be included
                         in output        
                           True = append the tag
                           False = do not append the tag
                         

        Returns
        -------
          result       : string
                         combined string with ASC-FITS compliant structure

        Raises
        ------
          TypeError    : 'label' argument is not string type
          TypeError    : 'body' argument is not string type
          TypeError    : 'seqno' argument is not integer type
          ValueError   : 'seqno' value out of range
        """

        if isinstance(label, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'label' argument must be a string")
        if isinstance(body, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'body' argument must be a string")
        if isinstance(seqno, integer_types) is False:
            raise TypeError("'seqno' argument must be an int or long")
        if label not in self.__tokens:
            raise ValueError("'label' does not match allowed values")
        if seqno < 1 or seqno > 99999:
            raise ValueError("'seqno' out of range")

        if with_tag:
            result = self.__tag + "  " + label + "  :" + '{:56.56}'.format(body) + "ASC" + '{:05d}'.format(seqno)
        else:
            result = " " + label + "  :" + '{:56.56}'.format(body) + "ASC" + '{:05d}'.format(seqno)


        return result


    def as_cards(self, tool, version, params, seqno, with_tag=False, expand_stacks=True ):
        """
        Generate a list of ASC-FITS compliant card from input values.

        Interprets input arguments and generates the corresponding HISTORY card block.

        Parameters
        ----------
          tool         : string
                         tool name - must not be empty

          version      : string
                         tool version - may be empty

          params       : list
                         list of tuples containing [name, value, [stack]]
                         for each tool parameter. Each item provided as a string.

          seqno        : int
                         initial sequence number (1 <= seqno <= 99999)

          with_tag     : boolean, default = False
                         Flag indicating if 'HISTORY' tag should be included
                         in output        
                           True = append the tag
                           False = do not append the tag

          expand_stacks: boolean, default = True
                         flag to control generation of STCK cards
                           True  = generate STCK cards
                           False = do not generate STCK cards

        Notes
        -----
          Sequence number will wrap if it exceeds the maximum within block.
          In TOOL card
            + 2 spaces will be added between tool and version
            + Combined string has max length of 56 chars, remainder will be truncated


        Returns
        -------
          result       : string
                         combined string with ASC-FITS compliant structure
 
        Raises
        ------
          TypeError    : 'tool' argument is not string type
          TypeError    : 'version' argument is not string type
          TypeError    : 'params' argument is not list type
          TypeError    : 'seqno' argument is not integer type
          ValueError   : 'tool' value is empty
        """
        if isinstance(tool, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'tool' argument must be a string")
        if isinstance(version, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'version' argument must be a string")
        if not isinstance(params, list):
            raise TypeError("'params' argument must be a list")
        if isinstance(seqno, integer_types) is False:
            raise TypeError("'seqno' argument must be an int or long")

        if len( tool.strip() ) == 0:
            raise ValueError("'tool' value is empty.")
        # seqno range vetted in as_card()
        if params and len( params[0] ) < 3:
            raise ValueError("'params' list missing content, must have 3 components.")

        result = []

        # Check for 'special' cases.. setup accordingly
        if (tool.startswith("PIXLIB") or tool.startswith("ARDLIB")): # Special cases
            token = self.PARM_TOKEN                  # Use PARAM for head token :(
            sep   = " "                              # Single space between tool and version
        else:
            token = self.TOOL_TOKEN
            sep   = "  "

        # Generate TOOL card
        body  = tool + sep + version
        seqnum = seqno
        card = self.as_card( token, body, seqnum, with_tag )
        result.append( card )

        # Generate PARAM/STCK card(s)
        for entry in params:
            seqnum = self.__increment_seqno( seqnum )
            token  = self.PARM_TOKEN

            pname = ""
            if entry[0] is not None:
                if isinstance(entry[0], str if sys.version_info[0] >= 3 else basestring) is False:
                    raise TypeError("params-name value must be a string")
                pname = entry[0]
            body = pname

            pvalue = ""
            if entry[1] is not None:
                if isinstance(entry[1], str if sys.version_info[0] >= 3 else basestring) is False:
                    raise TypeError("params-value value must be a string")
                pvalue = entry[1]

            if pvalue != "":
                body = body + "=" + pvalue

            (pcards,seqnum) = self.__make_paramstack_cards( token, body, seqnum, with_tag )
            result.extend( pcards )

            if expand_stacks:
                # Generate any STCK cards for this param
                if (entry[2] is not None):
                    if not isinstance(entry[2], list):
                        raise TypeError("params-stack value must be a list")

                    token  = self.STCK_TOKEN
                    for body in entry[2]:
                        if isinstance(body, str if sys.version_info[0] >= 3 else basestring) is False:
                            raise TypeError("params-stack list content must be a string")

                        # generate cards and add to list.
                        seqnum = self.__increment_seqno( seqnum )
                        (scards,seqnum) = self.__make_paramstack_cards( token, body, seqnum, with_tag )
                        result.extend( scards )
        
        return result

    def is_compliant(self, card, strict=False ):
        """
        Evaluate the provided card for compliance with the ASC-FITS-2.0 standard.
        The following criteria are checked:
                                                              strict
                                                               T | F
                                                              ---+---
        o card[01:07] = 'HISTORY'                              Y | Y
        o card[08:09] = '  '                                   Y | N
        o card[10:13] = 'TOOL'|'PARM'|'CONT'|'STCK'            Y | Y
        o card[14:16] = '  :'                                  Y | N
        o card[73:75] = 'ASC'                                  Y | N
        o card[76:80] = numeric                                Y | Y
        o len(card)   = 80                                     Y | Y

        Parameters
        ----------
          card         : string
                         content to evaluate
          strict       : boolean, optional
                         flag to use strict enforcement of standard

        Returns
        -------
          result       : boolean
 
        Raises
        ------
          TypeError    : input card is not string type

        """
 
        if isinstance(card, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'card' argument must be a string")

        # Check card length
        if len(card) != self.__cardsize:
            return False

        # NOTE: Index shift from description above is intentional
        #       The description is 1s based.

        # Check critical elements
        result = (self.__get_tag(card) == self.__tag and
                  self.__get_label(card) in self.__tokens and
                  self.__is_numeric( self.__get_sequence(card) )
                  )
        if strict:
            # Check structural elements
            result = (result and 
                      card[7:9] == '  ' and
                      card[13:16] == '  :' and
                      card[72:75] == 'ASC'
                      )

        return result


    def is_head(self, card ):
        """
        Evaluate the provided card to see if it represents the head
        of an ASC-FITS compliant HISTORY block.

        Typically, this will be a card with label=="TOOL".
        However, we handle the following special cases:
          PIXLIB: History blocks start with PARAM record
          ARDLIB: History blocks start with PARAM record

        Parameters
        ----------
          card         : string
                         content to evaluate

        Returns
        -------
          result       : boolean
 
        Raises
        ------
          TypeError    : input card is not string type

        """
        if isinstance(card, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'card' argument must be a string")

        result = False

        label = self.__get_label(card)
        body  = self.__get_body(card)

        if label.upper() == self.TOOL_TOKEN:
            result = True
        elif label.upper() == self.PARM_TOKEN:
            if (body.upper().startswith("PIXLIB VERSION") or
                body.upper().startswith("ARDLIB VERSION")):
                result = True

        return result

    def body(self, card ):
        """
        Extract and return 'body' portion of the card, assumed to be compliant.

        Parameters
        ----------
          card         : string
                         content to extract from

        Returns
        -------
          result       : string
                         substring corresponding to the 'body'.
 
        Raises
        ------
          TypeError    : input card is not string type

        """

        if isinstance(card, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'card' argument must be a string")

        result = self.__get_body(card)
        return result


    def label(self, card ):
        """
        Extract and return 'label' portion of the card, assumed to be compliant.

        Parameters
        ----------
          card         : string
                         content to extract from

        Returns
        -------
          result       : string
                         substring corresponding to the 'label'.
 
        Raises
        ------
          TypeError    : input card is not string type

        """

        if isinstance(card, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'card' argument must be a string")

        result = self.__get_label(card)
        return result


    def seqno(self, card ):
        """
        Extract and return sequence number portion of the card, assumed to be compliant.

        Parameters
        ----------
          card         : string
                         content to extract from

        Returns
        -------
          result       : int
                         sequence number associated with the card
 
        Raises
        ------
          TypeError    : input card is not string type

        """

        if isinstance(card, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'card' argument must be a string")

        tstr = self.__get_sequence(card)
        result = int(tstr)
        return result


    def tool(self, card ):
        """
        Extract and return 'tool' portion of the card, assumed to be compliant.
        Will only operate on cards which represent the start of an ASC-FITS
        record block (eg: cards with label == "TOOL").


        Parameters
        ----------
          card         : string
                         content to extract from

        Returns
        -------
          result       : string, None
                         substring corresponding to the 'tool'.
                         None if the card is not a history block head card
 
        Raises
        ------
          TypeError    : input card is not string type

        """

        if isinstance(card, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'card' argument must be a string")
        
        result = None

        if self.is_head( card ):
            body = self.__get_body(card)
            (tool, version) = self.__split_tool_version( body )
            result = tool

        return result


    def version(self, card ):
        """
        Extract and return 'version' portion of the card, assumed to be compliant.
        Will only operate on cards which represent the start of an ASC-FITS
        record block (eg: cards with label == "TOOL").


        Parameters
        ----------
          card         : string
                         content to extract from

        Returns
        -------
          result       : string
                         substring corresponding to the 'version'.
 
        Raises
        ------
          TypeError    : input card is not string type

        """

        if isinstance(card, str if sys.version_info[0] >= 3 else basestring) is False:
            raise TypeError("'card' argument must be a string")
        
        result = None

        if self.is_head( card ):
            body = self.__get_body(card)
            (tool, version) = self.__split_tool_version( body )
            result = version

        return result
