#!/usr/bin/python3
import re
import pytz
import calendar
import operator
import argparse
import sys

class InvalidCronEntryError(Exception):
    pass

class SqueezeFieldObject():
    def __init__(self,field,minimum,maximum):
        self.sqzField = field
        self.min = minimum
        self.max = maximum

class CronEntry():
    """ this is just a placeholder. so that related values to an entry 
    can be stored in a single var. enough of dict style ref."""
    def __init__(self,entry=None,serverTz=None,jobTz=None):
        self.entry = entry
        self.serverTz = serverTz
        self.jobTz =  jobTz
        self.ts    = None
        self.adjustedTs= None
        self.domHit = False
        self.dowHit = False

    def __str__(self):
        return "domHit {self.domHit} dowHit {self.dowHit} ts {self.ts} adjustedTs {self.adjustedTs}".format(self=self)


DEFAULT_VALUES = {
    'minute' : '',
    'hour' : '',
    'dom' : '',
    'month' : '',
    'dow' : '',
    'year' : '',
}

VALID_SPECIAL_STRINGS = [
    '@reboot',
    '@yearly',
    '@annually',
    '@monthly',
    '@weekly',
    '@daily',
    '@midnight',
    '@hourly',
]


REGEX_PATTERNS = {
    'server_tz' : re.compile('^#\s*SERVER_TZ='),
    'job_tz' : re.compile('^#\s*JOB_TZ='),
    'comment' : re.compile('^\s*#'),
    'blank_line' : re.compile('^\s*$'),
    'variable' : re.compile('^\s*\w+='),
    'parse_entry' : re.compile('\s'),
    'astreisk' : re.compile('^\s*\*\s*$'),
    'is_num_only' : re.compile('^\s*\d+\s*$'),
    'number' : re.compile('\d'),
    'range' : re.compile('-'),
    'list' : re.compile(','),
    'step' : re.compile('/'),
    'week_day_abbr' : re.compile('mon|tue|wed|thu|fri|sat|sun',re.I),
    'month_abbr' : re.compile('jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec',re.I)
}

ENTRY_ORDER = ['minute', 'hour', 'dom', 'month', 'dow', 'command']
ENTRY_TIME_FIELDS = [ 'month', 'dom', 'dow', 'hour', 'minute' ]
ENTRY_SORT_ORDER = [ 'month', 'dom', 'hour', 'minute', 'dow'] ## sort in this order
SQUEEZE_ORDER = [ 'minute', 'hour', 'dom', 'month', 'dow'] ## squeeze in this order
SQUEEZE_FILED_OBJS = {}

WEEK_DAY_SHORT_NAMES = []
MONTH_SHORT_NAMES = []

def SetSqueezeFieldObjects():
    global SQUEEZE_FILED_OBJS

    SQUEEZE_FILED_OBJS['dom'] =  SqueezeFieldObject('dom',1,31)
    SQUEEZE_FILED_OBJS['dow'] =  SqueezeFieldObject('dow',1,7)
    SQUEEZE_FILED_OBJS['month'] =  SqueezeFieldObject('month',1,12)
    SQUEEZE_FILED_OBJS['minute'] =  SqueezeFieldObject('minute',0,59)
    SQUEEZE_FILED_OBJS['hour'] =  SqueezeFieldObject('hour',0,59)


def SetWeekDayShortNames():
    global WEEK_DAY_SHORT_NAMES
    WEEK_DAY_SHORT_NAMES = []
    for i in range(7):
        WEEK_DAY_SHORT_NAMES.append(calendar.day_abbr[i].lower())

def SetMonthShortNames():
    global MONTH_SHORT_NAMES
    MONTH_SHORT_NAMES = []
    for i in range(1,12+1):
        MONTH_SHORT_NAMES.append(calendar.month_abbr[i].lower())

def GetMonthNoForShortName(inp):
    try:
        return MONTH_SHORT_NAMES.index(inp.lower())+1
    except ValueError:
        return -1

