
#!/usr/bin/env python
import datetime
import numpy as np

__authors__ = ["Ming Chen"]
__license__ = "Public Domain"
__maintainer__ = "Ming Chen"
__email__ = "ming.chen@noaa.gov"
__date__  = "Feb 18, 2024"


class gpsdate(object):

   def __init__(self, date_str: str):
       self.ymd =  fmtdate(date_str)
       mydate = datetime.datetime.strptime(self.ymd, "%Y%m%d")
       self.year = str(mydate.year)
       self.month = str(mydate.month)
       self.doy =  str(mydate.strftime('%j'))
    
       # Define the GPS epoch (January 6, 1980)
       gps_epoch = datetime.datetime(1980, 1, 6)
    
       # Calculate the difference in days between the given date and the GPS epoch.
       delta = mydate - gps_epoch
       dd = delta.days
       self.mjday = str(dd + 44244)  #MJD: "{mjday}.000000"

       # Determine the week number and day-of-week offset.
       self.gweek = str(dd // 7)
       self.gday  = str(dd % 7)
  
   def datestr(self, date_str: str, output_format: str = None) -> str:
       if output_format is None:
          out_str = f"{self.gweek:04d}{self.gday}"
       else:
          mydate = datetime.datetime.strptime(self.ymd, "%Y%m%d")
          out_str = mydate.strftime(output_format)
       return out_str


# Function to convert the date to 'yyyyddd' or 'yyyymndd' format
def mkdate(date_str):

    date_str = fmtdate(date_str)
    try:
        return datetime.datetime.strptime(date_str, '%Y%m%d')
    except ValueError:
        raise ValueError(f"Invalid date format: {date_str}")

        
def gpsw2date(gpsw: str) -> str:
    """
    Convert a GPS week number (as a string) into a date string in YYYYMMDD format.
    Here we assume the GPS epoch is January 6, 1980.
    """
    try:
       gps_week = int(gpsw[0:4])
       gps_day = int(gpsw[4:5])
       gps_epoch = datetime.datetime(1980, 1, 6)
       date_obj = gps_epoch + datetime.timedelta(weeks=gps_week)+ datetime.timedelta(days=gps_day)
       return date_obj.strftime("%Y%m%d")
    except ValueError:
       return False


def date2gpsw(date_str: str) -> str:
    """
    Convert a date string in YYYYMMDD format to a GPS week string.
    
    The GPS week string is composed of:
      - A 4-digit, zero-padded week number since the GPS epoch (January 6, 1980).
      - A single digit representing the day offset within that week.
    
    Parameters:
      date_str: A date string in the format "YYYYMMDD".
      
    Returns:
      A string in the format "WWWWd", where WWWW is the 4-digit week number and d is the day-of-week offset.
      
    Raises:
      ValueError: If the input is not exactly 8 digits or if the date is invalid.
    """
    # Check that the input has exactly 8 characters.
    if len(date_str) != 8:
        raise ValueError("Usage: date2gpsw(YYYYMMDD)")
    
    # Validate and parse the date string.
    try:
        mydate = datetime.datetime.strptime(date_str, "%Y%m%d")
    except ValueError as e:
        raise ValueError("Invalid date input") from e

    # Define the GPS epoch (January 6, 1980)
    gps_epoch = datetime.datetime(1980, 1, 6)
    
    # Calculate the difference in days between the given date and the GPS epoch.
    delta = mydate - gps_epoch
    dd = delta.days

    # Determine the week number and day-of-week offset.
    weeks = dd // 7
    wday = dd % 7

    # Format the week number as a zero-padded 4-digit string and append the day offset.
    gps_week_str = f"{weeks:04d}{wday}"
    return gps_week_str


def fmtdate(input_date: str, output_format: str = None) -> str:
    """
    Convert an input date string into a formatted date string.
    
    Parameters:
      input_date: The date string in one of the following formats:
         - 8-digit date (YYYYMMDD)
         - 7-digit date (YYYYDDD, where DDD is day-of-year)
         - 5-digit GPS week (which will be converted using gpsw2date)
      output_format: (Optional) A strftime format string (e.g., "%Y-%m-%d"). 
         If not provided, the output defaults to "YYYYMMDD".
    
    Returns:
      A string representing the date in the desired format.
    
    Raises:
      ValueError: If the input_date is not valid or not in one of the expected formats.
    """
    
    error_msg = "Wrong datetime input: expected yyyyddd, yyyymmdd, or GPS wwwwd."
    if not input_date.isdigit():
        raise ValueError(error_msg)

    # Case 1: 8-digit input (assumed YYYYMMDD)
    if len(input_date) == 8:
        try:
            date_obj = datetime.datetime.strptime(input_date, "%Y%m%d")
        except ValueError as e:
            raise ValueError("Invalid date format for YYYYMMDD") from e

    # Case 2: 7-digit input (assumed YYYYDDD, day-of-year format)
    elif len(input_date) == 7:
        try:
            date_obj = datetime.datetime.strptime(input_date, "%Y%j")
        except ValueError as e:
            raise ValueError("Invalid date format for YYYYDDD") from e

    # Case 3: 5-digit input (assumed GPS week)
    elif len(input_date) == 5:
        try:
            gps_date_str = gpsw2date(input_date)
            date_obj = datetime.datetime.strptime(gps_date_str, "%Y%m%d")
        except ValueError as e:
            raise ValueError("Invalid date conversion from GPS week") from e

    else:
        raise ValueError(error_msg)

    # If an output format is specified, reformat the date accordingly.
    if output_format:
        formatted_date = date_obj.strftime(output_format)
    else:
        formatted_date = date_obj.strftime("%Y%m%d")
    
    return formatted_date

# Example usages:
if __name__ == "__main__":
    # 8-digit date example
    print("8-digit input:", mkdate("20250315"))  # Output: 20250315
    
    # 7-digit date example (e.g., 2025001 = Jan 1, 2025)
    print("7-digit input:", mkdate("2025001"))  # Output: 20250101
    
    # GPS week example (using GPS week "23576" for instance)
    print("GPS week input:", mkdate("23576"))
    
    # Using an output format (e.g., YYYY-MM-DD)
    print("Formatted date:", mkdate("20250315", "%Y-%m-%d"))

    try:
        # Example: Converting March 15, 2025.
        gps_week = date2gpsw("20250315")
        print("GPS week string:", gps_week)
    except ValueError as err:
        print(err)
