public class DdsLoader {
    public static ImageSet loadFromFile(String ddsFilepath) throws IOException {
        InputStream ddsInputStream = DdsLoader.class.getResourceAsStream(ddsFilepath);
        byte[] ddsFile = readDdsFile(ddsInputStream);

        // Check the first 4 bytes.
        int magicTest = readDoubleWord(ddsFile, 0);
        if (magicTest != MagicNumbers.DDS_MAGIC_NUMBER) {
            throw new DdsFileMalformedException(ddsFilepath, "The Magic number is missing from the file.");
        }

        if (ddsFile.length < DdsHeader.BYTES + 4) {
            throw new DdsFileMalformedException(ddsFilepath, "The data is way too small to store actual information.");
        }

        // Collect info from the DDS file.
        DdsHeader ddsHeader = new DdsHeader(ddsFile);
        Dds10Header dds10Header = getDds10Header(ddsFile, ddsHeader);
        ImageSet.Dimensions ddsDimensions = getDimensions(ddsHeader);
        ImageFormat.UncheckedImageFormat ddsFormat = getFormat(ddsHeader, dds10Header);

        // Get image counts.
        int numArrays = (dds10Header.arraySize > 1) ? dds10Header.arraySize : 1;
        int numFaces = (dds10Header.miscFlag & Dds10MiscFlags.RESOURCE_MISC_TEXTURECUBE) != 0 ? 6 : 1;
        int numMipmaps = (ddsHeader.flags & DdsFlags.MIPMAPCOUNT) != 0 ? ddsHeader.mipmapCount : 1;

        int baseOffset = getOffsetToData(ddsHeader);

        // Build the image creator.
        ImageCreator imageCreator = new ImageCreator(new ImageFormat(ddsFormat), ddsDimensions, numMipmaps, numArrays, numFaces);
        int cumulativeOffset = baseOffset;

        for (int arrayIx = 0; arrayIx < numArrays; arrayIx++) {
            for (int faceIx = 0; faceIx < numFaces; faceIx++) {
                for (int mipmapLevel = 0; mipmapLevel < numMipmaps; mipmapLevel++) {
                    int mipmapLevelSize = calcMipmapSize(ddsFormat, ddsDimensions, mipmapLevel);

                    // Get specific data from ddsFile.
                    byte[] mipmapLevelData = Arrays.copyOfRange(ddsFile, cumulativeOffset, cumulativeOffset + mipmapLevelSize);

                    // Set data for the current mipmap level in imageCreator.
                    imageCreator.setImageData(mipmapLevelData, true, mipmapLevel, arrayIx, faceIx);

                    // Advance offset to read next mipmapLevelData from ddsFile.
                    cumulativeOffset += mipmapLevelSize;
                }
            }
        }

        return imageCreator.createImage();
    }

    ////////////////////////////////
    private static class DdsFileMalformedException extends RuntimeException {
        private DdsFileMalformedException(String filename, String message) {
            super(filename + ": " + message);
        }
    }

    private static class DdsFileUnsupportedException extends RuntimeException {
        private DdsFileUnsupportedException(String filename, String message) {
            super(filename + ": " + message);
        }
    }

    ////////////////////////////////
    private static class MagicNumbers {
        static final int DDS_MAGIC_NUMBER = 0x20534444;  // "DDS "
        static final int DDS10_FOUR_CC = 0x30314458;     // "DX10"
        static final int DDSFOURCC_DXT1 = 0x31545844;    // "DXT1"
        static final int DDSFOURCC_DXT3 = 0x33545844;    // "DXT3"
        static final int DDSFOURCC_DXT5 = 0x35545844;    // "DXT5"
    }


    private static class DdsFlags {
        static final int CAPS = 0x00000001;
        static final int HEIGHT = 0x00000002;
        static final int WIDTH = 0x00000004;
        static final int PITCH = 0x00000008;
        static final int PIXELFORMAT = 0x00001000;
        static final int MIPMAPCOUNT = 0x00020000;
        static final int LINEARSIZE = 0x00080000;
        static final int DEPTH = 0x00800000;
    }

    private static class Dds10MiscFlags {
        static final int RESOURCE_MISC_TEXTURECUBE = 0x00000004;
    }

    private static class Dds10ResourceDimensions {
        static final int TEXTURE1D = 2;
        static final int TEXTURE2D = 3;
        static final int TEXTURE3D = 4;
    }

    private static class DdsCaps2 {
        static final int CUBEMAP = 0x00000200;
        static final int CUBEMAP_POSITIVEX = 0x00000400;
        static final int CUBEMAP_NEGATIVEX = 0x00000800;
        static final int CUBEMAP_POSITIVEY = 0x00001000;
        static final int CUBEMAP_NEGATIVEY = 0x00002000;
        static final int CUBEMAP_POSITIVEZ = 0x00004000;
        static final int CUBEMAP_NEGATIVEZ = 0x00008000;
        static final int VOLUME = 0x00200000;

        static final int CUBEMAP_ALL = CUBEMAP |
                CUBEMAP_POSITIVEX | CUBEMAP_NEGATIVEX |
                CUBEMAP_POSITIVEY | CUBEMAP_NEGATIVEY |
                CUBEMAP_POSITIVEZ | CUBEMAP_NEGATIVEZ;
    }

    private static class DxgiFormat {
        static final int UNKNOWN = 0;
    }


    private static class DdsPixelFormat {
        static final int BYTES = Integer.BYTES * (8);

        int size;
        int flags;
        int fourCC;
        int rgbBitCount;
        int rBitMask;
        int gBitMask;
        int bBitMask;
        int aBitMask;
    }

    private static class DdsPixelFormatFlags {
        static final int ALPHAPIXELS = 0x00000001;
        static final int ALPHA = 0x00000002;
        static final int FOURCC = 0x00000004;
        static final int RGB = 0x00000040;
        static final int YUV = 0x00000200;
        static final int LUMINANCE = 0x00020000;
    }


    private static class DdsHeader {
        static final int BYTES = Integer.BYTES * (7 + 11) + DdsPixelFormat.BYTES + Integer.BYTES * (5);

        int size;
        int flags;
        int height;
        int width;
        int pitchOrLinearSize;
        int depth;
        int mipmapCount;

        int reserved1[] = new int[11];

        DdsPixelFormat ddsPixelFormat;

        int caps;
        int caps2;
        int caps3;
        int caps4;
        int reserved2;

        DdsHeader(byte[] ddsFile) {
            size = readDoubleWord(ddsFile, 4);
            flags = readDoubleWord(ddsFile, 8);
            height = readDoubleWord(ddsFile, 12);
            width = readDoubleWord(ddsFile, 16);
            pitchOrLinearSize = readDoubleWord(ddsFile, 20);
            depth = readDoubleWord(ddsFile, 24);
            mipmapCount = readDoubleWord(ddsFile, 28);

            for (int i = 0; i < reserved1.length; i++) {
                reserved1[i] = readDoubleWord(ddsFile, 32 + 4 * i);
            }

            ddsPixelFormat = new DdsPixelFormat();
            ddsPixelFormat.size = readDoubleWord(ddsFile, 76);
            ddsPixelFormat.flags = readDoubleWord(ddsFile, 80);
            ddsPixelFormat.fourCC = readDoubleWord(ddsFile, 84);
            ddsPixelFormat.rgbBitCount = readDoubleWord(ddsFile, 88);
            ddsPixelFormat.rBitMask = readDoubleWord(ddsFile, 92);
            ddsPixelFormat.gBitMask = readDoubleWord(ddsFile, 96);
            ddsPixelFormat.bBitMask = readDoubleWord(ddsFile, 100);
            ddsPixelFormat.aBitMask = readDoubleWord(ddsFile, 104);

            caps = readDoubleWord(ddsFile, 108);
            caps2 = readDoubleWord(ddsFile, 112);
            caps3 = readDoubleWord(ddsFile, 116);
            caps4 = readDoubleWord(ddsFile, 120);
            reserved2 = readDoubleWord(ddsFile, 124);
        }
    }

    private static class Dds10Header {
        static final int SIZE = Integer.BYTES * (5);

        int dxgiFormat;
        int resourceDimension;
        int miscFlag;
        int arraySize;
        int reserved;
    }

    ////////////////////////////////
    private static byte[] readDdsFile(InputStream ddsInStream) throws IOException {
        assert ddsInStream != null;

        ByteArrayOutputStream ddsOutStream = new ByteArrayOutputStream();
        int bytesRead;

        byte[] buffer = new byte[4096];

        while ((bytesRead = ddsInStream.read(buffer)) != -1) {
            ddsOutStream.write(buffer, 0, bytesRead);
        }

        return ddsOutStream.toByteArray();
    }


    // Read four bytes.
    private static int readDoubleWord(byte bytes[], int startIx) {
        int dw = 0;
        // Read 4 bytes.
        for (int i = startIx; i < startIx + 4; i++) {
            dw += (bytes[i] & 0xff) << (8 * (i - startIx));
        }
        return dw;
    }


    // Will either generate this or return the actual one.
    private static Dds10Header getDds10Header(byte[] ddsFile, DdsHeader ddsHeader) {
        if (ddsHeader.ddsPixelFormat.fourCC == MagicNumbers.DDS10_FOUR_CC) {
            Dds10Header dds10Header = new Dds10Header();
            int offsetToNewHeader = DdsHeader.BYTES + 4;

            dds10Header.dxgiFormat = readDoubleWord(ddsFile, offsetToNewHeader);
            dds10Header.resourceDimension = readDoubleWord(ddsFile, offsetToNewHeader + 4);
            dds10Header.miscFlag = readDoubleWord(ddsFile, offsetToNewHeader + 8);
            dds10Header.arraySize = readDoubleWord(ddsFile, offsetToNewHeader + 12);
            dds10Header.reserved = readDoubleWord(ddsFile, offsetToNewHeader + 16);
            return dds10Header;
        }

        // Compute the header manually. Namely, compute the DXGI_FORMAT for the given data.
        Dds10Header dds10header = new Dds10Header();

        // Get dimensionality. Assume 2D unless otherwise stated.
        dds10header.resourceDimension = Dds10ResourceDimensions.TEXTURE2D;

        if ((ddsHeader.caps2 & DdsCaps2.VOLUME) != 0 && (ddsHeader.flags & DdsFlags.DEPTH) != 0) {
            dds10header.resourceDimension = Dds10ResourceDimensions.TEXTURE3D;
        }

        // Get cubemap.
        int cubemapTest = ddsHeader.caps2 & DdsCaps2.CUBEMAP_ALL;
        if (cubemapTest == 0) {
            dds10header.miscFlag = 0;
        } else {
            // All faces must be specified or none. Otherwise unsupported.
            if (cubemapTest != DdsCaps2.CUBEMAP_ALL) {
                throw new DdsFileUnsupportedException("", "All cubemap faces must be specified.");
            }

            dds10header.miscFlag = Dds10MiscFlags.RESOURCE_MISC_TEXTURECUBE;
        }

        // Array size is... zero?
        dds10header.arraySize = 0;

        // Use the old-style format.
        dds10header.dxgiFormat = DxgiFormat.UNKNOWN;
        return dds10header;
    }


    private static ImageSet.Dimensions getDimensions(DdsHeader ddsHeader) {
        ImageSet.Dimensions ddsDimensions = new ImageSet.Dimensions();
        ddsDimensions.numDimensions = 1;
        ddsDimensions.width = ddsHeader.width;

        if ((ddsHeader.flags & DdsFlags.HEIGHT) != 0) {
            ddsDimensions.numDimensions = 2;
            ddsDimensions.height = ddsHeader.height;
        }

        if ((ddsHeader.flags & DdsFlags.DEPTH) != 0) {
            ddsDimensions.numDimensions = 3;
            ddsDimensions.depth = ddsHeader.depth;
        }

        return ddsDimensions;
    }