def GetWeekDayNoForShortName(inp):
    try:
        return WEEK_DAY_SHORT_NAMES.index(inp.lower())+1
    except ValueError:
        return -1

def SetDefaultValuesDomDow(entry):
    """modify global defaults based on entrie's dom/dow"""
    global DEFAULT_VALUES
    if REGEX_PATTERNS['astreisk'].match(entry['dom']) and REGEX_PATTERNS['astreisk'].match(entry['dow']):
        """lets use global defaults"""
        pass
    elif REGEX_PATTERNS['number'].search(entry['dom']) and REGEX_PATTERNS['number'].search(entry['dow']):
        """here its just default, actual values will be generated by routines further down"""
        pass
    elif REGEX_PATTERNS['number'].search(entry['dow']):
        """since dow will be set further down, we can clear dom"""
        DEFAULT_VALUES['dom'] = []
    elif REGEX_PATTERNS['number'].search(entry['dom']):
        """since dom will be set further down, we can clear dow"""
        DEFAULT_VALUES['dow'] = []
    else:
        pass

def SetDefaultValues():
    global DEFAULT_VALUES

    td = pytz.datetime.datetime.now()
    DEFAULT_VALUES['year'] = [td.year]
    DEFAULT_VALUES['minute'] = [x for x in range(0,59+1)]
    DEFAULT_VALUES['hour'] = [x for x in range(0,23+1)]
    DEFAULT_VALUES['month'] = [td.month] # for month it is ok.
    DEFAULT_VALUES['dom'] = [] # since its either dom/dow, we take default dow. 
    DEFAULT_VALUES['dow'] = [x for x in range(1,7+1)]

    return

def IsValidCronEntry(line):
    return True

def ParseCronEntry(line):
    """ break a cron entry into dict"""
    values = []

    entries = line.split()
    for entry in entries:
        if REGEX_PATTERNS['blank_line'].match(entry):
            continue
        elif len(values) < 5:
            values.append(entry)
        else:
            break

    values.append(' '.join(entries[5:]))

    return dict(zip(ENTRY_ORDER,values))

def ExpandRange(r,s=1):
    """expand 1-5 to [1..5], step by 1-10/2"""
    if REGEX_PATTERNS['step'].search(r):
        [r1,s] = r.split('/')
        s=int(s)
    else:
        r1 = r

    (start,end) = r1.split('-')
    return [i for i in range(int(start),int(end)+1,s)]

def NormalizeEntry(inp,stepStartVal=1):
    """ break range, 1-5, and list 1,2,3 into individual values"""
    expanded = []
    if REGEX_PATTERNS['list'].search(inp):
        for entry in inp.split(','):
            if REGEX_PATTERNS['range'].search(entry):
                expanded.extend(ExpandRange(entry))
            else:
                expanded.append(entry)
    elif REGEX_PATTERNS['range'].search(inp):
        expanded.extend(ExpandRange(inp))
    elif REGEX_PATTERNS['step'].search(inp):
        inp = str(stepStartVal) + '-' + inp
        expanded.extend(ExpandRange(inp))
    else:
        expanded = [ inp ] # expanded.append(inp) too will do

    retVal = []
    for x in expanded:
        try:
            retVal.append(int(x))
        except ValueError:
            pass

    return retVal

def ExpandMonths(inp):
    if REGEX_PATTERNS['astreisk'].match(inp):
        return DEFAULT_VALUES['month']
    elif REGEX_PATTERNS['month_abbr'].match(inp):
        if REGEX_PATTERNS['list'].search(inp):
            monthNames = inp.split(',')
            months = []
            for m in monthNames:
                months.append(str(GetMonthNoForShortName(m)))

            inp = ','.join(months)
        else:
            inp = str(GetMonthNoForShortName(inp))

    return NormalizeEntry(inp)

