#
# Copyright 2019 Pixar
#
# Licensed under the terms set forth in the LICENSE.txt file available at
# https://openusd.org/license.
#

def _GetFloatStringPrecision(floatString):
    """
    Gets the floating point precision specified by floatString.

    floatString can either contain an actual float in string form, or it can be
    a frame placeholder. We simply split the string on the dot (.) and return
    the length of the part after the dot, if any.

    If there is no dot in the string, a precision of zero is assumed.
    """
    floatPrecision = 0

    if not floatString:
        return floatPrecision

    floatStringParts = floatString.split('.')
    if len(floatStringParts) > 1:
        floatPrecision = len(floatStringParts[1])

    return floatPrecision

class FrameSpecIterator(object):
    """
    A simple iterator object that handles splitting multiple comma-separated
    FrameSpecs into their equivalent UsdUtils.TimeCodeRanges, and then yields
    all of the time codes in all of those ranges sequentially when iterated.
    
    This object also stores the minimum floating point precision required to
    disambiguate any neighboring time codes in the FrameSpecs given. This can
    be used to validate that the frame placeholder in a frame format string has
    enough precision to uniquely identify every frame without collisions.
    """
    from pxr import UsdUtils

    FRAMESPEC_SEPARATOR = ','

    def __init__(self, frameSpec):
        from pxr import UsdUtils

        # Assume all frames are integral until we find a frame spec with a
        # non-integral stride.
        self._minFloatPrecision = 0

        self._timeCodeRanges = []
        subFrameSpecs = frameSpec.split(self.FRAMESPEC_SEPARATOR)
        for subFrameSpec in subFrameSpecs:
            timeCodeRange = UsdUtils.TimeCodeRange.CreateFromFrameSpec(
                subFrameSpec)
            self._timeCodeRanges.append(timeCodeRange)

            specParts = subFrameSpec.split(
                UsdUtils.TimeCodeRange.Tokens.StrideSeparator)
            if len(specParts) == 2:
                stride = specParts[1]
                stridePrecision = _GetFloatStringPrecision(stride)
                self._minFloatPrecision = max(self._minFloatPrecision,
                    stridePrecision)

    def __iter__(self):
        for timeCodeRange in self._timeCodeRanges:
            for timeCode in timeCodeRange:
                yield timeCode.GetValue()

    @property
    def minFloatPrecision(self):
        return self._minFloatPrecision


def AddCmdlineArgs(argsParser, altDefaultTimeHelpText='', altFramesHelpText=''):
    """
    Adds frame-related command line arguments to argsParser.

    The resulting 'frames' argument will be an iterable of UsdTimeCodes.

    If no command-line arguments are given, 'frames' will be a list containing
    the startTimeCode from the usdStage, using UsdStage::GetStartTimeCode(). 
    If '--defaultTime' is given, 'frames' will be a list containing only 
    Usd.TimeCode.Default(). Otherwise, '--frames' must be given a FrameSpec (or 
    a comma-separated list of multiple FrameSpecs), and 'frames' will be a 
    FrameSpecIterator which when iterated will yield the time codes specified 
    by the FrameSpec(s) as floats.
    """
    timeGroup = argsParser.add_mutually_exclusive_group()

    helpText = altDefaultTimeHelpText
    if not helpText:
        helpText = (
            'explicitly operate at the Default time code (the default '
            'behavior is to operate at the startTimeCode authored on the '
            'UsdStage which defaults to 0.0)')
    timeGroup.add_argument('--defaultTime', '-d', action='store_true',
        dest='defaultTime', help=helpText)

    helpText = altFramesHelpText
    if not helpText:
        helpText = (
            'specify FrameSpec(s) of the time codes to operate on - A '
            'FrameSpec consists of up to three floating point values for the '
            'start time code, end time code, and stride of a time code range. '
            'A single time code can be specified, or a start and end time '
            'code can be specified separated by a colon (:). When a start '
            'and end time code are specified, the stride may optionally be '
            'specified as well, separating it from the start and end time '
            'codes with (x). Multiple FrameSpecs can be combined as a '
            'comma-separated list. The following are examples of valid '
            'FrameSpecs: 123 - 101:105 - 105:101 - 101:109x2 - 101:110x2 - '
            '101:104x0.5')
    timeGroup.add_argument('--frames', '-f', action='store', type=str,
        dest='frames', metavar='FRAMESPEC[,FRAMESPEC...]',
        help=helpText)

