""" Helper functions. """

# COPYRIGHT:   (c) Awasu Pty. Ltd. 2015-2020 (all rights reserved).
#              Unauthorized use of this code is prohibited.
#
# LICENSE:     This software is provided 'as-is', without any express
#              or implied warranty.
#
#              In no event will the author be held liable for any damages
#              arising from the use of this software.
#
#              Permission is granted to anyone to use this software
#              for any purpose, and to alter it and redistribute it freely,
#              subject to the following restrictions:
#
#              - The origin of this software must not be misrepresented;
#                you must not claim that you wrote the original software.
#                If you use this software, an acknowledgement is requested
#                but not required.
#
#              - Altered source versions must be plainly marked as such,
#                and must not be misrepresented as being the original software.
#                Altered source is encouraged to be submitted back to
#                the original author so it can be shared with the community.
#                Please share your changes.
#
#              - This notice may not be removed or altered from any
#                source distribution.

import sys
import re
import time
import datetime
import calendar
import xml.dom.minidom
import xml.parsers.expat
import unittest

# ---------------------------------------------------------------------

def safe_xml( val ):
    """Convert a value into something that's safe to insert into XML."""
    if val is None:
        return ""
    if type(val) is bytes:
        val = val.encode( "utf-8" )
    else:
        val = str( val )
    val = val.replace( "&", "&amp;" ).replace( "<", "&lt;" ).replace( ">", "&gt;" )
    val = val.replace( "\"", "&quot;" ) # nb: in case the value is an attribute
    return val

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def pretty_xml( val ):
    """Prettify XML."""
    try:
        # NOTE: We remove blank lines generated by toprettyxml() (because it preserves
        # whitespace between XML tags).
        buf = xml.dom.minidom.parseString( val ).toprettyxml( indent="  " )
        lines = buf.split( "\n" )
        if lines[0] == "<?xml version=\"1.0\" ?>":
            del lines[0]
        return "\n".join( [ x for x in lines if x.strip() ] )
    except xml.parsers.expat.ExpatError:
        # NOTE: If the XML is invalid, we just return it verbatim.
        return "*** WARNING: Invalid XML ***\n" + val

# ---------------------------------------------------------------------

def parse_rfc2822_timestamp( tstamp ):
    """Parse an RFC 2822 timestamp."""

    # we sometimes get timestamps without the DOW :shrug:
    if re.match( "[A-Za-z]{3}, ", tstamp ):
        tstamp = tstamp[5:]

    # parse the date
    mo = re.match( "[0-9][0-9]?", tstamp ) # nb: we sometimes get single-digit dates :-/
    if not mo:
        return None # nb: we sometimes get weird rubbish :-/
    date = int( mo.group() )
    tstamp = tstamp[mo.end()+1:]

    # parse the rest of the timestamp
    #   0----5----0----5----0----5
    #   Apr 2014 15:07:51 +0000
    # NOTE: We can't use strptime(), since it will look for month names
    # using the current locale, but we need English.
    month_names = {
        "jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6,
        "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12
    }
    month = month_names[ tstamp[:3].lower() ]
    year = int( tstamp[4:8] )
    hours = int( tstamp[9:11] )
    minutes = int( tstamp[12:14] )
    seconds = int( tstamp[15:17] )
    tstamp2 = datetime.datetime( year, month, date, hours, minutes, seconds )

    # adjust for the time zone
    if tstamp[18] in ("+","-"):
        tz_delta = 60 * int(tstamp[19:21]) + int(tstamp[21:23])
        tz_delta *= -1 if tstamp[18] == "+" else +1
        tstamp2 += datetime.timedelta( seconds=60*tz_delta )

    return calendar.timegm( tstamp2.utctimetuple() )

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def make_iso8601_timestamp( tstamp ):
    """Return an ISO 8601 timestamp (for Atom feeds)."""
    if type(tstamp) is int:
        tstamp = time.gmtime( tstamp )
    return "{year:4d}-{month:02d}-{date:02d}T{hours:02d}:{minutes:02d}:{seconds:02d}Z".format(
        year = tstamp.tm_year,
        month = tstamp.tm_mon,
        date = tstamp.tm_mday,
        hours = tstamp.tm_hour,
        minutes = tstamp.tm_min,
        seconds = tstamp.tm_sec
    )

# ---------------------------------------------------------------------

def make_atom_text_type( mime_type ):
    """Convert a MIME type to an Atom text type."""
    if mime_type == "text/plain":
        return "text"
    elif mime_type == "text/html":
        return "html"
    return "???"

# ---------------------------------------------------------------------

def dump_bytes( buf, caption="", prefix="", out=sys.stdout ):
    """Formatted byte dump."""
    if caption:
        print( caption, file=out ) # nb: no line prefix
    # dump the value as bytes
    for line_no in range( 0, int((len(buf)+15)/16) ):
        print( "{}{:04x}: ".format( prefix, 16*line_no ), end="", file=out )
        row_bytes = buf[ 16*line_no : 16*line_no+16 ]
        line_buf = " ".join( [ "{:02x}".format(ord(ch)) for ch in row_bytes ] )
        if len(line_buf) > 23: # nb: 23 = 8 hex values (2 digits each), plus 7 separators
            line_buf = "{} {}".format( line_buf[:23], line_buf[23:] )
        line_buf += " "*(48-len(line_buf))
        print( line_buf, end="", file=out )
        print(
            " | " + "".join( [ ch if 32 <= ord(ch) < 127 else "." for ch in row_bytes ] ),
            file=out
        )

# ---------------------------------------------------------------------

class UtilsTestCase( unittest.TestCase ):

    def test_safe_xml( self ):
        self.assertEqual( safe_xml('foo="<bar>"'), "foo=&quot;&lt;bar&gt;&quot;" )

    def test_rfc2822_timestamps( self ):
        self.assertEqual( parse_rfc2822_timestamp( "Tue, 01 Apr 2014 12:02:03 +0400" ),
            1396339323
        )
        self.assertEqual( parse_rfc2822_timestamp( "1 Apr 2014 12:02:03 +0400" ),
            1396339323
        )
        self.assertEqual( parse_rfc2822_timestamp( "foo" ),
            None
        )

    def test_iso8601_timestamp( self ):
        self.assertEqual(
            make_iso8601_timestamp( time.struct_time( ( 2001, 2, 3, 4, 5, 6, 0,1,0 ) ) ),
            "2001-02-03T04:05:06Z"
        )

# ---------------------------------------------------------------------

if __name__ == "__main__":
    # run the unit tests
    unittest.main()