def ExpandDoM(inp):
    if REGEX_PATTERNS['astreisk'].match(inp):
        return DEFAULT_VALUES['dom']
    else:
        return NormalizeEntry(inp)

def ExpandDoW(inp):
    if REGEX_PATTERNS['astreisk'].match(inp):
        return DEFAULT_VALUES['dow']
    elif REGEX_PATTERNS['week_day_abbr'].match(inp):
        if REGEX_PATTERNS['list'].search(inp):
            wdNames = inp.split(',')
            wd = []
            for m in wdNames:
                wd.append(str(GetWeekDayNoForShortName(m)))

            inp = ','.join(wd)
        else:
            inp = str(GetWeekDayNoForShortName(inp))

    return NormalizeEntry(inp)

def ExpandHour(inp):
    if REGEX_PATTERNS['astreisk'].match(inp):
        return DEFAULT_VALUES['hour']
    else:
        return NormalizeEntry(inp,stepStartVal=0)

def ExpandMinutes(inp):
    if REGEX_PATTERNS['astreisk'].match(inp):
        return DEFAULT_VALUES['minute']
    else:
        return NormalizeEntry(inp,stepStartVal=0)

def GetEntryAsTimeStamps(record,tz):
    """ given a dict, rep a cron entry.
    convert in into datetime() - which can be used for tz adjustment
    this can for some instances generate 60*60*24*31/7 entries."""
    expandedTs = []

    year = DEFAULT_VALUES['year'][0]
    expandedMonth = ExpandMonths(record['month'])
    expandedDoW = ExpandDoW(record['dow'])

    expandedDoM = ExpandDoM(record['dom'])
    expandedHours = ExpandHour(record['hour'])
    expandedMins = ExpandMinutes(record['minute'])

    for month in expandedMonth:
        """loop through all the days for specified months.
        if dom/dow is set, then add that datetime()"""

        """
        when both dow & dom are populated.
        Its only a "bizarre behavior" -> Paul Vixie says in cron.c code file:
        /* the dom/dow situation is odd.  '* * 1,15 * Sun' will run on the
        * first and fifteenth AND every Sunday;  '* * * * Sun' will run *only*
        * on Sundays;  '* * 1,15 * *' will run *only* the 1st and 15th.  this
        * is why we keep 'e->dow_star' and 'e->dom_star'.  yes, it's bizarre.
        * like many bizarre things, it's the standard.
        */
        """
        for d in calendar.Calendar(firstweekday=1).itermonthdates(year,month):
            if d.month != month: #itermonthdates() returns complete weeks at beg,end
                continue 

            domHit = dowHit = False
            try:
                domHit = True if expandedDoM.index(d.day) >= 0 else False
            except ValueError:
                pass

            try:
                dowHit = True if expandedDoW.index(d.isoweekday()) >= 0 else False
            except ValueError:
                pass

            if domHit or dowHit:
                for hr in expandedHours:
                    for mins in expandedMins:
                        ts = pytz.datetime.datetime(d.year,d.month,d.day,hr,mins)
                        cronEntryObj = CronEntry(record,serverTz=tz)
                        cronEntryObj.ts = ts
                        cronEntryObj.dowHit = dowHit
                        cronEntryObj.domHit = domHit
                        expandedTs.append(cronEntryObj)

    return expandedTs

def AdjustForTz(record,serverTz,jobTz):
    """given a cron record, adjust for given tz"""
    expEntryObjs = GetEntryAsTimeStamps(record,serverTz)

    adjustedEntries = []
    utcTzObj = pytz.utc
    serverTzObj = pytz.timezone(serverTz)
    jobTzObj = pytz.timezone(jobTz)

    for entryObj in expEntryObjs:
        """to account for dst, always convert tz from utc
        http://pytz.sourceforge.net/
        """
        jobTs = jobTzObj.localize(entryObj.ts)
        utcTs = jobTs.astimezone(utcTzObj)
        entryObj.adjustedTs = utcTs.astimezone(serverTzObj)
        adjustedEntries.append(ReplaceEntryWithServerTs(entryObj))

    return adjustedEntries