    private static ImageFormat.UncheckedImageFormat getFormat(DdsHeader ddsHeader, Dds10Header dds10Header) throws DdsFileUnsupportedException {
        if (dds10Header.dxgiFormat != DxgiFormat.UNKNOWN) {
            throw new RuntimeException("Not implemented.");
        }

        for (OldDdsFormatConv oldFormatConvert : DdsLoader.oldFormatConvert) {
            if (doesMatchFormat(oldFormatConvert.ddsFmt, ddsHeader)) {
                return oldFormatConvert.uncheckedImageFormat;
            }
        }

        throw new DdsFileUnsupportedException("", "Could not use the DDS9's image format.");
    }

    private static int getOffsetToData(DdsHeader ddsHeader) {
        int byteOffset = DdsHeader.BYTES + 4;
        if (ddsHeader.ddsPixelFormat.fourCC == MagicNumbers.DDS10_FOUR_CC) {
            byteOffset += Dds10Header.SIZE;
        }
        return byteOffset;
    }


    // Computes the bytesize of a single scanline of an image of the given format, with the given line width.
    // For compressed textures, the value returned is the number of bytes for every 4 scanlines.
    private static int calcLineSize(ImageFormat.UncheckedImageFormat ddsFormat, int lineWidth) {
        // This is from the DDS suggestions for line size computations.
        if (ddsFormat.bitDepth == ImageFormat.BitDepth.COMPRESSED) {
            int blockSize = 16;
            if (ddsFormat.type == ImageFormat.PixelDataType.COMPRESSED_BC1
                    || ddsFormat.type == ImageFormat.PixelDataType.COMPRESSED_UNSIGNED_BC4
                    || ddsFormat.type == ImageFormat.PixelDataType.COMPRESSED_SIGNED_BC4) {
                blockSize = 8;
            }
            return ((lineWidth + 3) / 4) * blockSize;
        } else {
            int bytesPerPixel = Util.calcBytesPerPixel(new ImageFormat(ddsFormat));
            return lineWidth * bytesPerPixel;
        }
    }

    private static int calcMipmapSize(ImageFormat.UncheckedImageFormat ddsFormat, ImageSet.Dimensions ddsDimensions, int mipmapLevel) {
        ImageSet.Dimensions mipmapDimensions = Util.calcMipmapLevelDimensions(ddsDimensions, mipmapLevel);
        int lineSize = calcLineSize(ddsFormat, mipmapDimensions.width);

        int effectiveHeight = 1;
        if (mipmapDimensions.numDimensions > 1) {
            effectiveHeight = mipmapDimensions.height;
            if (ddsFormat.bitDepth == ImageFormat.BitDepth.COMPRESSED) {
                effectiveHeight = (effectiveHeight + 3) / 4;
            }
        }

        int effectiveDepth = 1;
        if (mipmapDimensions.numDimensions > 2) {
            effectiveDepth = mipmapDimensions.depth;
            if (ddsFormat.bitDepth == ImageFormat.BitDepth.COMPRESSED) {
                effectiveDepth = (effectiveDepth + 3) / 4;
            }
        }

        int numLines = effectiveHeight * effectiveDepth;
        return lineSize * numLines;
    }


