#!/usr/bin/env python

# @HEADER
# ************************************************************************
#
#            TriBITS: Tribal Build, Integrate, and Test System
#                    Copyright 2013 Sandia Corporation
#
# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
# the U.S. Government retains certain rights in this software.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the Corporation nor the names of the
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY SANDIA CORPORATION "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SANDIA CORPORATION OR THE
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# ************************************************************************
# @HEADER

import datetime

#
# Help message
#


usageHelp = r"""cdash_testing_date.py --cdash-project-start-time="hh:mm" [other options]

Returns to STDOUT the date string YYYY-MM-DD that corresponds the the matching
CDash test day.  Is should be compatible with the 'date=YYYY-MM-DD' argument
to many CDash PHP pages for a build that has a starting time stamp that
matches the requested date.

This tool is meant to match the CDash logic for what testing day a given CDash
build matches for the CDash 'date' field in various PHP pages for a given
CDash build with a give build start time (as reported to CDash from CTest).

For example, if the CDash project start time is 04:00 UTC and the CDash
build start time is 2018-01-22T00:00 UTC, then the relative CDash build date
time would be 2018-01-21T20:00 UTC which is the previous testing day
2018-01-21.  But if the CDash build start time was 2018-01-22T04:10 UTC,
then the relative CDash build date time would be 2018-01-22:00:10 UTC and
the testing day would be 2018-01-22.
"""

#
# Helper functions
#


def injectCmndLineOptionsInParser(clp, gitoliteRootDefault=""):
  
  clp.add_option(
    "--cdash-project-start-time", dest="cdashProjectStartTimeUtcStr", type="string", default="",
    help="Starting time for the CDash testing day in 'hh:mm' in UTC."\
      + " Check the CDash project settings for the testing day start time." )
  
  clp.add_option(
    "--day-incr", dest="dayIncrInt", type="int", default="0",
    help="Increment for the testing date (default '0') [optional]" )
  
  clp.add_option(
    "--cdash-build-start-time", dest="cdashBuildStartTime", type="string", default="",
    help="CDash build start time in format 'YYYY-MM-DDThh:mm UTC'.  If empty ''"\
      +" then the current date/time in UTC is used.  (default '') [optional]" )
  
  clp.add_option(
    "--debug-level", dest="debugLevel", type="int", default="0",
    help="Debug level.  An integer >= 0 (default '0')" )


def getCmndLineOptions():
  from optparse import OptionParser
  clp = OptionParser(usage=usageHelp)
  injectCmndLineOptionsInParser(clp)
  (options, args) = clp.parse_args()
  if options.cdashProjectStartTimeUtcStr == "":
    raise Exception("Error, input argument --cdash-project-start-time must be non"\
      +"-empty and must be of the format hh:mm UTC")
  return options


# Return the current time in UTC as a datetime object
def getCurrentDateTimeUtc():
  return datetime.datetime.utcnow()


# Get the timezone offset as a timedelta object w.r.t to UTC
#
# The supported timezones for timeZoneStr are the strings:
#
# * UTC: 0
# * EDT: 4
# * EST: 5
# * CDT: 5
# * CST: 6
# * MDT: 6
# * MST: 7
#
# NOTE: Any timezone that CDash returns for the 'buildstarttime' field must be
# added below.
#
def getTimeZoneOffset(timeZoneStr):
  if timeZoneStr == "UTC": timezoneOffsetInt = 0
  elif timeZoneStr == "EDT": timezoneOffsetInt = 4
  elif timeZoneStr == "EST": timezoneOffsetInt = 5
  elif timeZoneStr == "CDT": timezoneOffsetInt = 5
  elif timeZoneStr == "CST": timezoneOffsetInt = 6
  elif timeZoneStr == "MDT": timezoneOffsetInt = 6
  elif timeZoneStr == "MST": timezoneOffsetInt = 7
  else: raise Exception("Error, unrecognized timezone '"+timeZoneStr+"'!")
  return datetime.timedelta(hours=timezoneOffsetInt)


# Return a timezone aware datetime object given an input date and time given
# in the format "<YYYY>-<MM>-<DD>T<hh>:<mm>:<ss> <TZ>".
#
# Note. the timzone <TZ> can be any of those supported by the function
# getTimeZoneOffset()
def getBuildStartTimeUtcFromStr(buildStartTimeStr):
  buildStartTimeStrArray = buildStartTimeStr.split(" ")
  if len(buildStartTimeStrArray) == 2:
    timezoneOffset = getTimeZoneOffset(buildStartTimeStrArray[1])
  else:
    timezoneOffset = 0
  localDateTime = datetime.datetime.strptime(buildStartTimeStrArray[0], "%Y-%m-%dT%H:%M:%S")
  return localDateTime + timezoneOffset 


# Return a timedelta object for the CDash Project start time passed in as a
# string in the format "<hh>:<mm>" in UTC.
def getProjectTestingDayStartTimeDeltaFromStr(cdashProjectStartTimeUtcStr):
  t = datetime.datetime.strptime(cdashProjectStartTimeUtcStr, '%H:%M')
  return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)