def IsEntryNumberAlone(inp):
    pass

def ReplaceEntryWithServerTs(entryObj):
    """ entryObj. contains both actual cron entry and tz adjusted datetime()
    this function generates a tz adjusted cron entry.
    it applies lot of logic on how the out adjusted cron entry should be.
    it can get ugly for dom/dow especially"""
    retVal = {}

    entry = entryObj.entry
    serverTs = entryObj.adjustedTs

    retVal['command'] = entry['command']
    """min can't be copied to output as it is. ind-uk tz diff
    is 5.30hrs. so * might move from 30-59 of hour x & 0-29 hour x+1"""
    retVal['minute'] = str(serverTs.minute)

    if REGEX_PATTERNS['astreisk'].match(entry['hour']):
        retVal['hour'] = '*'
    else:
        retVal['hour'] = str(serverTs.hour)

    if REGEX_PATTERNS['astreisk'].match(entry['month']):
        retVal['month'] = '*'
    else:
        retVal['month'] = str(serverTs.month)

    if entryObj.domHit and entryObj.dowHit:
        """both dom & dow were configured in cron entry(dow can be from default too)"""
        retVal['dom'] = str(serverTs.day)
        if REGEX_PATTERNS['astreisk'].match(entry['dow']):
            retVal['dow'] = entry['dow']
        else:
            retVal['dow'] = str(serverTs.isoweekday())
    elif entryObj.domHit:
        retVal['dom'] = str(serverTs.day)
        #retVal['dow'] = '*'
        if REGEX_PATTERNS['astreisk'].match(entry['dow']):
            retVal['dow'] = entry['dow']
        else:
            retVal['dow'] = str(serverTs.isoweekday())
    elif entryObj.dowHit:
        if REGEX_PATTERNS['astreisk'].match(entry['month']):
            retVal['dom'] = entry['dom']
        else:
            """ for tz shift in mins. wrapping for next month
            * 20 3 30,31 1-7 for London->India 31st becomes first of next month
            30-59 1 3 31 1-7 #30th
            00-29 2 3 31 1-7 #30th
            30-59 0 4 1 1-7 #31st
            00-29 1 4 1 1-7 #31st
            """

            #retVal['dom'] = str(serverTs.day)
            expandedMonth = ExpandMonths(entry['month'])
            if serverTs.month in expandedMonth:
                retVal['dom'] = str(serverTs.day)
            else:
                tzAdjustToNextMonth = False
                for m in expandedMonth:
                    if int(serverTs.month) == (m+1):
                        tzAdjustToNextMonth = True
                        break
                    else:
                        pass
                if tzAdjustToNextMonth:
                    retVal['dom'] = str(serverTs.day)
                else:
                    retVal['dom'] = entry['dom']

        if REGEX_PATTERNS['astreisk'].match(entry['dow']):
            retVal['dow'] = entry['dow']
        else:
            retVal['dow'] = str(serverTs.isoweekday())
    else:
        retVal['dom'] = '*'
        retVal['dow'] = '*'

    return retVal

def GetLineAsRecord(line):
    record = {}
    if IsValidCronEntry(line):
        return ParseCronEntry(line)
    else:
        raise InvalidCronEntryError(line)

def PrintLine(line,fileObj=sys.stdout,end="",flush=True):
    print(line,file=fileObj,end=end,flush=flush)

def PrintEntry(job,fileObj):
    for k in ENTRY_ORDER:
        PrintLine(job[k],fileObj=fileObj,end=' ',flush=False)

    PrintLine('',fileObj=fileObj,end="\n")

def GenerateSortKey(entry):
    """generate custom sort key based on ENTRY_SORT_ORDER"""
    key = ''

    for x in ENTRY_SORT_ORDER:
        if entry[x] == '*':
            key += '01'
        elif REGEX_PATTERNS['range'].search(entry[x]):
            key += '99'
        elif REGEX_PATTERNS['list'].search(entry[x]):
            key += '99'
        else:
            key += "{:02d}".format(int(entry[x]))

    return int(key)