def GetFramePlaceholder(frameFormat):
    """
    Gets the frame placeholder in a frame format string.

    This function expects the input frameFormat string to contain exactly one
    frame placeholder. The placeholder must be composed of exactly one or two
    groups of one or more hashes ('#'), and if there are two, they must be
    separated by a dot ('.').

    If no such placeholder exists in the frame format string, None is returned.
    """
    if not frameFormat:
        return None

    import re

    PLACEHOLDER_PATTERN = r'^[^#]*(?P<placeholder>#+(\.#+)?)[^#]*$'

    matches = re.search(PLACEHOLDER_PATTERN, frameFormat)
    if not matches:
        return None

    placeholder = matches.group(1)

    return placeholder

def ConvertFramePlaceholderToFloatSpec(frameFormat):
    """
    Converts the frame placeholder in a frame format string to a Python
    {}-style float specifier for use with string.format().

    This function expects the input frameFormat string to contain exactly one
    frame placeholder. The placeholder must be composed of exactly one or two
    groups of one or more hashes ('#'), and if there are two, they must be
    separated by a dot ('.').

    The hashes after the dot indicate the floating point precision to use in
    the frame numbers inserted into the frame format string. If there is only
    a single group of hashes, the precision is zero and the inserted frame
    numbers will be integer values.

    The overall width of the frame placeholder specifies the minimum width to
    use when inserting frame numbers into the frame format string. Formatted
    frame numbers smaller than the minimum width will be zero-padded on the
    left until they reach the minimum width.

    If the input frame format string does not contain exactly one frame
    placeholder, this function will return None, indicating that this frame
    format string cannot be used when operating with a frame range.
    """
    placeholder = GetFramePlaceholder(frameFormat)
    if not placeholder:
        return None

    # Frame numbers are zero-padded up to the field width.
    specFill = 0

    # The full width of the placeholder determines the minimum field width.
    specWidth = len(placeholder)

    # The hashes after the dot, if any, determine the precision. If there are
    # none, integer frame numbers are used.
    specPrecision = 0
    parts = placeholder.split('.')
    if len(parts) > 1:
        specPrecision = len(parts[1])

    floatSpec = ('{frame:' +
        '{fill}{width}.{precision}f'.format(fill=specFill,
            width=specWidth, precision=specPrecision) +
        '}')

    return frameFormat.replace(placeholder, floatSpec)

def ValidateCmdlineArgs(argsParser, args, usdStage, frameFormatArgName=None):
    """
    Validates the frame-related arguments in args parsed by argsParser.

    This populates 'frames' with the appropriate iterable based on the
    command-line arguments given, so it should be called after parse_args() is
    called on argsParser.

    When working with frame ranges, particularly when writing out images for
    each frame, it is common to also have command-line arguments such as an
    output image path for specifying where those images should be written. The
    value given to this argument should include a frame placeholder so that it
    can have the appropriate time code inserted. If the application has such an
    argument, its name can be specified using frameFormatArgName. That arg will
    be checked to ensure that it has a frame placeholder and it will be given
    a value with that placeholder replaced with a Python format specifier so
    that the value is ready to use with the str.format(frame=<timeCode>)
    method. If a frame range is not provided as an argument, then it is an
    error to include a frame placeholder in the frame format string.
    """
    from pxr import Usd

    framePlaceholder = None
    frameFormat = None
    if frameFormatArgName is not None:
        frameFormat = getattr(args, frameFormatArgName)
        framePlaceholder = GetFramePlaceholder(frameFormat)
        frameFormat = ConvertFramePlaceholderToFloatSpec(frameFormat)

    if args.frames:
        args.frames = FrameSpecIterator(args.frames)

        if frameFormatArgName is not None:
            if not frameFormat:
                argsParser.error('%s must contain exactly one frame number '
                    'placeholder of the form "###"" or "###.###". Note that '
                    'the number of hash marks is variable in each group.' %
                    frameFormatArgName)

            placeholderPrecision = _GetFloatStringPrecision(framePlaceholder)

            if placeholderPrecision < args.frames.minFloatPrecision:
                argsParser.error('The given FrameSpecs require a minimum '
                    'floating point precision of %d, but the frame '
                    'placeholder in %s only specified a precision of %d (%s). '
                    'The precision of the frame placeholder must be equal to '
                    'or greater than %d.' % (args.frames.minFloatPrecision,
                        frameFormatArgName, placeholderPrecision,
                        framePlaceholder,args.frames.minFloatPrecision))

            setattr(args, frameFormatArgName, frameFormat)
    else:
        if frameFormat:
            argsParser.error('%s cannot contain a frame number placeholder '
                'when not operating on a frame range.' % frameFormatArgName)

        if args.defaultTime:
            args.frames = [Usd.TimeCode.Default().GetValue()]
        else:
            args.frames = [usdStage.GetStartTimeCode()]

    return args