# Return a timedelta object for a day increment pass in as an signed integer.
def getDayIncrTimeDeltaFromInt(dayIncrInt):
  return datetime.timedelta(days=dayIncrInt)


# Return the string "YYYY-MM-DDThh:mm:ss UTC" corresponding to the input
# datetime object.
def getBuildStartTimeUtcStrFromUtcDT(datetimeUtcDT, removeSpace=False):
  dateTimeStr = datetimeUtcDT.strftime("%Y-%m-%dT%H:%M:%S UTC")
  if removeSpace:
    return dateTimeStr.replace(" ", "")
  return dateTimeStr


# Return the string "<YYYY>-<MM>-<DD>" for an input datetime object.
def getDateStrFromDateTime(dateTime):
  return dateTime.strftime("%Y-%m-%d")


# Return the datetime object for just <YYYY>-<MM>-<DD> for an input datetime
# object.
def getDateOnlyFromDateTime(dateTime):
  return datetime.datetime(year=dateTime.year, month=dateTime.month, day=dateTime.day)


# Compute the CDash testing day for a given build using its 'buildstarttime'
# field and the testing day start time and return it as a datetime object.
#
# buildStartTimeStr [in]: The 'buildstarttime' field string as returned from
# CDash in the format "<YYYY>-<MM>-<DD>T<hh>:<mm>:<ss> <TZ>".  Note. the
# timzone <TZ> can be any of those supported by the function
# getTimeZoneOffset()
#
# testingDayStartTimeUtcTD [in]: The testing day start time as the a timedelta
# object.
#
# ToDo: Take into account the CDash server timezone which determine what noon
# means!
# 
def getTestingDayDateFromBuildStartTimeDT(
  buildStartTimeStr, testingDayStartTimeUtcTD,
  ):

  #print()
  #print("testingDayStartTimeUtcTD = "+str(testingDayStartTimeUtcTD))
  #print("testingDayStartTimeUtcTD.seconds = "+str(testingDayStartTimeUtcTD.seconds))

  # Constants
  oneDayTD = datetime.timedelta(days=1)
  noonTD = getProjectTestingDayStartTimeDeltaFromStr("12:00")

  # Convert input 'buildstartime' to datetime object in UtC
  buildStartTimeUtcDT = getBuildStartTimeUtcFromStr(buildStartTimeStr)

  # Get the date and time separately for the input 'buildstartime'
  buildStartTimeDateDT = datetime.datetime(
    year=buildStartTimeUtcDT.year,
    month=buildStartTimeUtcDT.month,
    day=buildStartTimeUtcDT.day,
    )
  #print("buildStartTimeDateDT = "+str(buildStartTimeDateDT))
  buildStartTimeTimeTD = buildStartTimeUtcDT - buildStartTimeDateDT
  #print("buildStartTimeTimeTD = "+str(buildStartTimeTimeTD))
  #print("buildStartTimeTimeTD.seconds = "+str(buildStartTimeTimeTD.seconds))

  if buildStartTimeTimeTD.seconds < testingDayStartTimeUtcTD.seconds:
    buildStartTimeDateDT -= oneDayTD
    #print("buildStartTimeDateDT = "+str(buildStartTimeDateDT))

  if testingDayStartTimeUtcTD > noonTD:
    buildStartTimeDateDT += oneDayTD
    #print("buildStartTimeDateDT = "+str(buildStartTimeDateDT))

  return buildStartTimeDateDT


# Compute the CDash testing day for a given build using its 'buildstarttime'
# field and the testing day start time and return as a string "YYYY-MM-DD".
#
# See getTestingDayDateFromBuildStartTimeDT()
#
def getTestingDayDateFromBuildStartTimeStr(
  buildStartTimeStr, testingDayStartTimeUtcTD,
  ):
  return getDateStrFromDateTime(
    getTestingDayDateFromBuildStartTimeDT(buildStartTimeStr, testingDayStartTimeUtcTD) )


# Return the shifted CDash build start relative to the given CDash project
# testing day start time (as configured on CDash).
#
# This function is meant to match the CDash logic for what testing day a given
# CDash build matches for the CDash 'date' field in various PHP pages for a
# given CDash build with a give build start time (as reported to CDash from
# CTest).
#
# For example, if the CDash project start time is 04:00 UTC and the CDash
# build start time is 2018-01-22T00:00 UTC, then the relative CDash build date
# time would be 2018-01-21T20:00 UTC which is the previous testing day
# 2018-01-21.  But if the CDash build start time was 2018-01-22T04:10 UTC,
# then the relative CDash build date time would be 2018-01-22:00:10 UTC and
# the testing day would be 2018-01-22.
#
# To extract just the year, month and day from the returned datetime object as
# a datetime object, use the function getDateOnlyFromDateTime().
#
# To extract the "<YYYY>-<MM>-<DD>" string from teh returned datetime object,
# use the function getDateStrFromDateTime().
# 
def getRelativeCDashBuildStartTime(
  cdashBuildStartTimeUtc,   # CDash build start time in UTC (datetime objet)
  cdashProjectStartTimeUtc, # CDash start time in UTC (timedelta object)
  dayIncr,                  # Day increment in past or future (timedelta object)
  ):
  relativeCDashBuildDateTime = \
    cdashBuildStartTimeUtc - cdashProjectStartTimeUtc + dayIncr
  return relativeCDashBuildDateTime