    ////////////////////////////////
    private static OldDdsFormatConv oldFormatConvert[] = {
            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGBA, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB | DdsPixelFormatFlags.ALPHAPIXELS, 32, 0xff, 0xff00, 0xff0000, 0xff000000, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGBA, ImageFormat.ComponentOrder.BGRA, ImageFormat.BitDepth.PACKED_32_BIT_8888_REV, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB | DdsPixelFormatFlags.ALPHAPIXELS, 32, 0xff0000, 0xff00, 0xff, 0xff000000, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGBX, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 32, 0xff, 0xff00, 0xff0000, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGB, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 24, 0xff, 0xff00, 0xff0000, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGB, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 24, 0xff, 0xff00, 0xff0000, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGB, ImageFormat.ComponentOrder.BGRA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 24, 0xff0000, 0xff00, 0xff, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGB, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PACKED_16_BIT_565, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 16, 0xf800, 0x7e0, 0x1f, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGBA, ImageFormat.ComponentOrder.BGRA, ImageFormat.BitDepth.PACKED_16_BIT_1555_REV, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 16, 0x7c00, 0x3e0, 0x1f, 0x8000, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RGBA, ImageFormat.ComponentOrder.BGRA, ImageFormat.BitDepth.PACKED_16_BIT_4444_REV, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 16, 0xf00, 0xf0, 0xf, 0xf000, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.COMPRESSED_BC1, ImageFormat.PixelComponents.COLOR_RGB, ImageFormat.ComponentOrder.COMPRESSED, ImageFormat.BitDepth.COMPRESSED, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.FOURCC, 0, 0, 0, 0, 0, MagicNumbers.DDSFOURCC_DXT1)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.COMPRESSED_BC2, ImageFormat.PixelComponents.COLOR_RGBA, ImageFormat.ComponentOrder.COMPRESSED, ImageFormat.BitDepth.COMPRESSED, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.FOURCC, 0, 0, 0, 0, 0, MagicNumbers.DDSFOURCC_DXT3)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.COMPRESSED_BC3, ImageFormat.PixelComponents.COLOR_RGBA, ImageFormat.ComponentOrder.COMPRESSED, ImageFormat.BitDepth.COMPRESSED, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.FOURCC, 0, 0, 0, 0, 0, MagicNumbers.DDSFOURCC_DXT5)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RG, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_16, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 32, 0xffff, 0xffff0000, 0, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RG, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 16, 0xffff, 0xffff0000, 0, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RED, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_16, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.LUMINANCE, 16, 0xffff, 0, 0, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RED, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.LUMINANCE, 8, 0xff, 0, 0, 0, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RG, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_16, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.LUMINANCE | DdsPixelFormatFlags.ALPHAPIXELS, 16, 0xffff, 0, 0, 0xffff0000, 0)
            ),

            new OldDdsFormatConv(
                    new ImageFormat.UncheckedImageFormat(ImageFormat.PixelDataType.NORM_UNSIGNED_INTEGER, ImageFormat.PixelComponents.COLOR_RG, ImageFormat.ComponentOrder.RGBA, ImageFormat.BitDepth.PER_COMP_8, 1),
                    new OldDdsFmtMatch(DdsPixelFormatFlags.LUMINANCE | DdsPixelFormatFlags.ALPHAPIXELS, 8, 0xff, 0, 0, 0xff00, 0)
            )
    };


    private static class OldDdsFmtMatch {
        int flags;
        int bitDepth;
        int rBitmask;
        int gBitmask;
        int bBitmask;
        int aBitmask;
        int fourCC;

        OldDdsFmtMatch(int flags, int bitDepth, int rBitmask, int gBitmask, int bBitmask, int aBitmask, int fourCC) {
            this.flags = flags;
            this.bitDepth = bitDepth;
            this.rBitmask = rBitmask;
            this.gBitmask = gBitmask;
            this.bBitmask = bBitmask;
            this.aBitmask = aBitmask;
            this.fourCC = fourCC;
        }
    }


    private static class OldDdsFormatConv {
        ImageFormat.UncheckedImageFormat uncheckedImageFormat;
        OldDdsFmtMatch ddsFmt;

        OldDdsFormatConv(ImageFormat.UncheckedImageFormat uncheckedImageFormat, OldDdsFmtMatch ddsFmt) {
            this.uncheckedImageFormat = uncheckedImageFormat;
            this.ddsFmt = ddsFmt;
        }
    }


    private static boolean doesMatchFormat(OldDdsFmtMatch ddsFmt, DdsHeader ddsHeader) {
        if ((ddsHeader.ddsPixelFormat.flags & ddsFmt.flags) == 0) return false;

        if ((ddsFmt.flags & DdsPixelFormatFlags.FOURCC) != 0) {
            // None of the bit counts matter. Just check the fourCC
            if (ddsFmt.fourCC != ddsHeader.ddsPixelFormat.fourCC) return false;
        } else {
            // Check the bitcounts, not the fourCC.
            if (ddsHeader.ddsPixelFormat.rgbBitCount != ddsFmt.bitDepth) return false;
            if ((ddsFmt.rBitmask & ddsHeader.ddsPixelFormat.rBitMask) != ddsFmt.rBitmask) return false;
            if ((ddsFmt.gBitmask & ddsHeader.ddsPixelFormat.gBitMask) != ddsFmt.gBitmask) return false;
            if ((ddsFmt.bBitmask & ddsHeader.ddsPixelFormat.bBitMask) != ddsFmt.bBitmask) return false;
            if ((ddsFmt.aBitmask & ddsHeader.ddsPixelFormat.aBitMask) != ddsFmt.aBitmask) return false;
        }

        return true;
    }
} public class ImageSet {
    public static class Dimensions {
        public int numDimensions;  // The number of dimensions of an image. Can be 1, 2, or 3.
        public int width;          // The width of the image. Always valid.
        public int height;         // The height of the image. Only valid if numDimensions is 2 or 3.
        public int depth;          // The depth of the image. Only valid if numDimensions is 3.

        ////////////////////////////////
        public Dimensions() {
        }

        public Dimensions(Dimensions dimensions) {
            numDimensions = dimensions.numDimensions;
            width = dimensions.width;
            height = dimensions.height;
            depth = dimensions.depth;
        }

        ////////////////////////////////
        // Computes the number of rows of pixel data in the image.
        public int calcNumLines() {
            switch (numDimensions) {
                case 1:
                    return 1;

                case 2:
                    return height;

                case 3:
                    return depth * height;
            }
            // Should not be possible.
            return -1;
        }
    }

    public static class SingleImage {
        public SingleImage(ImageSet imageSet, int mipmapLevel, int arrayIx, int faceIx) {
            this.imageSet = imageSet;
            this.arrayIx = arrayIx;
            this.faceIx = faceIx;
            this.mipmapLevel = mipmapLevel;
        }

        ////////////////////////////////
        public Dimensions getDimensions() {
            return imageSet.getDimensions(mipmapLevel);
        }

        public ByteBuffer getImageData() {
            return imageSet.getImageData(mipmapLevel, arrayIx, faceIx);
        }

        public final int getSize() {
            return imageSet.getSize(mipmapLevel);
        }

        ////////////////////////////////
        private ImageSet imageSet;
        private int arrayIx, faceIx, mipmapLevel;
    }

    ////////////////////////////////
    ImageSet(ImageFormat imageFormat, Dimensions imageDimensions, int mipmapCount, int arrayCount, int faceCount,
             ArrayList<byte[]> imageData, int[] imageSizes) {
        format = imageFormat;
        dimensions = imageDimensions;
        this.imageData = imageData;
        this.imageSizes = imageSizes;
        this.arrayCount = arrayCount;
        this.mipmapCount = mipmapCount;
        this.faceCount = faceCount;
    }

    ////////////////////////////////
    public int getMipmapCount() {
        return mipmapCount;
    }


    public ImageFormat getFormat() {
        return format;
    }


    public Dimensions getDimensions() {
        return dimensions;
    }

    public Dimensions getDimensions(int mipmapLevel) {
        return Util.calcMipmapLevelDimensions(dimensions, mipmapLevel);
    }


    public SingleImage getImage(int mipmapLevel, int arrayIx, int faceIx) {
        return new SingleImage(this, mipmapLevel, arrayIx, faceIx);
    }

    ////////////////////////////////
    public int getArrayCount() {
        return arrayCount;
    }

    public int getFaceCount() {
        return faceCount;
    }


    public int getSize(int mipmap) {
        return imageSizes[mipmap];
    }


    public ByteBuffer getImageData(int mipmapLevel, int arrayIx, int faceIx) {
        byte[] imageData = this.imageData.get(mipmapLevel);
        int imageDataOffset = ((arrayIx * faceCount) + faceIx) * imageSizes[mipmapLevel];

        ByteBuffer imageDataBuffer = BufferUtils.createByteBuffer(imageSizes[mipmapLevel]);
        for (int i = imageDataOffset; i < imageDataOffset + imageSizes[mipmapLevel]; i++) {
            imageDataBuffer.put(imageData[i]);
        }
        imageDataBuffer.flip();
        return imageDataBuffer;
    }

    ////////////////////////////////
    private ImageFormat format;
    private Dimensions dimensions;

    private int[] imageSizes;
    private ArrayList<byte[]> imageData;

    private int arrayCount;
    private int faceCount;
    private int mipmapCount;
} public class TextureGenerator {
    public static class ForcedConvertFlags {
        public static int FORCE_SRGB_COLORSPACE_FMT = 0x0001;   // When possible, will force the format to use the sRGB colorspace. Does not cause erroring for formats that can't be sRGB, unless your GL implementation doesn't support sRGB.
        public static int FORCE_BC1_ALPHA_FMT = 0x0002;         // When used with a BC1 texture, will force the texture to have an alpha. Ignored otherwise.
        public static int FORCE_LUMINANCE_FMT = 0x0008;         // Red and RG textures will become luminance and luminance/alpha textures in all cases. Exceptions will be thrown if the GL implementation does not support those luminance/alpha formats (ie: is core).

        public static int FORCE_INTEGRAL_FMT = 0x0020;          // Image formats that contain normalized integers will be uploaded as non-normalized integers. Ignored for floating-point or compressed formats.
        public static int FORCE_SIGNED_FMT = 0x0040;            // Image formats that contain unsigned integers will be uploaded as signed integers. Ignored if the format is not an integer/integral format, or if it isn't BC4 or BC5 compressed.
        public static int FORCE_COLOR_RENDERABLE_FMT = 0x0080;  // NOT YET SUPPORTED! Will force the use of formats that are required to be valid render targets. This will add components if necessary, but it will throw if conversion would require fundamentally changing the basic format (from signed to unsigned, compressed textures, etc).

        public static int FORCE_ARRAY_TEXTURE = 0x0004;         // NOT YET SUPPORTED! The texture will be an array texture even if the depth is not present. Ignored for formats that can't be arrays. Will throw if array textures of that type are not supported (ie: cubemap arrays, 2D arrays for lesser hardware, etc).
        public static int USE_TEXTURE_STORAGE = 0x0100;         // If ARB_texture_storage or GL 4.2 is available, then texture storage functions will be used to create the textures. Otherwise regular glTex* functions will be used.
        public static int FORCE_TEXTURE_STORAGE = 0x0200;       // If ARB_texture_storage or GL 4.2 is available, then texture storage functions will be used to create the textures. Otherwise, an exception will be thrown.
        public static int USE_DSA = 0x0400;                     // If EXT_direct_state_access is available, then DSA functions will be used to create the texture. Otherwise, regular ones will be used.
        public static int FORCE_DSA = 0x0800;                   // If EXT_direct_state_access is available, then DSA functions will be used to create the texture. Otherwise, an exception will be thrown.
    }

    ////////////////////////////////
    public static int createTexture(ImageSet imageSet, int forceConvertBits) {
        int textureName = glGenTextures();

        try {
            createTexture(textureName, imageSet, forceConvertBits);
        } catch (Exception e) {
            e.printStackTrace();
            glDeleteTextures(textureName);
        }

        return textureName;
    }

    ////////////////////////////////
    public static class OpenGLPixelTransferParams {
        public int format;          // The GLenum format value of pixel transfer operations.
        public int type;            // The GLenum type value of pixel transfer operations.
        public int blockByteCount;  // The size in bytes for blocks in compressed formats. Necessary to compute the size of the pixel data.
    }


    public static int getInternalFormat(ImageFormat imageFormat, int forceConvertBits) {
        int internalFormat = getStandardOpenGLFormat(imageFormat, forceConvertBits);

        boolean convertToLA = useLAInsteadOfRG(forceConvertBits);

        // Convert any R or RG formats to L or LA formats.
        switch (internalFormat) {
            case GL_COMPRESSED_RED_RGTC1:
                return LARG_COMPRESSED_CONV(convertToLA, internalFormat, GL_COMPRESSED_LUMINANCE_LATC1_EXT);
            case GL_COMPRESSED_SIGNED_RED_RGTC1:
                return LARG_COMPRESSED_CONV(convertToLA, internalFormat, GL_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT);
            case GL_COMPRESSED_RG_RGTC2:
                return LARG_COMPRESSED_CONV(convertToLA, internalFormat, GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT);
            case GL_COMPRESSED_SIGNED_RG_RGTC2:
                return LARG_COMPRESSED_CONV(convertToLA, internalFormat, GL_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT);

            case GL_R8I:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE8I_EXT);
            case GL_R16I:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE16I_EXT);
            case GL_R32I:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE32I_EXT);
            case GL_RG8I:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA8I_EXT);
            case GL_RG16I:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA16I_EXT);
            case GL_RG32I:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA32I_EXT);

            case GL_R8UI:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE8UI_EXT);
            case GL_R16UI:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE16UI_EXT);
            case GL_R32UI:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE32UI_EXT);
            case GL_RG8UI:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA8UI_EXT);
            case GL_RG16UI:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA16UI_EXT);
            case GL_RG32UI:
                return LARG_INTEGRAL_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA32UI_EXT);

            case GL_R16F:
                return LARG_FLOAT_CONV(convertToLA, internalFormat, GL_LUMINANCE16F_ARB);
            case GL_R32F:
                return LARG_FLOAT_CONV(convertToLA, internalFormat, GL_LUMINANCE32F_ARB);
            case GL_RG16F:
                return LARG_FLOAT_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA16F_ARB);
            case GL_RG32F:
                return LARG_FLOAT_CONV(convertToLA, internalFormat, GL_LUMINANCE_ALPHA32F_ARB);

            case EXTTextureSnorm.GL_R8_SNORM:
                return LARG_SNORM_CONV(convertToLA, internalFormat, GL_LUMINANCE8_SNORM);
            case EXTTextureSnorm.GL_R16_SNORM:
                return LARG_SNORM_CONV(convertToLA, internalFormat, GL_LUMINANCE16_SNORM);
            case EXTTextureSnorm.GL_RG8_SNORM:
                return LARG_SNORM_CONV(convertToLA, internalFormat, GL_LUMINANCE8_ALPHA8_SNORM);
            case EXTTextureSnorm.GL_RG16_SNORM:
                return LARG_SNORM_CONV(convertToLA, internalFormat, GL_LUMINANCE16_ALPHA16_SNORM);

            case GL_R8:
                return LARG_NORM_CONV(convertToLA, internalFormat, GL_LUMINANCE8);
            case GL_R16:
                return LARG_NORM_CONV(convertToLA, internalFormat, GL_LUMINANCE16);
            case GL_RG8:
                return LARG_NORM_CONV(convertToLA, internalFormat, GL_LUMINANCE8_ALPHA8);
            case GL_RG16:
                return LARG_NORM_CONV(convertToLA, internalFormat, GL_LUMINANCE16_ALPHA16);
        }

        return internalFormat;
    }


    public static OpenGLPixelTransferParams getUploadFormatType(ImageFormat imageFormat, int forceConvertBits) {
        OpenGLPixelTransferParams params = new OpenGLPixelTransferParams();
        params.type = 0xFFFFFFFF;
        params.format = 0xFFFFFFFF;
        params.blockByteCount = 0;

        PixelDataType pixelDataType = getDataType(imageFormat, forceConvertBits);
        if (pixelDataType.ordinal() >= PixelDataType.NUM_UNCOMPRESSED_TYPES.ordinal()) {
            switch (pixelDataType) {
                case COMPRESSED_BC1:
                case COMPRESSED_UNSIGNED_BC4:
                case COMPRESSED_SIGNED_BC4:
                    params.blockByteCount = 8;
                    break;

                default:
                    params.blockByteCount = 16;
                    break;
            }

            // Provide reasonable parameters.
            params.format = GL_RGBA;
            params.type = GL_FLOAT;
            return params;
        }

        params.type = getOpenGLType(imageFormat, pixelDataType, forceConvertBits);
        params.format = getOpenGLFormat(imageFormat, pixelDataType, forceConvertBits);
        return params;
    }


    public static int getTextureType(ImageSet imageSet, int forceConvertBits) {
        Dimensions dims = imageSet.getDimensions();

        switch (dims.numDimensions) {
            case 1:
                // May be 1D or 1D array.
                if (isArrayTexture(imageSet, forceConvertBits)) {
                    return GL_TEXTURE_1D_ARRAY;
                } else {
                    return GL_TEXTURE_1D;
                }

            case 2:
                // 2D, 2D array, 2D cube, or 2D array cube.
                if (isArrayTexture(imageSet, forceConvertBits)) {
                    if (imageSet.getFaceCount() > 1) {
                        return GL_TEXTURE_CUBE_MAP_ARRAY;
                    } else {
                        return GL_TEXTURE_2D_ARRAY;
                    }
                } else {
                    if (imageSet.getFaceCount() > 1) {
                        return GL_TEXTURE_CUBE_MAP;
                    } else {
                        return GL_TEXTURE_2D;
                    }
                }

            case 3:
                // 3D.
                return GL_TEXTURE_3D;
        }

        return -1;
    }

    ////////////////////////////////
    private static int[] packedTypes = {
            GL_UNSIGNED_SHORT_5_6_5,          // BitDepth.PACKED_16_BIT_565
            GL_UNSIGNED_SHORT_5_5_5_1,        // BitDepth.PACKED_16_BIT_5551
            GL_UNSIGNED_SHORT_4_4_4_4,        // BitDepth.PACKED_16_BIT_4444
            GL_UNSIGNED_INT_8_8_8_8,          // BitDepth.PACKED_32_BIT_8888
            GL_UNSIGNED_INT_10_10_10_2,       // BitDepth.PACKED_32_BIT_1010102
            GL_UNSIGNED_INT_24_8,             // BitDepth.PACKED_32_BIT_248

            GL_UNSIGNED_SHORT_5_6_5_REV,      // BitDepth.PACKED_16_BIT_565_REV
            GL_UNSIGNED_SHORT_1_5_5_5_REV,    // BitDepth.PACKED_16_BIT_1555_REV
            GL_UNSIGNED_SHORT_4_4_4_4_REV,    // BitDepth.PACKED_16_BIT_4444_REV

            GL_UNSIGNED_INT_8_8_8_8_REV,      // BitDepth.PACKED_32_BIT_8888_REV
            GL_UNSIGNED_INT_2_10_10_10_REV,   // BitDepth.PACKED_32_BIT_2101010_REV
            GL_UNSIGNED_INT_10F_11F_11F_REV,  // BitDepth.PACKED_32_BIT_101111_REV
            GL_UNSIGNED_INT_5_9_9_9_REV,      // BitDepth.PACKED_32_BIT_5999_REV
    };

    // Starts from PixelComponents.COLOR_RGB. non-integral vs. integral.
    private static int[] bgraFormats = {
            GL_BGR, GL_BGR_INTEGER,    // PixelComponents.COLOR_RGB
            GL_BGRA, GL_BGRA_INTEGER,  // PixelComponents.COLOR_RGBX
            GL_BGRA, GL_BGRA_INTEGER,  // PixelComponents.COLOR_RGBA
            GL_BGR, GL_BGR_INTEGER,    // PixelComponents.COLOR_RGB_sRGB
            GL_BGRA, GL_BGRA_INTEGER,  // PixelComponents.COLOR_RGBX_sRGB
            GL_BGRA, GL_BGRA_INTEGER,  // PixelComponents.COLOR_RGBA_sRGB
    };

    // Non-integral vs. integral.
    private static int[] rgbaFormats = {
            GL_RED, GL_RED_INTEGER,    // PixelComponents.COLOR_RED
            GL_RG, GL_RG_INTEGER,      // PixelComponents.COLOR_RG
            GL_RGB, GL_RGB_INTEGER,    // PixelComponents.COLOR_RGB
            GL_RGBA, GL_RGBA_INTEGER,  // PixelComponents.COLOR_RGBX
            GL_RGBA, GL_RGBA_INTEGER,  // PixelComponents.COLOR_RGBA
            GL_RGB, GL_RGB_INTEGER,    // PixelComponents.COLOR_RGB_sRGB
            GL_RGBA, GL_RGBA_INTEGER,  // PixelComponents.COLOR_RGBX_sRGB
            GL_RGBA, GL_RGBA_INTEGER,  // PixelComponents.COLOR_RGBA_sRGB
    };

    // Non-integral vs. integral.
    private static int[] rgbaLuminanceFormats = {
            GL_LUMINANCE, GL_LUMINANCE_INTEGER_EXT,              // PixelComponents.COLOR_RED
            GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA_INTEGER_EXT,  // PixelComponents.COLOR_RG
            GL_RGB, GL_RGB_INTEGER,                              // PixelComponents.COLOR_RGB
            GL_RGBA, GL_RGBA_INTEGER,                            // PixelComponents.COLOR_RGBX
            GL_RGBA, GL_RGBA_INTEGER,                            // PixelComponents.COLOR_RGBA
            GL_RGB, GL_RGB_INTEGER,                              // PixelComponents.COLOR_RGB_sRGB
            GL_RGBA, GL_RGBA_INTEGER,                            // PixelComponents.COLOR_RGBX_sRGB
            GL_RGBA, GL_RGBA_INTEGER,                            // PixelComponents.COLOR_RGBA_sRGB
    };


    private static PixelDataType getDataType(ImageFormat imageFormat, int forceConvertBits) {
        boolean forceIntegral = (forceConvertBits & ForcedConvertFlags.FORCE_INTEGRAL_FMT) != 0;
        boolean forceSigned = (forceConvertBits & ForcedConvertFlags.FORCE_SIGNED_FMT) != 0;

        if (!forceIntegral && !forceSigned) return imageFormat.getPixelDataType();

        switch (imageFormat.getPixelDataType()) {
            case NORM_UNSIGNED_INTEGER:
                if (forceIntegral) {
                    if (forceSigned) {
                        return PixelDataType.SIGNED_INTEGRAL;
                    } else {
                        return PixelDataType.UNSIGNED_INTEGRAL;
                    }
                } else {
                    return PixelDataType.NORM_SIGNED_INTEGER;
                }

            case NORM_SIGNED_INTEGER:
                if (forceIntegral) return PixelDataType.SIGNED_INTEGRAL;

            case UNSIGNED_INTEGRAL:
                if (forceSigned) return PixelDataType.SIGNED_INTEGRAL;

            case COMPRESSED_UNSIGNED_BC4:
                if (forceSigned) return PixelDataType.COMPRESSED_SIGNED_BC4;
            case COMPRESSED_UNSIGNED_BC5:
                if (forceSigned) return PixelDataType.COMPRESSED_SIGNED_BC5;
        }

        return imageFormat.getPixelDataType();
    }


    private static int getOpenGLType(ImageFormat imageFormat, PixelDataType pixelDataType, int forceConvertBits) {
        switch (imageFormat.getBitDepth()) {
            case COMPRESSED:
                return 0xFFFFFFFF;

            case PER_COMP_8:
                if (isTypeSigned(pixelDataType)) {
                    return GL_BYTE;
                } else {
                    return GL_UNSIGNED_BYTE;
                }

            case PER_COMP_16:
                if (pixelDataType == PixelDataType.FLOAT) {
                    throwIfHalfFloatNotSupported();
                    return GL_HALF_FLOAT;
                } else {
                    if (isTypeSigned(pixelDataType)) {
                        return GL_SHORT;
                    } else {
                        return GL_UNSIGNED_SHORT;
                    }
                }

            case PER_COMP_32:
                if (pixelDataType == PixelDataType.FLOAT) {
                    throwIfFloatNotSupported();
                    return GL_FLOAT;
                } else {
                    if (isTypeSigned(pixelDataType)) {
                        return GL_INT;
                    } else {
                        return GL_UNSIGNED_INT;
                    }
                }

            default:
                int typeIndex = imageFormat.getBitDepth().ordinal() - BitDepth.NUM_PER_COMPONENT.ordinal();
                if (!((0 <= typeIndex) && (typeIndex < packedTypes.length))) {
                    throw new RuntimeException("Couldn't get the GL type field, due to the bitdepth " +
                            "being outside the packed type array.");
                }

                int testType = packedTypes[typeIndex];
                // Test for implemented features.
                switch (testType) {
                    case GL_UNSIGNED_INT_10F_11F_11F_REV:
                        throwIfPackedFloatNotSupported();
                        break;

                    case GL_UNSIGNED_INT_5_9_9_9_REV:
                        throwIfSharedExpNotSupported();
                        break;
                }
                return testType;
        }
    }

    private static int getOpenGLFormat(ImageFormat imageFormat, PixelDataType pixelDataType, int forceConvertBits) {
        if (imageFormat.getPixelComponents() == PixelComponents.DEPTH) {
            throwIfDepthNotSupported();
            return GL_DEPTH_COMPONENT;
        }

        if (imageFormat.getPixelComponents() == PixelComponents.DEPTH_X) {
            throwIfDepthStencilNotSupported();
            return GL_DEPTH_STENCIL;
        }

        if (isTypeIntegral(pixelDataType)) {
            throwIfIntegralNotSupported();
        }

        int arrayOffset = isTypeIntegral(pixelDataType) ? 1 : 0;

        if (imageFormat.getComponentOrder() == ComponentOrder.BGRA) {
            int formatIndex = imageFormat.getPixelComponents().ordinal() - PixelComponents.COLOR_RGB.ordinal();
            formatIndex *= 2;

            if (!((0 <= formatIndex) && (formatIndex < bgraFormats.length))) {
                throw new RuntimeException("Couldn't get the GL format field with ORDER_BGRA, due to " +
                        "the order being outside the bgraFormats array.");
            }

            return bgraFormats[formatIndex];
        } else {
            int formatIndex = imageFormat.getPixelComponents().ordinal();
            formatIndex *= 2;

            if (!((0 <= formatIndex) && (formatIndex < rgbaFormats.length))) {
                throw new RuntimeException("Couldn't get the GL format field with ORDER_RGBA, due to " +
                        "the order being outside the rgbaFormats array.");
            }

            boolean isRGChannel = getComponentCount(imageFormat, forceConvertBits) < 3;

            if (useLAInsteadOfRG(forceConvertBits)) {
                if (isRGChannel && arrayOffset == 1) {
                    throwIfEXT_IntegralNotSupported();
                }
                if (isRGChannel) {
                    throwIfLANotSupported();
                }
                return rgbaLuminanceFormats[formatIndex];
            } else {
                if (isRGChannel) {
                    throwIfRGNotSupported();
                }
                return rgbaFormats[formatIndex];
            }
        }
    }


    private static boolean isTypeSigned(PixelDataType pixelDataType) {
        PixelDataType[] signedIntegerFormats = {PixelDataType.SIGNED_INTEGRAL, PixelDataType.NORM_SIGNED_INTEGER};
        return Arrays.asList(signedIntegerFormats).contains(pixelDataType);
    }

    private static boolean isTypeIntegral(PixelDataType pixelDataType) {
        PixelDataType[] integralIntegerFormats = {PixelDataType.SIGNED_INTEGRAL, PixelDataType.UNSIGNED_INTEGRAL};
        return Arrays.asList(integralIntegerFormats).contains(pixelDataType);
    }


    private static int getComponentCount(ImageFormat imageFormat, int forceConvertBits) {
        // TODO: Forceconv.
        PixelComponents[] twoCompFormats = {PixelComponents.COLOR_RG, PixelComponents.DEPTH_X};
        PixelComponents[] threeCompFormats = {PixelComponents.COLOR_RGB, PixelComponents.COLOR_RGB_SRGB};
        PixelComponents[] fourCompFormats = {PixelComponents.COLOR_RGBX, PixelComponents.COLOR_RGBA,
                PixelComponents.COLOR_RGBX_SRGB, PixelComponents.COLOR_RGBA_SRGB};

        if (Arrays.asList(twoCompFormats).contains(imageFormat.getPixelComponents())) return 2;
        if (Arrays.asList(threeCompFormats).contains(imageFormat.getPixelComponents())) return 3;
        if (Arrays.asList(fourCompFormats).contains(imageFormat.getPixelComponents())) return 4;
        return 1;
    }

    private static boolean useLAInsteadOfRG(int forceConvertBits) {
        if ((forceConvertBits & ForcedConvertFlags.FORCE_LUMINANCE_FMT) != 0) return true;
        try {
            throwIfRGNotSupported();
        } catch (RuntimeException e) {
            return true;
        }
        return false;
    }


    private static boolean isArrayTexture(ImageSet imageSet, int forceConvertBits) {
        // No such thing as 3D array textures.
        return imageSet.getDimensions().numDimensions != 3 && ((forceConvertBits & ForcedConvertFlags.FORCE_ARRAY_TEXTURE) != 0 || imageSet.getArrayCount() > 1);
    }

    ////////////////////////////////
    private static void throwIfS3TCNotSupported() {
        if (!GL.getCapabilities().GL_EXT_texture_compression_s3tc) {
            throw new RuntimeException("S3TC not supported.");
        }
    }

    private static void throwIfLATCNotSupported() {
        if (!GL.getCapabilities().GL_EXT_texture_compression_latc) {
            throw new RuntimeException("LATC not supported.");
        }
    }

    private static void throwIfRGTCNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_ARB_texture_compression_rgtc || !GL.getCapabilities().GL_EXT_texture_compression_rgtc) {
                throw new RuntimeException("RGTC, part of GL 3.0 and above, is not supported.");
            }
        }
    }

    private static void throwIfBPTCNotSupported() {
        if (!GL.getCapabilities().GL_ARB_texture_compression_bptc) {
            throw new RuntimeException("PBTC not supported.");
        }
    }

    private static void throwIfSRGBNotSupported() {
        if (!GL.getCapabilities().OpenGL21) {
            if (!GL.getCapabilities().GL_EXT_texture_sRGB) {
                throw new RuntimeException("sRGB textures not supported.");
            }
        }
    }

    private static void throwIfEXT_SRGBNotSupported() {
        if (!GL.getCapabilities().GL_EXT_texture_sRGB) {
            // TODO temporary "fix" for OS X, needs rework
            if (Platform.get() != Platform.MACOSX) {
                throw new RuntimeException("sRGB and S3TC textures not supported.");
            }
        }
    }

    private static void throwIfSharedExpNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_EXT_texture_shared_exponent) {
                throw new RuntimeException("Shared exponent texture format not supported.");
            }
        }
    }

    private static void throwIfFloatNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_ARB_texture_float) {
                throw new RuntimeException("Float textures not supported.");
            }
        }
    }

    private static void throwIfEXT_FloatNotSupported() {
        if (!GL.getCapabilities().GL_ARB_texture_float) {
            throw new RuntimeException("ARB Float textures not supported.");
        }
    }

    private static void throwIfHalfFloatNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_ARB_half_float_pixel) {
                throw new RuntimeException("Half floats textures not supported.");
            }
        }
    }

    private static void throwIfSnormNotSupported() {
        if (!GL.getCapabilities().OpenGL31) {
            if (!GL.getCapabilities().GL_EXT_texture_snorm) {
                throw new RuntimeException("Signed normalized textures not supported.");
            }
        }
    }

    private static void throwIfEXT_SnormNotSupported() {
        if (!GL.getCapabilities().GL_EXT_texture_snorm) {
            throw new RuntimeException("Signed normalized texture extension not supported.");
        }
    }

    private static void throwIfPackedFloatNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_EXT_packed_float) {
                throw new RuntimeException("Packed 11, 11, 10 float textures not supported.");
            }
        }
    }

    private static void throwIfIntegralNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_EXT_texture_integer) {
                throw new RuntimeException("Integral textures not supported.");
            }
        }
    }

    private static void throwIfEXT_IntegralNotSupported() {
        if (!GL.getCapabilities().GL_EXT_texture_integer) {
            throw new RuntimeException("Integral texture extension not supported.");
        }
    }

    private static void throwIfRGNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_ARB_texture_rg) {
                throw new RuntimeException("RG textures not supported.");
            }
        }
    }

    private static void throwIfLANotSupported() {
        if (!GL.getCapabilities().OpenGL30) return;

        if (!GL.getCapabilities().OpenGL32) {
            if (!GL.getCapabilities().GL_ARB_compatibility) {
                throw new RuntimeException("Core OpenGL contexts cannot use Luminance/alpha.");
            }
        } else {
            int profileMask = glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);

            if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0) {
                throw new RuntimeException("Core OpenGL contexts cannot use Luminance/alpha.");

            }
        }
    }

    private static void throwIfForceRendertarget(int forceConvertBits) {
        if ((forceConvertBits & ForcedConvertFlags.FORCE_COLOR_RENDERABLE_FMT) != 0) {
            throw new RuntimeException("The image format cannot be forced to be a renderable format without compromising the data.");
        }
    }

    private static void throwIfDepthNotSupported() {
        if (!GL.getCapabilities().OpenGL14) {  // Yes, really. Depth textures are old.
            if (!GL.getCapabilities().GL_ARB_depth_texture) {
                throw new RuntimeException("Depth textures not supported.");
            }
        }
    }

    private static void throwIfDepthStencilNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_EXT_packed_depth_stencil || !GL.getCapabilities().GL_ARB_framebuffer_object) {
                throw new RuntimeException("Depth/stencil textures not supported.");
            }
        }
    }

    private static void throwIfDepthFloatNotSupported() {
        if (!GL.getCapabilities().OpenGL30) {
            if (!GL.getCapabilities().GL_NV_depth_buffer_float) {
                throw new RuntimeException("Floating-point depth buffers not supported.");
            }
        }
    }

    ////////////////////////////////
    // Ordered by BaseDataFormat*2. The first is 16-bit, the second is 32-bit.
    private static int[] floatFormats = {
            GL_R16F,
            GL_R32F,
            GL_RG16F,
            GL_RG32F,
            GL_RGB16F,
            GL_RGB32F,
            GL_RGB16F,
            GL_RGB32F,
            GL_RGBA16F,
            GL_RGBA32F,
            0,                      // sRGB
            0,                      // sRGB
            0,                      // sRGB
            0,                      // sRGB
            0,                      // sRGB
            0,                      // sRGB
            0,                      // 16-bit float depth buffer.
            GL_DEPTH_COMPONENT32F,
            0,                      // UNorm+stencil.
    };

    // Ordered by number of components * size (8, 16=>0, 1, 2).
    private static int[] signedNormFormats = {
            EXTTextureSnorm.GL_R8_SNORM, EXTTextureSnorm.GL_R16_SNORM,
            EXTTextureSnorm.GL_RG8_SNORM, EXTTextureSnorm.GL_RG16_SNORM,
            EXTTextureSnorm.GL_RGB8_SNORM, EXTTextureSnorm.GL_RGB16_SNORM,
            EXTTextureSnorm.GL_RGBA8_SNORM, EXTTextureSnorm.GL_RGBA16_SNORM,
    };

    // Ordered by number of components * size (8, 16, 32=>0, 1, 2).
    private static int[] signIntegralFormats = {
            GL_R8I, GL_R16I, GL_R32I,
            GL_RG8I, GL_RG16I, GL_RG32I,
            GL_RGB8I, GL_RGB16I, GL_RGB32I,
            GL_RGBA8I, GL_RGBA16I, GL_RGBA32I,
    };

    // Ordered by number of components * size (8, 16, 32=>0, 1, 2).
    private static int[] unsignIntegralFormats = {
            GL_R8UI, GL_R16UI, GL_R32UI,
            GL_RG8UI, GL_RG16UI, GL_RG32UI,
            GL_RGB8UI, GL_RGB16UI, GL_RGB32UI,
            GL_RGBA8UI, GL_RGBA16UI, GL_RGBA32UI,
    };


    private static int getStandardOpenGLFormat(ImageFormat imageFormat, int forceConvertBits) {
        PixelDataType pixelDataType = getDataType(imageFormat, forceConvertBits);

        switch (pixelDataType) {
            case NORM_UNSIGNED_INTEGER:
                // Only 16-bit for non Depth_x.
                if (imageFormat.getPixelComponents() == PixelComponents.DEPTH) {
                    throwIfDepthNotSupported();
                    return GL_DEPTH_COMPONENT16;
                }

                // Only 24x8 for this.
                if (imageFormat.getPixelComponents() == PixelComponents.DEPTH_X) {
                    throwIfDepthStencilNotSupported();
                    return GL_DEPTH24_STENCIL8;
                }

                // Color formats.
                if (isSRGBFormat(imageFormat, forceConvertBits)) {
                    throwIfSRGBNotSupported();

                    if (getComponentCount(imageFormat, forceConvertBits) == 3) {
                        return GL_SRGB8;
                    } else {
                        return GL_SRGB8_ALPHA8;
                    }
                }

                switch (imageFormat.getBitDepth()) {
                    case PER_COMP_8: {
                        int components[] = {GL_R8, GL_RG8, GL_RGB8, GL_RGBA8};
                        int numComponents = getComponentCount(imageFormat, forceConvertBits);
                        return components[numComponents - 1];
                    }

                    case PER_COMP_16: {
                        int components[] = {GL_R16, GL_RG16, GL_RGB16, GL_RGBA16};
                        int numComponents = getComponentCount(imageFormat, forceConvertBits);
                        return components[numComponents - 1];
                    }

                    case PACKED_16_BIT_565:
                    case PACKED_16_BIT_565_REV:
                        return GL_RGB8;

                    case PACKED_16_BIT_5551:
                    case PACKED_16_BIT_1555_REV:
                        return GL_RGB5_A1;

                    case PACKED_16_BIT_4444:
                    case PACKED_16_BIT_4444_REV:
                        return GL_RGBA4;

                    case PACKED_32_BIT_8888:
                    case PACKED_32_BIT_8888_REV:
                        return GL_RGBA8;

                    case PACKED_32_BIT_1010102:
                    case PACKED_32_BIT_2101010_REV:
                        return GL_RGB10_A2;
                }

                throw new RuntimeException("Unisgned normalize integer doesn't match accepted bitdepths.");

            case NORM_SIGNED_INTEGER: {
                throwIfSnormNotSupported();
                throwIfForceRendertarget(forceConvertBits);

                int numComponents = getComponentCount(imageFormat, forceConvertBits);
                int compSize = perComponentSize(imageFormat, forceConvertBits);
                compSize /= 16; // map to 0, 1.

                int index = ((numComponents - 1) * 2) + compSize;
                return signedNormFormats[index];
            }

            case UNSIGNED_INTEGRAL:
            case SIGNED_INTEGRAL: {
                throwIfIntegralNotSupported();

                int numComponents = getComponentCount(imageFormat, forceConvertBits);
                int compSize = perComponentSize(imageFormat, forceConvertBits);
                compSize /= 16; // map to 0, 1, 2.

                int index = ((numComponents - 1) * 3) + compSize;

                if (pixelDataType == PixelDataType.SIGNED_INTEGRAL) {
                    return signIntegralFormats[index];
                } else {
                    return unsignIntegralFormats[index];
                }
            }

            case FLOAT: {
                throwIfFloatNotSupported();

                if (imageFormat.getBitDepth().ordinal() < BitDepth.NUM_PER_COMPONENT.ordinal()) {
                    int offset = 0;
                    if (imageFormat.getBitDepth() == BitDepth.PER_COMP_32) {
                        offset = 1;
                    } else {
                        throwIfHalfFloatNotSupported();
                    }

                    if (imageFormat.getPixelComponents() == PixelComponents.DEPTH) {
                        throwIfDepthFloatNotSupported();
                    }

                    return throwInvalidFormatIfZero(floatFormats[(2 * imageFormat.getPixelComponents().ordinal()) + offset]);
                } else {
                    // Only one packed format.
                    throwIfPackedFloatNotSupported();
                    return GL_R11F_G11F_B10F;
                }
            }

            case SHARED_EXP_FLOAT:
                throwIfSharedExpNotSupported();
                throwIfForceRendertarget(forceConvertBits);
                return GL_RGB9_E5;

            case COMPRESSED_BC1: {
                throwIfS3TCNotSupported();

                if (isSRGBFormat(imageFormat, forceConvertBits)) {
                    throwIfEXT_SRGBNotSupported();

                    if ((forceConvertBits & ForcedConvertFlags.FORCE_BC1_ALPHA_FMT) != 0) {
                        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
                    } else {
                        if (getComponentCount(imageFormat, forceConvertBits) == 3) {
                            return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
                        } else {
                            return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
                        }
                    }
                } else {
                    if ((forceConvertBits & ForcedConvertFlags.FORCE_BC1_ALPHA_FMT) != 0) {
                        return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
                    } else {
                        if (getComponentCount(imageFormat, forceConvertBits) == 3) {
                            return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
                        } else {
                            return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
                        }
                    }
                }
            }

            case COMPRESSED_BC2:
                throwIfS3TCNotSupported();

                if (isSRGBFormat(imageFormat, forceConvertBits)) {
                    throwIfEXT_SRGBNotSupported();
                    return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
                } else {
                    return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
                }

            case COMPRESSED_BC3:
                throwIfS3TCNotSupported();

                if (isSRGBFormat(imageFormat, forceConvertBits)) {
                    throwIfEXT_SRGBNotSupported();
                    return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
                } else {
                    return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
                }

            case COMPRESSED_UNSIGNED_BC4:
                return GL_COMPRESSED_RED_RGTC1;

            case COMPRESSED_SIGNED_BC4:
                return GL_COMPRESSED_SIGNED_RED_RGTC1;

            case COMPRESSED_UNSIGNED_BC5:
                throw new RuntimeException("No such constant in LWJGL :(");
                //return GL_COMPRESSED_RG_RGTC2;

            case COMPRESSED_SIGNED_BC5:
                throw new RuntimeException("No such constant in LWJGL :(");
                //return GL_COMPRESSED_SIGNED_RG_RGTC2;

            case COMPRESSED_UNSIGNED_BC6H:
                throwIfBPTCNotSupported();
                return GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB;

            case COMPRESSED_SIGNED_BC6H:
                throwIfBPTCNotSupported();
                return GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB;

            case COMPRESSED_BC7:
                throwIfBPTCNotSupported();

                if (isSRGBFormat(imageFormat, forceConvertBits)) {
                    return GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB;
                } else {
                    return GL_COMPRESSED_RGBA_BPTC_UNORM_ARB;
                }
        }

        throw new RuntimeException("???");
    }


    private static boolean isSRGBFormat(ImageFormat imageFormat, int forceConvertBits) {
        PixelComponents[] srgbFormats = {PixelComponents.COLOR_RGB_SRGB, PixelComponents.COLOR_RGBX_SRGB, PixelComponents.COLOR_RGBA_SRGB};

        if (Arrays.asList(srgbFormats).contains(imageFormat.getPixelComponents())) return true;
        if ((forceConvertBits & ForcedConvertFlags.FORCE_SRGB_COLORSPACE_FMT) == 0) return false;

        PixelDataType[] srgbTypes = {PixelDataType.NORM_UNSIGNED_INTEGER, PixelDataType.COMPRESSED_BC1,
                PixelDataType.COMPRESSED_BC2, PixelDataType.COMPRESSED_BC3, PixelDataType.COMPRESSED_BC7};

        if (Arrays.asList(srgbTypes).contains(imageFormat.getPixelDataType())) {
            if (imageFormat.getPixelDataType() != PixelDataType.NORM_UNSIGNED_INTEGER) return true;
        } else {
            return false;
        }

        // Unsigned normalized integers. Check for RGB or RGBA components.
        PixelComponents convertableFormats[] = {PixelComponents.COLOR_RGB, PixelComponents.COLOR_RGBX, PixelComponents.COLOR_RGBA};
        return Arrays.asList(convertableFormats).contains(imageFormat.getPixelComponents());
    }

    private static int perComponentSize(ImageFormat imageFormat, int forceConvertBits) {
        // TODO: Forceconv.
        switch (imageFormat.getBitDepth()) {
            case PER_COMP_8:
                return 8;
            case PER_COMP_16:
                return 16;
            case PER_COMP_32:
                return 32;
            default:
                return -1;
        }
    }

    private static int throwInvalidFormatIfZero(int input) {
        if (input == 0) {
            throw new RuntimeException();
        }
        return input;
    }


    private static int LARG_COMPRESSED_CONV(boolean convertToLA, int oldformat, int newFormat) {
        if (convertToLA) {
            throwIfLATCNotSupported();
            return newFormat;
        } else {
            throwIfRGTCNotSupported();
        }
        return oldformat;
    }

    private static int LARG_INTEGRAL_CONV(boolean convertToLA, int oldformat, int newFormat) {
        if (convertToLA) {
            throwIfEXT_IntegralNotSupported();
            return newFormat;
        } else {
            throwIfRGNotSupported();
        }
        return oldformat;
    }

    private static int LARG_FLOAT_CONV(boolean convertToLA, int oldformat, int newFormat) {
        if (convertToLA) {
            throwIfEXT_FloatNotSupported();
            return newFormat;
        } else {
            throwIfRGNotSupported();
        }
        return oldformat;
    }

    private static int LARG_SNORM_CONV(boolean convertToLA, int oldformat, int newFormat) {
        if (convertToLA) {
            throwIfEXT_SnormNotSupported();
            return newFormat;
        } else {
            throwIfRGNotSupported();
        }
        return oldformat;
    }

    private static int LARG_NORM_CONV(boolean convertToLA, int oldformat, int newFormat) {
        if (convertToLA) {
            throwIfLANotSupported();
            return newFormat;
        } else {
            throwIfRGNotSupported();
        }
        return oldformat;
    }

    ////////////////////////////////
    private static class TextureBinder {
        void bind(int textureTarget, int texture) {
            this.texture = texture;
            texTarget = textureTarget;
            glBindTexture(texTarget, this.texture);
        }

        ////////////////////////////////
        private int texture;
        private int texTarget;
    }


    private static void createTexture(int textureName, ImageSet imageSet, int forceConvertBits) {
        if ((forceConvertBits & ForcedConvertFlags.FORCE_TEXTURE_STORAGE) != 0) {
            if (!isTextureStorageSupported()) {
                throw new RuntimeException("The current OpenGL implementation does not support ARB_texture_storage or GL 4.2 or above.");
            }

            forceConvertBits |= ForcedConvertFlags.USE_TEXTURE_STORAGE;
        }

        if ((forceConvertBits & ForcedConvertFlags.USE_TEXTURE_STORAGE) != 0) {
            if (!isTextureStorageSupported()) {
                forceConvertBits &= ~ForcedConvertFlags.USE_TEXTURE_STORAGE;
            }
        }

        if ((forceConvertBits & ForcedConvertFlags.FORCE_DSA) != 0) {
            if (!isDirectStateAccessSupported()) {
                throw new RuntimeException("The current OpenGL implementation does not support EXT_direct_state_access.");
            }

            forceConvertBits |= ForcedConvertFlags.USE_DSA;
        }

        if ((forceConvertBits & ForcedConvertFlags.USE_DSA) != 0) {
            if (!isDirectStateAccessSupported()) {
                forceConvertBits &= ~ForcedConvertFlags.USE_DSA;
            }
        }

        ImageFormat imageFormat = imageSet.getFormat();
        int internalFormat = getInternalFormat(imageFormat, forceConvertBits);
        OpenGLPixelTransferParams upload = getUploadFormatType(imageFormat, forceConvertBits);

        switch (getTextureType(imageSet, forceConvertBits)) {
            case GL_TEXTURE_2D:
                build2DTexture(textureName, imageSet, forceConvertBits, internalFormat, upload);
                break;

            default:
                throw new RuntimeException("Not implemented.");
        }
    }


    private static boolean isTextureStorageSupported() {
        return GL.getCapabilities().OpenGL42 && GL.getCapabilities().GL_ARB_texture_storage;
    }

    private static boolean isDirectStateAccessSupported() {
        return GL.getCapabilities().GL_EXT_direct_state_access;
    }


    private static void build2DTexture(int textureName, ImageSet imageSet, int forceConvertBits, int internalFormat,
                                       OpenGLPixelTransferParams upload) {
        setupUploadState(imageSet.getFormat(), forceConvertBits);
        TextureBinder textureBinder = new TextureBinder();

        if ((forceConvertBits & ForcedConvertFlags.USE_DSA) == 0) {
            textureBinder.bind(GL_TEXTURE_2D, textureName);
            textureName = 0;
        }

        int numMipmaps = imageSet.getMipmapCount();
        texStorageBase(GL_TEXTURE_2D, forceConvertBits, imageSet.getDimensions(), numMipmaps, internalFormat, upload, textureName);

        for (int mipmapLevel = 0; mipmapLevel < numMipmaps; mipmapLevel++) {
            Dimensions imageDimensions = imageSet.getDimensions(mipmapLevel);
            ByteBuffer imageDataBuffer = imageSet.getImageData(mipmapLevel, 0, 0);
            texSubImage(textureName, GL_TEXTURE_2D, mipmapLevel, internalFormat, imageDimensions, upload, imageDataBuffer, imageSet.getSize(mipmapLevel));
        }

        finalizeTexture(textureName, GL_TEXTURE_2D, imageSet);
    }

    private static void setupUploadState(ImageFormat imageFormat, int forceConvertBits) {
        glPixelStorei(GL_UNPACK_SWAP_BYTES, GL_FALSE);
        glPixelStorei(GL_UNPACK_LSB_FIRST, GL_FALSE);
        glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
        glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
        glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
        glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
        glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
        glPixelStorei(GL_UNPACK_ALIGNMENT, imageFormat.getLineAlignment());
    }


    // Works for just 1D/2D/3D.
    private static void texStorageBase(int textureTarget, int forceConvertBits, Dimensions imageDimensions, int numMipmaps,
                                       int internalFormat, OpenGLPixelTransferParams upload, int textureName) {
        if ((forceConvertBits & ForcedConvertFlags.USE_TEXTURE_STORAGE) != 0) {
            throw new RuntimeException("Not implemented.");
        } else {
            manTexStorageBase(textureName, textureTarget, imageDimensions, numMipmaps, internalFormat, upload);
        }
    }

    // Only works for TEXTURE_1D, 2D, and 3D.
    // DSA-style
    private static void manTexStorageBase(int texture, int textureTarget, Dimensions imageDimensions, int numMipmaps,
                                          int internalFormat, OpenGLPixelTransferParams upload) {
        // Zero means bound, so no DSA.
        if (texture == 0) {
            manTexStorageBase(textureTarget, imageDimensions, numMipmaps, internalFormat, upload);
            return;
        }
        throw new RuntimeException("Not implemented.");
    }

    private static void manTexStorageBase(int textureTarget, Dimensions imageDimensions, int numMipmaps,
                                          int internalFormat, OpenGLPixelTransferParams upload) {
        for (int mipmap = 0; mipmap < numMipmaps; mipmap++) {
            Dimensions levelDims = Util.calcMipmapLevelDimensions(imageDimensions, mipmap);
            switch (imageDimensions.numDimensions) {
                case 1:
                    throw new RuntimeException("Not implemented.");

                case 2:
                    glTexImage2D(textureTarget, mipmap, internalFormat, levelDims.width, levelDims.height, 0,
                            upload.format, upload.type, (ByteBuffer) null);
                    break;

                case 3:
                    throw new RuntimeException("Not implemented.");
            }
        }
    }


    private static void texSubImage(int texture, int textureTarget, int mipmapLevel, int internalFormat, Dimensions imageDimensions,
                                    OpenGLPixelTransferParams upload, ByteBuffer imageData, int imageSize) {
        // Zero means bound, so no DSA.
        if (texture == 0) {
            texSubImage(textureTarget, mipmapLevel, internalFormat, imageDimensions, upload, imageData, imageSize);
            return;
        }
        throw new RuntimeException("Not implemented.");
    }

    private static void texSubImage(int textureTarget, int mipmapLevel, int internalFormat, Dimensions imageDimensions,
                                    OpenGLPixelTransferParams upload, ByteBuffer imageData, int imageSize) {
        switch (imageDimensions.numDimensions) {
            case 1:
                if (upload.blockByteCount != 0) {
                    glCompressedTexSubImage1D(textureTarget, mipmapLevel, 0, imageDimensions.width, internalFormat, imageData);
                } else {
                    glTexSubImage1D(textureTarget, mipmapLevel, 0, imageDimensions.width, upload.format, upload.type, imageData);
                }
                break;

            case 2:
                if (upload.blockByteCount != 0) {
                    glCompressedTexSubImage2D(textureTarget, mipmapLevel, 0, 0, imageDimensions.width,
                            imageDimensions.height, internalFormat, imageData);
                } else {
                    glTexSubImage2D(textureTarget, mipmapLevel, 0, 0, imageDimensions.width, imageDimensions.height,
                            upload.format, upload.type, imageData);
                }
                break;

            case 3:
                if (upload.blockByteCount != 0) {
                    glCompressedTexSubImage3D(textureTarget, mipmapLevel, 0, 0, 0, imageDimensions.width,
                            imageDimensions.height, imageDimensions.depth, internalFormat, imageData);
                } else {
                    glTexSubImage3D(textureTarget, mipmapLevel, 0, 0, 0, imageDimensions.width, imageDimensions.height,
                            imageDimensions.depth, upload.format, upload.type, imageData);
                }
                break;
        }
    }


    private static void finalizeTexture(int texture, int textureTarget, ImageSet imageSet) {
        // Zero means bound, so no DSA.
        if (texture == 0) {
            finalizeTexture(textureTarget, imageSet);
            return;
        }
        throw new RuntimeException("Not implemented.");
    }

    // Texture must be bound to the target.
    private static void finalizeTexture(int textureTarget, ImageSet imageSet) {
        int numMipmaps = imageSet.getMipmapCount();

        glTexParameteri(textureTarget, GL_TEXTURE_BASE_LEVEL, 0);
        glTexParameteri(textureTarget, GL_TEXTURE_MAX_LEVEL, numMipmaps - 1);

        // Ensure the texture is texture-complete.
        ImageFormat imageFormat = imageSet.getFormat();
        if (isTypeIntegral(imageFormat.getPixelDataType())) {
            glTexParameteri(textureTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(textureTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        } else {
            glTexParameteri(textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        }
    }
} public enum MouseButtons {
    MB_LEFT_BTN,
    MB_RIGHT_BTN,
    MB_MIDDLE_BTN
} public static class ViewData {
    public Vector3f targetPos;
    public Quaternionf orient;
    public float radius;
    public float degSpinRotation;

    public ViewData(Vector3f targetPos, Quaternionf orient, float radius, float degSpinRotation) {
        this.targetPos = targetPos;
        this.orient = orient;
        this.radius = radius;
        this.degSpinRotation = degSpinRotation;
    }

    public ViewData(ViewData viewData) {
        targetPos = new Vector3f(viewData.targetPos);
        orient = new Quaternionf(viewData.orient);
        radius = viewData.radius;
        degSpinRotation = viewData.degSpinRotation;
    }
} public static class ViewPole extends ViewProvider {
    private enum RotateMode {
        RM_DUAL_AXIS_ROTATE,
        RM_BIAXIAL_ROTATE,
        RM_XZ_AXIS_ROTATE,
        RM_Y_AXIS_ROTATE,
        RM_SPIN_VIEW_AXIS
    }

    private enum TargetOffsetDir {
        DIR_UP,
        DIR_DOWN,
        DIR_FORWARD,
        DIR_BACKWARD,
        DIR_RIGHT,
        DIR_LEFT
    }

    private ViewData currView;
    private ViewScale viewScale;

    private ViewData initialView;
    private MouseButtons actionButton;
    private boolean rightKeyboardCtrls;

    // Used when rotating.
    private boolean isDragging;
    private RotateMode rotateMode;

    private float degStarDragSpin;
    private Vector2i startDragMouseLoc;
    private Quaternionf startDragOrient;

    private Vector3f offsets[] = {
            new Vector3f(0.0f, 1.0f, 0.0f),
            new Vector3f(0.0f, -1.0f, 0.0f),
            new Vector3f(0.0f, 0.0f, -1.0f),
            new Vector3f(0.0f, 0.0f, 1.0f),
            new Vector3f(1.0f, 0.0f, 0.0f),
            new Vector3f(-1.0f, 0.0f, 0.0f)
    };

    ////////////////////////////////
    public ViewPole(ViewData initialView, ViewScale viewScale) {
        this(initialView, viewScale, MouseButtons.MB_LEFT_BTN, false);
    }

    public ViewPole(ViewData initialView, ViewScale viewScale, MouseButtons actionButton) {
        this(initialView, viewScale, actionButton, false);
    }

    public ViewPole(ViewData initialView, ViewScale viewScale, MouseButtons actionButton, boolean rightKeyboardCtrls) {
        currView = new ViewData(initialView);
        this.viewScale = viewScale;
        this.initialView = initialView;
        this.actionButton = actionButton;
        this.rightKeyboardCtrls = rightKeyboardCtrls;
    }

    ////////////////////////////////
    @Override
    public void mouseMove(Vector2i position) {
        if (isDragging) {
            onDragRotate(position);
        }
    }

    @Override
    public void mouseClick(MouseButtons button, boolean isPressed, MouseModifiers modifiers, Vector2i position) {
        if (isPressed) {
            // Ignore all other button presses when dragging.
            if (!isDragging) {
                if (button == actionButton) {
                    if (modifiers == MouseModifiers.MM_KEY_CTRL) {
                        beginDragRotate(position, RotateMode.RM_BIAXIAL_ROTATE);
                    } else if (modifiers == MouseModifiers.MM_KEY_ALT) {
                        beginDragRotate(position, RotateMode.RM_SPIN_VIEW_AXIS);
                    } else {
                        beginDragRotate(position, RotateMode.RM_DUAL_AXIS_ROTATE);
                    }
                }
            }
        } else {
            // Ignore all other button releases when not dragging
            if (isDragging) {
                if (button == actionButton) {
                    if (rotateMode == RotateMode.RM_DUAL_AXIS_ROTATE || rotateMode == RotateMode.RM_SPIN_VIEW_AXIS
                            || rotateMode == RotateMode.RM_BIAXIAL_ROTATE) {
                        endDragRotate(position);
                    }
                }
            }
        }
    }

    @Override
    public void mouseWheel(int direction, MouseModifiers modifiers, Vector2i position) {
        if (direction > 0) {
            moveCloser(modifiers != MouseModifiers.MM_KEY_SHIFT);
        } else {
            moveAway(modifiers != MouseModifiers.MM_KEY_SHIFT);
        }
    }


    @Override
    public void charPress(int key, boolean isShiftPressed, float lastFrameDuration) {
        if (rightKeyboardCtrls) {
            switch (key) {
                case GLFW_KEY_I:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_FORWARD,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_K:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_BACKWARD,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_L:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_RIGHT,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_J:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_LEFT,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_O:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_UP,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_U:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_DOWN,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;
            }
        } else {
            switch (key) {
                case GLFW_KEY_W:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_FORWARD,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_S:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_BACKWARD,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_D:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_RIGHT,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_A:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_LEFT,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_E:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_UP,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;

                case GLFW_KEY_Q:
                    offsetTargetPos(
                            TargetOffsetDir.DIR_DOWN,
                            isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset,
                            lastFrameDuration);
                    break;
            }
        }
    }


    @Override
    public Matrix4f calcMatrix() {
        Matrix4f mat = new Matrix4f();

        // Remember: these transforms are in reverse order.

        // In this space, we are facing in the correct direction. Which means that the camera point
        // is directly behind us by the radius number of units.
        mat.translate(new Vector3f(0.0f, 0.0f, -currView.radius));

        // Rotate the world to look in the right direction.
        Quaternionf angleAxis = new Quaternionf().setAngleAxis((float) Math.toRadians(currView.degSpinRotation), 0.0f, 0.0f, 1.0f);
        Quaternionf fullRotation = angleAxis.mul(currView.orient);

        mat.mul(fullRotation.get(new Matrix4f()));

        // Translate the world by the negation of the lookat point, placing the origin at the lookat point.
        mat.translate(new Vector3f(currView.targetPos).negate());
        return mat;
    }

    ////////////////////////////////
    public void reset() {
        if (!isDragging) {
            currView = new ViewData(initialView);
        }
    }


    public ViewData getView() {
        return currView;
    }

    ////////////////////////////////
    private void beginDragRotate(Vector2i ptStart, RotateMode rotMode) {
        rotateMode = rotMode;
        startDragMouseLoc = ptStart;
        degStarDragSpin = currView.degSpinRotation;
        startDragOrient = currView.orient;
        isDragging = true;
    }


    private void onDragRotate(Vector2i ptCurr) {
        Vector2i diff = new Vector2i(ptCurr).sub(startDragMouseLoc);
        int diffX = diff.x;
        int diffY = diff.y;

        switch (rotateMode) {
            case RM_DUAL_AXIS_ROTATE:
                processXYChange(diffX, diffY);
                break;

            case RM_BIAXIAL_ROTATE:
                if (Math.abs(diffX) > Math.abs(diffY)) {
                    processXChange(diffX, true);
                } else {
                    processYChange(diffY, true);
                }
                break;

            case RM_XZ_AXIS_ROTATE:
                processXChange(diffX);
                break;

            case RM_Y_AXIS_ROTATE:
                processYChange(diffY);
                break;

            case RM_SPIN_VIEW_AXIS:
                processSpinAxis(diffX, diffY);
                break;
        }
    }


    private void endDragRotate(Vector2i ptEnd) {
        endDragRotate(ptEnd, true);
    }

    private void endDragRotate(Vector2i ptEnd, boolean keepResults) {
        if (keepResults) {
            onDragRotate(ptEnd);
        } else {
            currView.orient = startDragOrient;
        }

        isDragging = false;
    }


    private void processXChange(int diffX) {
        processXChange(diffX, false);
    }

    private void processXChange(int diffX, boolean clearY) {
        float degAngleDiff = diffX * viewScale.rotationScale;

        // Rotate about the world-space Y axis.
        Quaternionf angleAxisY = new Quaternionf().setAngleAxis((float) Math.toRadians(degAngleDiff), 0.0f, 1.0f, 0.0f);
        currView.orient = new Quaternionf(startDragOrient).mul(angleAxisY);
    }


    private void processYChange(int diffY) {
        processYChange(diffY, false);
    }

    private void processYChange(int diffY, boolean clearXZ) {
        float degAngleDiff = diffY * viewScale.rotationScale;

        // Rotate about the local-space X axis.
        Quaternionf angleAxisX = new Quaternionf().setAngleAxis((float) Math.toRadians(degAngleDiff), 1.0f, 0.0f, 0.0f);
        currView.orient = angleAxisX.mul(startDragOrient);
    }


    private void processXYChange(int diffX, int diffY) {
        float degXAngleDiff = (diffX * viewScale.rotationScale);
        float degYAngleDiff = (diffY * viewScale.rotationScale);

        // Rotate about the world-space Y axis.
        Quaternionf angleAxisY = new Quaternionf().setAngleAxis((float) Math.toRadians(degXAngleDiff), 0.0f, 1.0f, 0.0f);
        currView.orient = new Quaternionf(startDragOrient).mul(angleAxisY);
        // Rotate about the local-space X axis.
        Quaternionf angleAxisX = new Quaternionf().setAngleAxis((float) Math.toRadians(degYAngleDiff), 1.0f, 0.0f, 0.0f);
        currView.orient = angleAxisX.mul(currView.orient);
    }


    private void processSpinAxis(int diffX, int diffY) {
        float degSpinDiff = diffX * viewScale.rotationScale;
        currView.degSpinRotation = degSpinDiff + degStarDragSpin;
    }


    private void moveCloser(boolean largeStep) {
        if (largeStep) {
            currView.radius -= viewScale.largeRadiusDelta;
        } else {
            currView.radius -= viewScale.smallRadiusDelta;
        }

        if (currView.radius < viewScale.minRadius) {
            currView.radius = viewScale.minRadius;
        }
    }


    private void moveAway(boolean largeStep) {
        if (largeStep) {
            currView.radius += viewScale.largeRadiusDelta;
        } else {
            currView.radius += viewScale.smallRadiusDelta;
        }

        if (currView.radius > viewScale.maxRadius) {
            currView.radius = viewScale.maxRadius;
        }
    }


    private void offsetTargetPos(TargetOffsetDir dir, float worldDistance, float lastFrameDuration) {
        Vector3f offsetDir = new Vector3f(offsets[dir.ordinal()]);
        offsetTargetPos(offsetDir.mul(worldDistance).mul(lastFrameDuration), lastFrameDuration);
    }

    private void offsetTargetPos(Vector3f cameraOffset, float lastFrameDuration) {
        Matrix4f currMat = calcMatrix();
        Quaternionf orientation = new Quaternionf();
        currMat.getNormalizedRotation(orientation);

        Quaternionf invOrient = new Quaternionf(orientation).conjugate();
        Vector3f worldOffset = invOrient.transform(new Vector3f(cameraOffset));

        currView.targetPos.add(worldOffset);
    }
} public static class ViewScale {
    public float minRadius;
    public float maxRadius;
    public float largeRadiusDelta;
    public float smallRadiusDelta;
    public float largePosOffset;
    public float smallPosOffset;
    public float rotationScale;

    public ViewScale(float minRadius, float maxRadius, float largeRadiusDelta, float smallRadiusDelta,
                     float largePosOffset, float smallPosOffset, float rotationScale) {
        this.minRadius = minRadius;
        this.maxRadius = maxRadius;
        this.largeRadiusDelta = largeRadiusDelta;
        this.smallRadiusDelta = smallRadiusDelta;
        this.largePosOffset = largePosOffset;
        this.smallPosOffset = smallPosOffset;
        this.rotationScale = rotationScale;
    }
} public abstract class Tutorial {
    protected long window;

    protected DoubleBuffer mouseBuffer1 = BufferUtils.createDoubleBuffer(1);
    protected DoubleBuffer mouseBuffer2 = BufferUtils.createDoubleBuffer(1);

    // Measured in seconds
    protected float elapsedTime;
    protected float lastFrameDuration;

    private double lastFrameTimestamp;

    protected FloatBuffer vec4Buffer = BufferUtils.createFloatBuffer(4);
    protected FloatBuffer mat3Buffer = BufferUtils.createFloatBuffer(9);
    protected FloatBuffer mat4Buffer = BufferUtils.createFloatBuffer(16);
    protected ByteBuffer projectionBlockBuffer = BufferUtils.createByteBuffer(ProjectionBlock.BYTES);
    protected ByteBuffer unprojectionBlockBuffer = BufferUtils.createByteBuffer(UnprojectionBlock.BYTES);
    protected ByteBuffer lightBlockBuffer = BufferUtils.createByteBuffer(LightBlock.BYTES);
    protected ByteBuffer materialBlockBuffer = BufferUtils.createByteBuffer(MaterialBlock.BYTES);

    ////////////////////////////////

    public final void start(int width, int height) {
        try {
            GLFWErrorCallback.createPrint(System.err).set();

            initGLFW();
            initWindow(width, height);

            glfwSwapInterval(1);
            GL.createCapabilities();

            initCallbacks();
            init();
            initViewport();

            printInfo();

            loop();

            glfwFreeCallbacks(window);
            glfwDestroyWindow(window);
        } finally {
            glfwTerminate();
            glfwSetErrorCallback(null).free();
        }
    }


    private void initGLFW() {
        if (!glfwInit()) throw new IllegalStateException("Unable to initialize GLFW");

        glfwDefaultWindowHints();
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
        if (Platform.get() == Platform.MACOSX) {
            glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
            glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
            glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        }
    }

    private void initWindow(int width, int height) {
        window = glfwCreateWindow(width, height, "jgltut", NULL, NULL);
        if (window == NULL) throw new RuntimeException("Failed to create the GLFW window");

        GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
        glfwSetWindowPos(window, (vidmode.width() - width) / 2, (vidmode.height() - height) / 2);

        glfwMakeContextCurrent(window);
        glfwShowWindow(window);
    }

    private void initViewport() {
        // From http://www.glfw.org/faq.html#why-is-my-output-in-the-lower-left-corner-of-the-window:
        // "On OS X with a Retina display, and possibly on other platforms in the future, screen coordinates and
        // pixels do not map 1:1. Use the framebuffer size, which is in pixels, instead of the window size."
        int[] w = new int[1], h = new int[1];
        glfwGetFramebufferSize(window, w, h);
        reshape(w[0], h[0]);
    }

    private void initCallbacks() {
        glfwSetFramebufferSizeCallback(window, (window, width, height) -> {
            reshape(width, height);
        });

        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) glfwSetWindowShouldClose(window, true);
        });
    }

    private void printInfo() {
        System.out.println();
        System.out.println("-----------------------------------------------------------");

        System.out.format("%-18s%s\n", "Running:", getClass().getName());
        System.out.format("%-18s%s\n", "OpenGL version:", glGetString(GL_VERSION));

        if (!GL.getCapabilities().OpenGL33) {
            System.out.println("You must have at least OpenGL 3.3 to run this tutorial.");
        }
    }

    private void loop() {
        long startTime = System.nanoTime();
        while (!glfwWindowShouldClose(window)) {
            elapsedTime = (float) ((System.nanoTime() - startTime) / 1000000000.0);

            double now = System.nanoTime();
            lastFrameDuration = (float) ((now - lastFrameTimestamp) / 1000000000.0);
            lastFrameTimestamp = now;

            update();
            display();

            glfwSwapBuffers(window);
            glfwPollEvents();
        }
    }

    ////////////////////////////////

    protected abstract void init();

    protected abstract void display();

    protected abstract void reshape(int w, int h);

    protected abstract void update();

    ////////////////////////////////

    protected final boolean isKeyPressed(int key) {
        return glfwGetKey(window, key) == 1;
    }

    protected final boolean isMouseButtonPressed(int key) {
        return glfwGetMouseButton(window, key) == 1;
    }
} public class LightBlock implements Bufferable {
    public static final int MAX_NUMBER_OF_LIGHTS = 5;
    public static final int BYTES = Float.BYTES * (4 + 1 + 1 + 2) + PerLight.BYTES * MAX_NUMBER_OF_LIGHTS;

    public Vector4f ambientIntensity;
    public float lightAttenuation;
    public float maxIntensity;
    public float padding[] = new float[2];
    public PerLight lights[] = new PerLight[MAX_NUMBER_OF_LIGHTS];

    @Override
    public ByteBuffer get(ByteBuffer buffer) {
        ambientIntensity.get(buffer);
        buffer.position(buffer.position() + Float.BYTES * 4);

        buffer.putFloat(lightAttenuation);

        buffer.putFloat(maxIntensity);

        buffer.putFloat(padding[0]);
        buffer.putFloat(padding[1]);

        for (PerLight light : lights) {
            if (light == null) break;
            light.get(buffer);
        }
        return buffer;
    }
} public class PerLight implements Bufferable {
    public static final int BYTES = Float.BYTES * (4 + 4);

    public Vector4f cameraSpaceLightPos;
    public Vector4f lightIntensity;

    @Override
    public ByteBuffer get(ByteBuffer buffer) {
        cameraSpaceLightPos.get(buffer);
        buffer.position(buffer.position() + Float.BYTES * 4);

        lightIntensity.get(buffer);
        buffer.position(buffer.position() + Float.BYTES * 4);
        return buffer;
    }
} public class ProjectionBlock implements Bufferable {
    public static final int BYTES = Float.BYTES * (16);

    public Matrix4f cameraToClipMatrix;

    @Override
    public ByteBuffer get(ByteBuffer buffer) {
        cameraToClipMatrix.get(buffer);
        buffer.position(buffer.position() + Float.BYTES * 16);
        return buffer;
    }
} public class SceneNode {
    SceneNode(SceneMesh mesh, SceneProgram program, Vector3f nodePos, ArrayList<TextureBinding> textureBindings) {
        this.mesh = mesh;
        this.program = program;

        stateBinders = new ArrayList<>();
        this.textureBindings = textureBindings;

        nodeTransform = new Transform();
        objTransform = new Transform();

        nodeTransform.trans = nodePos;
    }


    public void nodeSetOrient(Quaternionf orient) {
        nodeTransform.orient = new Quaternionf(orient);
    }

    public Quaternionf nodeGetOrient() {
        return nodeTransform.orient;
    }


    void setNodeScale(Vector3f nodeScale) {
        nodeTransform.scale = new Vector3f(nodeScale);
    }

    void setNodeOrient(Quaternionf nodeOrient) {
        nodeTransform.orient = new Quaternionf(nodeOrient).normalize();
    }


    void render(int samplers[], Matrix4f mat) {
        Matrix4f baseMat = new Matrix4f(mat);
        baseMat.mul(nodeTransform.getMatrix());
        Matrix4f objMat = new Matrix4f(baseMat).mul(objTransform.getMatrix());

        program.useProgram();
        glUniformMatrix4fv(program.getMatrixLoc(), false, objMat.get(mat4Buffer));

        if (program.getNormalMatLoc() != -1) {
            Matrix3f normMat = new Matrix3f(objMat).invert().transpose();
            glUniformMatrix3fv(program.getNormalMatLoc(), false, normMat.get(mat3Buffer));
        }

        for (SceneBinders.StateBinder stateBinder : stateBinders) {
            stateBinder.bindState(program.getProgram());
        }

        for (TextureBinding binding : textureBindings) {
            glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
            glBindTexture(binding.texture.getType(), binding.texture.getTexture());
            glBindSampler(binding.textureUnit, samplers[binding.sampler.ordinal()]);
        }

        mesh.render();

        for (TextureBinding binding : textureBindings) {
            glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
            glBindTexture(binding.texture.getType(), 0);
            glBindSampler(binding.textureUnit, 0);
        }

        for (SceneBinders.StateBinder stateBinder : stateBinders) {
            stateBinder.unbindState(program.getProgram());
        }

        glUseProgram(0);
    }


    void setStateBinder(SceneBinders.StateBinder stateBinder) {
        stateBinders.add(stateBinder);
    }


    int getProgram() {
        return program.getProgram();
    }

    ////////////////////////////////

    private SceneMesh mesh;
    private SceneProgram program;

    private ArrayList<SceneBinders.StateBinder> stateBinders;
    private ArrayList<TextureBinding> textureBindings;

    private Transform nodeTransform;
    private Transform objTransform;
} public static class UniformIntBinder extends UniformBinderBase {
    public void setValue(int val) {
        this.val = val;
    }

    ////////////////////////////////
    @Override
    void bindState(int prog) {
        glUniform1i(getUniformLoc(prog), val);
    }

    @Override
    void unbindState(int prog) {
    }

    ////////////////////////////////
    private int val;
} public static class UniformMat4Binder extends UniformBinderBase {
    public void setValue(Matrix4f val) {
        this.val = new Matrix4f(val);
    }

    ////////////////////////////////
    @Override
    void bindState(int prog) {
        glUniformMatrix4fv(getUniformLoc(prog), false, val.get(mat4Buffer));
    }

    @Override
    void unbindState(int prog) {
    }

    ////////////////////////////////
    private Matrix4f val = new Matrix4f();
    private FloatBuffer mat4Buffer = BufferUtils.createFloatBuffer(16);
} public static class UniformVec3Binder extends UniformBinderBase {
    public void setValue(Vector3f val) {
        this.val = new Vector3f(val);
    }

    ////////////////////////////////
    @Override
    void bindState(int prog) {
        glUniform3fv(getUniformLoc(prog), val.get(vec3Buffer));
    }

    @Override
    void unbindState(int prog) {
    }

    ////////////////////////////////
    private Vector3f val = new Vector3f();
    private FloatBuffer vec3Buffer = BufferUtils.createFloatBuffer(3);
}
import integeruser.jglsdk.glimg.DdsLoader;
import integeruser.jglsdk.glimg.ImageSet;
import integeruser.jglsdk.glimg.TextureGenerator;
import integeruser.jglsdk.glutil.MousePoles.MouseButtons;
import integeruser.jglsdk.glutil.MousePoles.ViewData;
import integeruser.jglsdk.glutil.MousePoles.ViewPole;
import integeruser.jglsdk.glutil.MousePoles.ViewScale;
import integeruser.jgltut.Tutorial;
import integeruser.jgltut.commons.LightBlock;
import integeruser.jgltut.commons.PerLight;
import integeruser.jgltut.commons.ProjectionBlock;
import integeruser.jgltut.framework.*;
import integeruser.jgltut.framework.Scene.SceneNode;
import integeruser.jgltut.framework.SceneBinders.UniformIntBinder;
import integeruser.jgltut.framework.SceneBinders.UniformMat4Binder;
import integeruser.jgltut.framework.SceneBinders.UniformVec3Binder;
import org.joml.*;
import org.lwjgl.BufferUtils;
import java.lang.Math;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;
import static org.lwjgl.opengl.GL13.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER_SRGB;
import static org.lwjgl.opengl.GL30.glBindBufferRange;
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
import static org.lwjgl.opengl.GL32.GL_DEPTH_CLAMP;
import static org.lwjgl.opengl.GL33.*;
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }


        if (isKeyPressed(GLFW_KEY_I)) {
            lightViewPole.charPress(GLFW_KEY_I, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_K)) {
            lightViewPole.charPress(GLFW_KEY_K, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_L)) {
            lightViewPole.charPress(GLFW_KEY_L, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_J)) {
            lightViewPole.charPress(GLFW_KEY_J, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_O)) {
            lightViewPole.charPress(GLFW_KEY_O, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_U)) {
            lightViewPole.charPress(GLFW_KEY_U, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private int coloredModelToCameraMatrixUnif;
    private int coloredProg;

    private int unlitProg;
    private int unlitModelToCameraMatrixUnif;
    private int unlitObjectColorUnif;


    private void loadAndSetupScene() {
        scene = new Scene("proj2d_scene.xml");

        nodes = new ArrayList<>();
        nodes.add(scene.findNode("cube"));
        nodes.add(scene.findNode("rightBar"));
        nodes.add(scene.findNode("leaningBar"));
        nodes.add(scene.findNode("spinBar"));
        nodes.add(scene.findNode("diorama"));
        nodes.add(scene.findNode("floor"));

        lightNumBinder = new UniformIntBinder();
        SceneBinders.associateUniformWithNodes(nodes, lightNumBinder, "numberOfLights");
        SceneBinders.setStateBinderWithNodes(nodes, lightNumBinder);

        lightProjMatBinder = new UniformMat4Binder();
        SceneBinders.associateUniformWithNodes(nodes, lightProjMatBinder, "cameraToLightProjMatrix");
        SceneBinders.setStateBinderWithNodes(nodes, lightProjMatBinder);

        camLightPosBinder = new UniformVec3Binder();
        SceneBinders.associateUniformWithNodes(nodes, camLightPosBinder, "cameraSpaceProjLightPos");
        SceneBinders.setStateBinderWithNodes(nodes, camLightPosBinder);

        int unlit = scene.findProgram("p_unlit");
        sphereMesh = scene.findMesh("m_sphere");

        int colored = scene.findProgram("p_colored");
        axesMesh = scene.findMesh("m_axes");

        // No more things that can throw.
        spinBarOrient = nodes.get(3).nodeGetOrient();
        unlitProg = unlit;
        unlitModelToCameraMatrixUnif = glGetUniformLocation(unlit, "modelToCameraMatrix");
        unlitObjectColorUnif = glGetUniformLocation(unlit, "objectColor");

        coloredProg = colored;
        coloredModelToCameraMatrixUnif = glGetUniformLocation(colored, "modelToCameraMatrix");
    }

    ////////////////////////////////
    private static int displayWidth = 500;
    private static int displayHeight = 500;

    private Scene scene;
    private ArrayList<SceneNode> nodes;
    private Mesh sphereMesh, axesMesh;

    private UniformMat4Binder lightProjMatBinder;
    private UniformVec3Binder camLightPosBinder;

    private float[] lightFOVs = {
            10.0f, 20.0f, 45.0f, 75.0f,
            90.0f, 120.0f, 150.0f, 170.0f
    };
    private int currFOVIndex = 3;

    private Timer timer = new Timer(Timer.Type.LOOP, 10.0f);

    private Quaternionf spinBarOrient;

    private boolean showOtherLights = true;
    private boolean drawCameraPos;

    ////////////////////////////////
    // View setup.
    private ViewData initialView = new ViewData(
            new Vector3f(0.0f, 0.0f, 10.0f),
            new Quaternionf(0.16043f, -0.376867f, -0.0664516f, 0.909845f),
            25.0f,
            0.0f
    );

    private ViewScale initialViewScale = new ViewScale(