def ConvertAsRangeIfPossible(obj,val):
    """Used by SqueezeOnField* routines. given a list 1,2,3 convert to 1-3"""
    if not REGEX_PATTERNS['list'].search(val):
        return val

    #print(obj.sqzField,val)
    vals = [int(v) for v in val.split(',')]
    v1 = vals[0]
    stepVal = vals[1]-vals[0]
    iterCount = 1
    small = large = v1
    for v in vals[1:]:
        iterCount+=1
        if v1+stepVal != v:
            break
        else:
            v1 = v
        if small > v:
            small = v
        if large < v:
            large = v

    if obj.sqzField == 'dow':
        v1 = vals[0]
        if AreDoWValuesInSeq(obj,vals[0],vals[1]):
            stepVal=1
        else:
            stepVal = vals[1]-vals[0]
        iterCount=1
        small = large = v1
        for v in vals[1:]:
            iterCount+=1
            if AreDoWValuesInSeq(obj,v1,v):
                v1 = v
            else:
                break
            if small > v:
                small = v
            if large < v:
                large = v

    if len(vals) == iterCount:
        if stepVal == 1 or len(vals)==2:
            return "{0}-{1}".format(small,large)
        else:
            return "{0}-{1}/{2}".format(small,large,stepVal)
    else:
        return val


def EntryFieldsSame(e1,e2,fields):
    v1=tuple(e1[k] for k in fields)
    v2=tuple(e2[k] for k in fields)

    return v1==v2

def AreDoMValuesInSeq(details,curVal,nextVal):
    if curVal+1 == nextVal:
        return True
    elif curVal == 30 and nextVal == details.min:
        return True
    elif curVal == 31 and nextVal == details.min:
        return True
    elif curVal == 28  and nextVal == details.min:
        return True
    elif curVal == 29  and nextVal == details.min:
        return True
    else:
        return False

def AreDoWValuesInSeq(details,curVal,nextVal):
    if curVal+1 == nextVal:
        return True
    elif curVal == 7 and nextVal == details.min:
        return True
    elif curVal == 0 and nextVal == details.min:
        return True
    else:
        return False

def AreMonthValuesInSeq(details,curVal,nextVal):
    if curVal+1 == nextVal:
        return True
    elif curVal == 12 and nextVal == details.min:
        return True
    else:
        return False

def AreHourValuesInSeq(details,curVal,nextVal):
    if curVal+1 == nextVal:
        return True
    elif curVal == 59 and nextVal == details.min:
        return True
    else:
        return False

def AreValuesInSeq(details,cur,nxt):
    f = details.sqzField
    curVal = int(cur[f])
    nextVal = int(nxt[f])

    if f == 'dom':
        return AreDoMValuesInSeq(details,curVal,nextVal)
    elif f == 'dow':
        return AreDoWValuesInSeq(details,curVal,nextVal)
    elif f == 'month':
        return AreMonthValuesInSeq(details,curVal,nextVal)
    elif f == 'minute' or f == 'hour':
        return AreHourValuesInSeq(details,curVal,nextVal)
    else:
        return False