# Return the shifted CDash build start relative to the given CDash project
# testing day start time given input from the command-line arguments.
#
# This function just converts the input command-line args and then calls
# getRelativeCDashBuildStartTime().
#
# Arguments:
#
# cdashBuildStartTimeStr [in] Reference build start time in
# "YYY-MM-DDThh:mm:ss TZ".  If 'None', then is taken from
# getCurrentDateTimeUtc().
#
# cdashProjectStartTimeUtcStr [in] CDash project build start time in
# UTC in the format "hh:mm".
#
# dayIncrInt [in] Integer for the day to return relative to
# cdashProjectStartTimeUtcStr.  The current testing day would be 0.  Yesterday
# would be -1 and so on.
#
# debugLevel [in] If 0, then no debugging.  If > 0, then debug output will be
# printed.
#
def getRelativeCDashBuildStartTimeFromCmndLineArgs(
  cdashBuildStartTimeStr,
  cdashProjectStartTimeUtcStr,
  dayIncrInt,
  debugLevel=0,
  ):

  if cdashBuildStartTimeStr:
    buildStartTimeUtc = getBuildStartTimeUtcFromStr(cdashBuildStartTimeStr)
  else:
    buildStartTimeUtc = getCurrentDateTimeUtc()
  if debugLevel: print("buildStartTimeUtc = "+str(buildStartTimeUtc))

  cdashStartTime = \
    getProjectTestingDayStartTimeDeltaFromStr(cdashProjectStartTimeUtcStr)
  if debugLevel: print("cdashStartTime = "+str(cdashStartTime))

  dayIncr = getDayIncrTimeDeltaFromInt(dayIncrInt)
  if debugLevel: print("dayIncr = "+str(dayIncr))

  return getRelativeCDashBuildStartTime(buildStartTimeUtc, cdashStartTime, dayIncr)


# Class to encapsulate handling of CDash testing day logic
#
class CDashProjectTestingDay(object):

  # Construct
  #
  # currentTestingDayDateStr [in]: The current project CDash testing date in
  # string format "YYYY-MM-DD".
  #
  # projectTestingDayStartTimeUtcStr [in]: The CDash projects's testing day start
  # time in UTC.  This is a string in the format "hh:mm".
  #
  def __init__(self, currentTestingDayDateStr, projectTestingDayStartTimeUtcStr):
    # Store input args
    self.__currentTestingDayDateStr = currentTestingDayDateStr
    self.__projectTestingDayStartTimeUtcStr = projectTestingDayStartTimeUtcStr
    # Set the start of the testing day in UTC
    self.__currentTestingDayDateDT = \
      datetime.datetime.strptime(currentTestingDayDateStr, "%Y-%m-%d")
    self.__projectTestingDayStartTimeUtcTD = \
      getProjectTestingDayStartTimeDeltaFromStr(projectTestingDayStartTimeUtcStr)
    noonTD = getProjectTestingDayStartTimeDeltaFromStr("12:00")
    oneDayTD = datetime.timedelta(days=1)
    if self.__projectTestingDayStartTimeUtcTD >= noonTD:
      self.__testingDayStartDateTimeUtcDT = \
         self.__currentTestingDayDateDT - oneDayTD + self.__projectTestingDayStartTimeUtcTD
    else:
      self.__testingDayStartDateTimeUtcDT = \
        self.__currentTestingDayDateDT + self.__projectTestingDayStartTimeUtcTD

  # Return the testing day start in UTC as a datetime object
  def getTestingDayStartUtcDT(self):
    return self.__testingDayStartDateTimeUtcDT

  # Return the testing day date (not time) as a datetime object
  def getCurrentTestingDayDateDT(self):
    return self.__currentTestingDayDateDT

  # Return the testing day as a datetime object for the input 'buildstartime'
  # string.
  def getTestingDayDateFromBuildStartTimeDT(self, buildStartTimeStr):
    return getTestingDayDateFromBuildStartTimeDT(
      buildStartTimeStr, self.__projectTestingDayStartTimeUtcTD)

  # Return the testing day string "YYYY-MM-DD" for the input 'buildstartime'
  # string.
  def getTestingDayDateFromBuildStartTimeStr(self, buildStartTimeStr):
    return getDateStrFromDateTime(
       self.getTestingDayDateFromBuildStartTimeDT(buildStartTimeStr) )


#
# Run the script
#

if __name__ == '__main__':

  inOptions = getCmndLineOptions()

  relativeCDashBuildBuildDateTime = getRelativeCDashBuildStartTimeFromCmndLineArgs(
    inOptions.cdashBuildStartTime, inOptions.cdashProjectStartTimeUtcStr,
    inOptions.dayIncrInt, inOptions.debugLevel,
    )

  print(getDateStrFromDateTime(relativeCDashBuildBuildDateTime))
