/* -*- tab-width: 4; -*- */
/* vi: set sw=2 ts=4 expandtab: */

/* Copyright 2019-2020 The Khronos Group Inc.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @file
 * @~English
 * @brief Utilitiy for interpreting a data format descriptor.
 * @author Andrew Garrard
 */

#include <stdint.h>
#include <stdio.h>
#include <KHR/khr_df.h>
#include "dfd.h"

/**
 * @~English
 * @brief Interpret a Data Format Descriptor for a simple format.
 *
 * @param DFD Pointer to a Data Format Descriptor to interpret,
              described as 32-bit words in native endianness.
              Note that this is the whole descriptor, not just
              the basic descriptor block.
 * @param R Information about the decoded red channel, if any.
 * @param G Information about the decoded green channel, if any.
 * @param B Information about the decoded blue channel, if any.
 * @param A Information about the decoded alpha channel, if any.
 * @param wordBytes Byte size of the channels (unpacked) or total size (packed).
 *
 * @return An enumerant describing the decoded value,
 *         or an error code in case of failure.
 **/
enum InterpretDFDResult interpretDFD(const uint32_t *DFD,
                                     InterpretedDFDChannel *R,
                                     InterpretedDFDChannel *G,
                                     InterpretedDFDChannel *B,
                                     InterpretedDFDChannel *A,
                                     uint32_t *wordBytes)
{
    /* We specifically handle "simple" cases that can be translated */
    /* to things a GPU can access. For simplicity, we also ignore */
    /* the compressed formats, which are generally a single sample */
    /* (and I believe are all defined to be little-endian in their */
    /* in-memory layout, even if some documentation confuses this). */
    /* We also just worry about layout and ignore sRGB, since that's */
    /* trivial to extract anyway. */

    /* DFD points to the whole descriptor, not the basic descriptor block. */
    /* Make everything else relative to the basic descriptor block. */
    const uint32_t *BDFDB = DFD+1;

    uint32_t numSamples = KHR_DFDSAMPLECOUNT(BDFDB);

    uint32_t sampleCounter;
    int determinedEndianness = 0;
    int determinedNormalizedness = 0;
    int determinedSignedness = 0;
    int determinedFloatness = 0;
    enum InterpretDFDResult result = 0; /* Build this up incrementally. */

    /* Clear these so following code doesn't get confused. */
    R->offset = R->size = 0;
    G->offset = G->size = 0;
    B->offset = B->size = 0;
    A->offset = A->size = 0;

    /* First rule out the multiple planes case (trivially) */
    /* - that is, we check that only bytesPlane0 is non-zero. */
    /* This means we don't handle YUV even if the API could. */
    /* (We rely on KHR_DF_WORD_BYTESPLANE0..3 being the same and */
    /* KHR_DF_WORD_BYTESPLANE4..7 being the same as a short cut.) */
    if ((BDFDB[KHR_DF_WORD_BYTESPLANE0] & ~KHR_DF_MASK_BYTESPLANE0)
        || BDFDB[KHR_DF_WORD_BYTESPLANE4]) return i_UNSUPPORTED_MULTIPLE_PLANES;

    /* Only support the RGB color model. */
    /* We could expand this to allow "UNSPECIFIED" as well. */
    if (KHR_DFDVAL(BDFDB, MODEL) != KHR_DF_MODEL_RGBSDA) return i_UNSUPPORTED_CHANNEL_TYPES;

    /* We only pay attention to sRGB. */
    if (KHR_DFDVAL(BDFDB, TRANSFER) == KHR_DF_TRANSFER_SRGB) result |= i_SRGB_FORMAT_BIT;

    /* We only support samples at coordinate 0,0,0,0. */
    /* (We could confirm this from texel_block_dimensions in 1.2, but */
    /* the interpretation might change in later versions.) */
    for (sampleCounter = 0; sampleCounter < numSamples; ++sampleCounter) {
        if (KHR_DFDSVAL(BDFDB, sampleCounter, SAMPLEPOSITION_ALL))
            return i_UNSUPPORTED_MULTIPLE_SAMPLE_LOCATIONS;
    }

    /* Set flags and check for consistency. */
    for (sampleCounter = 0; sampleCounter < numSamples; ++sampleCounter) {
        /* Note: We're ignoring 9995, which is weird and worth special-casing */
        /* rather than trying to generalise to all float formats. */
        if (!determinedFloatness) {
            if (KHR_DFDSVAL(BDFDB, sampleCounter, QUALIFIERS)
                 & KHR_DF_SAMPLE_DATATYPE_FLOAT) {
                result |= i_FLOAT_FORMAT_BIT;
                determinedFloatness = 1;
            }
        } else {
            /* Check whether we disagree with our predetermined floatness. */
            /* Note that this could justifiably happen with (say) D24S8. */
            if (KHR_DFDSVAL(BDFDB, sampleCounter, QUALIFIERS)
                 & KHR_DF_SAMPLE_DATATYPE_FLOAT) {
                if (!(result & i_FLOAT_FORMAT_BIT)) return i_UNSUPPORTED_MIXED_CHANNELS;
            } else {
                if ((result & i_FLOAT_FORMAT_BIT)) return i_UNSUPPORTED_MIXED_CHANNELS;
            }
        }
        if (!determinedSignedness) {
            if (KHR_DFDSVAL(BDFDB, sampleCounter, QUALIFIERS)
                 & KHR_DF_SAMPLE_DATATYPE_SIGNED) {
                result |= i_SIGNED_FORMAT_BIT;
                determinedSignedness = 1;
            }
        } else {
            /* Check whether we disagree with our predetermined signedness. */
            if (KHR_DFDSVAL(BDFDB, sampleCounter, QUALIFIERS)
                 & KHR_DF_SAMPLE_DATATYPE_SIGNED) {
                if (!(result & i_SIGNED_FORMAT_BIT)) return i_UNSUPPORTED_MIXED_CHANNELS;
            } else {
                if ((result & i_SIGNED_FORMAT_BIT)) return i_UNSUPPORTED_MIXED_CHANNELS;
            }
        }
        /* We define "unnormalized" as "sample_upper = 1". */
        /* We don't check whether any non-1 normalization value is correct */
        /* (i.e. set to the maximum bit value, and check min value) on */
        /* the assumption that we're looking at a format which *came* from */
        /* an API we can support. */
        if (!determinedNormalizedness) {
            /* The ambiguity here is if the bottom bit is a single-bit value, */
            /* as in RGBA 5:5:5:1, so we defer the decision if the channel only has one bit. */
            if (KHR_DFDSVAL(BDFDB, sampleCounter, BITLENGTH) > 0) {
                if ((result & i_FLOAT_FORMAT_BIT)) {
                    if (*(float *)(void *)&BDFDB[KHR_DF_WORD_SAMPLESTART +
                                                 KHR_DF_WORD_SAMPLEWORDS * sampleCounter +
                                                 KHR_DF_SAMPLEWORD_SAMPLEUPPER] != 1.0f) {
                        result |= i_NORMALIZED_FORMAT_BIT;
                    }
                } else {
                    if (KHR_DFDSVAL(BDFDB, sampleCounter, SAMPLEUPPER) != 1U) {
                        result |= i_NORMALIZED_FORMAT_BIT;
                    }
                }
                determinedNormalizedness = 1;
            }
        }
        /* Note: We don't check for inconsistent normalization, because */
        /* channels composed of multiple samples will have 0 in the */
        /* lower/upper range. */
        /* This heuristic should handle 64-bit integers, too. */
    }

    /* If this is a packed format, we work out our offsets differently. */
    /* We assume a packed format has channels that aren't byte-aligned. */
    /* If we have a format in which every channel is byte-aligned *and* packed, */
    /* we have the RGBA/ABGR ambiguity; we *probably* don't want the packed */
    /* version in this case, and if hardware has to pack it and swizzle, */
    /* that's up to the hardware to special-case. */
    for (sampleCounter = 0; sampleCounter < numSamples; ++sampleCounter) {
        if (KHR_DFDSVAL(BDFDB, sampleCounter, BITOFFSET) & 0x7U) {
            result |= i_PACKED_FORMAT_BIT;
            /* Once we're packed, we're packed, no need to keep checking. */
            break;
        }
    }

    /* Remember: the canonical ordering of samples is to start with */
    /* the lowest bit of the channel/location which touches bit 0 of */
    /* the data, when the latter is concatenated in little-endian order, */
    /* and then progress until all the bits of that channel/location */
    /* have been processed. Multiple channels sharing the same source */
    /* bits are processed in channel ID order. (I should clarify this */
    /* for partially-shared data, but it doesn't really matter so long */
    /* as everything is consecutive, except to make things canonical.) */
    /* Note: For standard formats we could determine big/little-endianness */
    /* simply from whether the first sample starts in bit 0; technically */
    /* it's possible to have a format with unaligned channels wherein the */
    /* first channel starts at bit 0 and is one byte, yet other channels */
    /* take more bytes or aren't aligned (e.g. D24S8), but this should be */
    /* irrelevant for the formats that we support. */
    if ((result & i_PACKED_FORMAT_BIT)) {
        /* A packed format. */
        uint32_t currentChannel = ~0U; /* Don't start matched. */
        uint32_t currentBitOffset = 0;
        uint32_t currentByteOffset = 0;
        uint32_t currentBitLength = 0;
        *wordBytes = (BDFDB[4] & 0xFFU);
        for (sampleCounter = 0; sampleCounter < numSamples; ++sampleCounter) {
            uint32_t sampleBitOffset = KHR_DFDSVAL(BDFDB, sampleCounter, BITOFFSET);
            uint32_t sampleByteOffset = sampleBitOffset >> 3U;
            /* The sample bitLength field stores the bit length - 1. */
            uint32_t sampleBitLength = KHR_DFDSVAL(BDFDB, sampleCounter, BITLENGTH) + 1;
            uint32_t sampleChannel = KHR_DFDSVAL(BDFDB, sampleCounter, CHANNELID);
            InterpretedDFDChannel *sampleChannelPtr;
            switch (sampleChannel) {
            case KHR_DF_CHANNEL_RGBSDA_RED:
                sampleChannelPtr = R;
                break;
            case KHR_DF_CHANNEL_RGBSDA_GREEN:
                sampleChannelPtr = G;
                break;
            case KHR_DF_CHANNEL_RGBSDA_BLUE:
                sampleChannelPtr = B;
                break;
            case KHR_DF_CHANNEL_RGBSDA_ALPHA:
                sampleChannelPtr = A;
                break;
            default:
                return i_UNSUPPORTED_CHANNEL_TYPES;
            }
            if (sampleChannel == currentChannel) {
                /* Continuation of the same channel. */
                /* Since a big (>32-bit) channel isn't "packed", */
                /* this should only happen in big-endian, or if */
                /* we have a wacky format that we won't support. */
                if (sampleByteOffset == currentByteOffset - 1U && /* One byte earlier */
                    ((currentBitOffset + currentBitLength) & 7U) == 0 && /* Already at the end of a byte */
                    (sampleBitOffset & 7U) == 0) { /* Start at the beginning of the byte */
                    /* All is good, continue big-endian. */
                    /* N.B. We shouldn't be here if we decided we were little-endian, */
                    /* so we don't bother to check that disagreement. */
                    result |= i_BIG_ENDIAN_FORMAT_BIT;
                    determinedEndianness = 1;
                } else {
                    /* Oh dear. */
                    /* We could be little-endian, but not with any standard format. */
                    /* More likely we've got something weird that we can't support. */
                    return i_UNSUPPORTED_NONTRIVIAL_ENDIANNESS;
                }
                /* Remember where we are. */
                currentBitOffset = sampleBitOffset;
                currentByteOffset = sampleByteOffset;
                currentBitLength = sampleBitLength;
                /* Accumulate the bit length. */
                sampleChannelPtr->size += sampleBitLength;
            } else {
                /* Everything is new. Hopefully. */
                currentChannel = sampleChannel;
                currentBitOffset = sampleBitOffset;
                currentByteOffset = sampleByteOffset;
                currentBitLength = sampleBitLength;
                if (sampleChannelPtr->size) {
                    /* Uh-oh, we've seen this channel before. */
                    return i_UNSUPPORTED_NONTRIVIAL_ENDIANNESS;
                }
                /* For now, record the bit offset in little-endian terms, */
                /* because we may not know to reverse it yet. */
                sampleChannelPtr->offset = sampleBitOffset;
                sampleChannelPtr->size = sampleBitLength;
            }
        }
        if ((result & i_BIG_ENDIAN_FORMAT_BIT)) {
            /* Our bit offsets to bit 0 of each channel are in little-endian terms. */
            /* We need to do a byte swap to work out where they should be. */
            /* We assume, for sanity, that byte sizes are a power of two for this. */
            uint32_t offsetMask = (*wordBytes - 1U) << 3U;
            R->offset ^= offsetMask;
            G->offset ^= offsetMask;
            B->offset ^= offsetMask;
            A->offset ^= offsetMask;
        }
    } else {
        /* Not a packed format. */
        /* Everything is byte-aligned. */
        /* Question is whether there multiple samples per channel. */
        uint32_t currentChannel = ~0U; /* Don't start matched. */
        uint32_t currentByteOffset = 0;
        uint32_t currentByteLength = 0;
        for (sampleCounter = 0; sampleCounter < numSamples; ++sampleCounter) {
            uint32_t sampleByteOffset = KHR_DFDSVAL(BDFDB, sampleCounter, BITOFFSET) >> 3U;
            uint32_t sampleByteLength = (KHR_DFDSVAL(BDFDB, sampleCounter, BITLENGTH) + 1) >> 3U;
            uint32_t sampleChannel = KHR_DFDSVAL(BDFDB, sampleCounter, CHANNELID);
            InterpretedDFDChannel *sampleChannelPtr;
            switch (sampleChannel) {
            case KHR_DF_CHANNEL_RGBSDA_RED:
                sampleChannelPtr = R;
                break;
            case KHR_DF_CHANNEL_RGBSDA_GREEN:
                sampleChannelPtr = G;
                break;
            case KHR_DF_CHANNEL_RGBSDA_BLUE:
                sampleChannelPtr = B;
                break;
            case KHR_DF_CHANNEL_RGBSDA_ALPHA:
                sampleChannelPtr = A;
                break;
            default:
                return i_UNSUPPORTED_CHANNEL_TYPES;
            }
            if (sampleChannel == currentChannel) {
                /* Continuation of the same channel. */
                /* Either big-endian, or little-endian with a very large channel. */
                if (sampleByteOffset == currentByteOffset - 1) { /* One byte earlier */
                    if (determinedEndianness && !(result & i_BIG_ENDIAN_FORMAT_BIT)) {
                        return i_UNSUPPORTED_NONTRIVIAL_ENDIANNESS;
                    }
                    /* All is good, continue big-endian. */
                    result |= i_BIG_ENDIAN_FORMAT_BIT;
                    determinedEndianness = 1;
                    /* Update the start */
                    sampleChannelPtr->offset = sampleByteOffset;
                } else if (sampleByteOffset == currentByteOffset + currentByteLength) {
                    if (determinedEndianness && (result & i_BIG_ENDIAN_FORMAT_BIT)) {
                        return i_UNSUPPORTED_NONTRIVIAL_ENDIANNESS;
                    }
                    /* All is good, continue little-endian. */
                    determinedEndianness = 1;
                } else {
                    /* Oh dear. */
                    /* We could be little-endian, but not with any standard format. */
                    /* More likely we've got something weird that we can't support. */
                    return i_UNSUPPORTED_NONTRIVIAL_ENDIANNESS;
                }
                /* Remember where we are. */
                currentByteOffset = sampleByteOffset;
                currentByteLength = sampleByteLength;
                /* Accumulate the byte length. */
                sampleChannelPtr->size += sampleByteLength;
                /* Assume these are all the same. */
                *wordBytes = sampleChannelPtr->size;
            } else {
                /* Everything is new. Hopefully. */
                currentChannel = sampleChannel;
                currentByteOffset = sampleByteOffset;
                currentByteLength = sampleByteLength;
                if (sampleChannelPtr->size) {
                    /* Uh-oh, we've seen this channel before. */
                    return i_UNSUPPORTED_NONTRIVIAL_ENDIANNESS;
                }
                /* For now, record the byte offset in little-endian terms, */
                /* because we may not know to reverse it yet. */
                sampleChannelPtr->offset = sampleByteOffset;
                sampleChannelPtr->size = sampleByteLength;
                /* Assume these are all the same. */
                *wordBytes = sampleByteLength;
            }
        }
    }
    return result;
}