def SqueezeOnFieldForTzShiftWithMins(entries,sqzFieldObjs):
    """
    call this only after calling SqueezeOnField(),  GetUniqueEntries(), SqueezeOnField() 
    similar to SqueezeOnField(), but squeezes entries where tz shift is in mins too.

    30-59 1 2 3 6 
    00-29 2 2 3 6 
    30-59 1 3 3 7 
    00-29 2 3 3 7 

    sould become 
    30-59 1 2-3 3  6-7
    00-29 2 2-3 3  6-7

    this routine does validtion to check the first 4 entries resemeble as above.
    if it does then proceeds to squeeze
    """
    if len(entries)<4:
        return entries

    (cur1,cur2,next1,next2) = entries[0:4]
    sqzFields = [ f.sqzField for f in sqzFieldObjs]

    for sqzField in sqzFields:
        if sqzField not in ENTRY_TIME_FIELDS:
            print("Given sqzField {0} not in ENTRY_TIME_FIELDS".format(sqzField),file=sys.stderr)
            return entries

    for entry in entries[0:4]:
        if REGEX_PATTERNS['range'].search(entry['minute']) or \
            REGEX_PATTERNS['list'].search(entry['minute']) or \
                REGEX_PATTERNS['number'].search(entry['minute']):
                pass
        else:
            return entries

    try:
        if int(cur1['hour'])+1 == int(cur2['hour']) and \
                int(next1['hour'])+1 == int(next2['hour']):
            pass
        else:
            return entries
    except ValueError:
        """hour is list,range or astreisk"""
        return entries

    """only sqzField should inc, other fields should be same - so can squeezed"""
    otherThanSqzField = set(ENTRY_TIME_FIELDS).difference(('hour',*sqzFields))
    canSqueeze = True

    for sqzFieldObj in sqzFieldObjs:
        sqzField = sqzFieldObj.sqzField
        try:
            if AreValuesInSeq(sqzFieldObj,cur1,next1) and \
                    AreValuesInSeq(sqzFieldObj,cur2,next2) and \
                    EntryFieldsSame(cur1,next1,otherThanSqzField) and \
                    EntryFieldsSame(cur2,next2,otherThanSqzField):
                pass
            else:
                canSqueeze = False
                break
        except ValueError:
            """sqzField is list,range or astreisk"""
            canSqueeze = False

    if canSqueeze == False:
        return entries

    squeezedEntries = []
    squeeze1 = cur1.copy()
    squeeze2 = cur2.copy()
    lastEntryAccounted = False
    totaEntries=len(entries)
    curIdx = 2
    while curIdx+2 <= totaEntries:
        lastEntryAccounted = False
        canSqueeze = True

        next1= entries[curIdx]
        next2= entries[curIdx+1]

        try:
            if int(cur1['hour'])+1 == int(cur2['hour']) and \
                    int(next1['hour'])+1 == int(next2['hour']):
                pass
            else:
                canSqueeze = False
        except ValueError:
            """hour is list,range or astreisk"""
            canSqueeze = False

        if canSqueeze:
            for sqzField in sqzFields:
                if REGEX_PATTERNS['is_num_only'].match(cur1[sqzField]) and \
                    REGEX_PATTERNS['is_num_only'].match(cur2[sqzField]) and \
                    REGEX_PATTERNS['is_num_only'].match(next1[sqzField]) and \
                    REGEX_PATTERNS['is_num_only'].match(next2[sqzField]):
                    pass
                else:
                    canSqueeze = False
                    break

        if canSqueeze:
            for sqzFieldObj in sqzFieldObjs:
                sqzField = sqzFieldObj.sqzField
                try:
                    if AreValuesInSeq(sqzFieldObj,cur1,next1) and \
                            AreValuesInSeq(sqzFieldObj,cur2,next2) and \
                            EntryFieldsSame(cur1,next1,otherThanSqzField) and \
                            EntryFieldsSame(cur2,next2,otherThanSqzField):
                        pass
                    else:
                        canSqueeze = False
                        break
                except ValueError:
                    """sqzField is list,range or astreisk"""
                    canSqueeze = False
                    break

        if canSqueeze == True:
            for sqzField in sqzFields:
                squeeze1[sqzField] += ',' + next1[sqzField]
                squeeze2[sqzField] += ',' + next2[sqzField]
            cur1 = next1
            cur2 = next2
            lastEntryAccounted = True
            curIdx+=2
            continue
        else:
            AppendToSqueezeList(squeezedEntries,sqzFieldObjs,squeeze1,squeeze2)
            cur1 = next1
            cur2 = next2
            squeeze1 = cur1.copy()
            squeeze2 = cur2.copy()
            curIdx+=2

    if lastEntryAccounted:
        AppendToSqueezeList(squeezedEntries,sqzFieldObjs,squeeze1,squeeze2)
    else:
        squeezedEntries.append(next1)
        squeezedEntries.append(next2)

    return squeezedEntries

def AppendToSqueezeList(li,sfObjs,s1,s2=None):
    for s in (s1,s2):
        if s is None:
            continue
        for sfObj in sfObjs:
            s[sfObj.sqzField] = ConvertAsRangeIfPossible(sfObj,s[sfObj.sqzField])

        li.append(s)

def SqueezeOnField(entries,sqzFieldObj):
    """entries are already sorted. 
    for n in entries:
        if n.sqzField = n+1.sqzField
         squeeze
        else
         can't squeeze, append this entry and continue
    """
    if len(entries)<3:
        return entries

    sqzField = sqzFieldObj.sqzField
    if sqzField not in ENTRY_TIME_FIELDS:
        print("Given sqzField {0} not in ENTRY_TIME_FIELDS".format(sqzField),file=sys.stderr)
        return entries

    squeezedEntries = []
    curSeqEntries = []
    prev = entries[0]
    curSeqEntries.append(prev)

    if REGEX_PATTERNS['is_num_only'].match(prev[sqzField]):
        pass
    else:
        return entries

    otherFields = ENTRY_TIME_FIELDS.copy()
    otherFields.remove(sqzField)

    stepVal = int(entries[1][sqzField]) - int(entries[0][sqzField])
    totalEntries=len(entries)
    totalEntriesLessOne = totalEntries-1
    curIdx=1
    curSeqCount=0
    lastEntryAccounted = False
    while curIdx < totalEntries:
        cur = entries[curIdx]
        lastEntryAccounted = False
        canSqueeze = True

        if REGEX_PATTERNS['is_num_only'].match(prev[sqzField]) and \
                REGEX_PATTERNS['is_num_only'].match(cur[sqzField]):
            pass
        else:
            canSqueeze = False

        if canSqueeze == True:
            if int(prev[sqzField])+stepVal == int(cur[sqzField]) and \
                    EntryFieldsSame(prev,cur,otherFields):
                pass
            else:
                canSqueeze = False

        if canSqueeze:
            curSeqCount+=1
            curSeqEntries.append(cur)
            lastEntryAccounted = True
        else:
            if curSeqCount > 2:
                """seq is over, and we have gatherered enough seq to sqz
                cur contains that isn't part of prev seq"""
                sqzEntry = curSeqEntries[0]
                for ent in curSeqEntries[1:]:
                    sqzEntry[sqzField] += ',' + ent[sqzField]
                AppendToSqueezeList(squeezedEntries,[sqzFieldObj],sqzEntry)
            elif curSeqCount == 0:
                """basic validation for sqz failed, sqzField isn't num. maybe list/range"""
                AppendToSqueezeList(squeezedEntries,[sqzFieldObj],curSeqEntries[0])
            else:
                """we have two entries in curSeqEntries.
                two will always form a seq if other fields are same. 
                so append curSeqEntries[0] - this is def not part of any seq, and curSeqEntries[0]=[1]
                we still not sure wether [0] and [1] are seq, so curIdx--"""
                curIdx-=1
                AppendToSqueezeList(squeezedEntries,[sqzFieldObj],curSeqEntries[0])
                #print(prev,cur,curIdx,curSeqEntries)
                cur = curSeqEntries[1]

            curSeqCount=0
            curSeqEntries.clear()
            curSeqEntries.append(cur)
            try:
                if curIdx < totalEntriesLessOne:
                    stepVal = int(entries[curIdx+1][sqzField]) - int(entries[curIdx][sqzField])
            except ValueError:
                """might fail in REGEX_PATTERNS['is_num_only']"""
                pass

        prev = cur
        curIdx+=1

    if lastEntryAccounted:
        if curSeqCount > 2:
            sqzEntry = curSeqEntries[0]
            for ent in curSeqEntries[1:]:
                sqzEntry[sqzField] += ',' + ent[sqzField]
            AppendToSqueezeList(squeezedEntries,[sqzFieldObj],sqzEntry)
        else:
            for ent in curSeqEntries:
                AppendToSqueezeList(squeezedEntries,[sqzFieldObj],ent)
    else:
        squeezedEntries.append(cur)

    return squeezedEntries
                
def GetUniqueEntries(entries):
    uniqueEntries = []
    for idx,rec in enumerate(entries):
        if rec not in entries[idx+1:]:
            uniqueEntries.append(rec)

    return uniqueEntries

def PrintEntriesForDebug(entries,msg=''):
    for x in entries:
        print("{msg}<>{month}.{dom}.{hour}.{minute}.{dow}".format(msg=msg,**x))

def Main(inFile,outFile=None):
    if outFile == None:
        outHand=sys.stdout
    else:
        outHand=open(outFile,'w')

    SetWeekDayShortNames()
    SetMonthShortNames()
    SetSqueezeFieldObjects()
    with open(inFile) as cronFileHandle:
        serverTz = ''
        jobTz = ''
        isJobTzSet = False
        for line in cronFileHandle:
            SetDefaultValues()
            if REGEX_PATTERNS['job_tz'].match(line):
                jobTz = line.split('=')[1].strip()
                isJobTzSet = True
    
            if REGEX_PATTERNS['server_tz'].match(line):
                serverTz = line.split('=')[1].strip()
                PrintLine(line,fileObj=outHand)
            elif REGEX_PATTERNS['comment'].match(line):
                PrintLine(line,fileObj=outHand)
            elif REGEX_PATTERNS['blank_line'].match(line):
                PrintLine(line,fileObj=outHand)
            elif REGEX_PATTERNS['variable'].match(line):
                PrintLine(line,fileObj=outHand)
            else:
                entryAsRecord = GetLineAsRecord(line)
                SetDefaultValuesDomDow(entryAsRecord)
                PrintLine(line,fileObj=outHand)

                if isJobTzSet:
                    #tzAdjustedEntryUnique = list(map(dict, frozenset(frozenset(tuple(e.items()) for e in tzAdjustedEntry))))
                    adjEntries = AdjustForTz(entryAsRecord,serverTz,jobTz)

                    #PrintEntriesForDebug(adjEntries,"AdjustedEntry")
                    for x in ([1,2]):
                        for k in SQUEEZE_ORDER:
                            adjEntries.sort(key=GenerateSortKey)
                            adjEntriesUnq = GetUniqueEntries(adjEntries)
                            adjEntriesSqz = SqueezeOnField(adjEntriesUnq,SQUEEZE_FILED_OBJS[k])
                            adjEntries    = adjEntriesSqz


                    ## lets try squeezedEntriesUnique for tz shift with mins, like india-england
                    adjEntries.sort(key=GenerateSortKey)
                    adjEntriesUnq = GetUniqueEntries(adjEntries)

                    sqzFieldObjs = [SQUEEZE_FILED_OBJS['dom'],SQUEEZE_FILED_OBJS['dow']]
                    adjEntries = SqueezeOnFieldForTzShiftWithMins(adjEntriesUnq,sqzFieldObjs)

                    for entry in adjEntries:
                        PrintEntry(entry,fileObj=outHand)

                    isJobTzSet = False
                else:
                    PrintEntry(entryAsRecord,fileObj=outHand)


if __name__ == '__main__':
    argParser = argparse.ArgumentParser()
    argParser.add_argument('-i','--infile',type=str,required=True)
    argParser.add_argument('-o','--outfile',type=str,required=False)

    parsedArgs = vars(argParser.parse_args())
    Main(parsedArgs['infile'],outFile=parsedArgs['outfile'])
