/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.texture.plugins;

import com.jme3.math.FastMath;
import com.jme3.texture.Image.Format;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * DXTFlipper is a utility class used to flip along Y axis DXT compressed textures.
 * 
 * @author Kirill Vainer
 */
public class DXTFlipper {

    private static final ByteBuffer bb = ByteBuffer.allocate(8);

    static {
        bb.order(ByteOrder.LITTLE_ENDIAN);
    }

    private static long readCode5(long data, int x, int y){
        long shift =   (4 * y + x) * 3;
        long mask = 0x7;
        mask <<= shift;
        long code = data & mask;
        code >>= shift;
        return code;
    }

    private static long writeCode5(long data, int x, int y, long code){
        long shift =  (4 * y + x) * 3;
        long mask = 0x7;
        code = (code & mask) << shift;
        mask <<= shift;
        mask = ~mask;
        data &= mask;
        data |= code; // write new code
        return data;
    }

    private static void flipDXT5Block(byte[] block, int h){
        if (h == 1)
            return;

        byte c0 = block[0];
        byte c1 = block[1];

        bb.clear();
        bb.put(block, 2, 6).flip();
        bb.clear();
        long l = bb.getLong();
        long n = l;

        if (h == 2){
            n = writeCode5(n, 0, 0, readCode5(l, 0, 1));
            n = writeCode5(n, 1, 0, readCode5(l, 1, 1));
            n = writeCode5(n, 2, 0, readCode5(l, 2, 1));
            n = writeCode5(n, 3, 0, readCode5(l, 3, 1));

            n = writeCode5(n, 0, 1, readCode5(l, 0, 0));
            n = writeCode5(n, 1, 1, readCode5(l, 1, 0));
            n = writeCode5(n, 2, 1, readCode5(l, 2, 0));
            n = writeCode5(n, 3, 1, readCode5(l, 3, 0));
        }else{
            n = writeCode5(n, 0, 0, readCode5(l, 0, 3));
            n = writeCode5(n, 1, 0, readCode5(l, 1, 3));
            n = writeCode5(n, 2, 0, readCode5(l, 2, 3));
            n = writeCode5(n, 3, 0, readCode5(l, 3, 3));

            n = writeCode5(n, 0, 1, readCode5(l, 0, 2));
            n = writeCode5(n, 1, 1, readCode5(l, 1, 2));
            n = writeCode5(n, 2, 1, readCode5(l, 2, 2));
            n = writeCode5(n, 3, 1, readCode5(l, 3, 2));

            n = writeCode5(n, 0, 2, readCode5(l, 0, 1));
            n = writeCode5(n, 1, 2, readCode5(l, 1, 1));
            n = writeCode5(n, 2, 2, readCode5(l, 2, 1));
            n = writeCode5(n, 3, 2, readCode5(l, 3, 1));

            n = writeCode5(n, 0, 3, readCode5(l, 0, 0));
            n = writeCode5(n, 1, 3, readCode5(l, 1, 0));
            n = writeCode5(n, 2, 3, readCode5(l, 2, 0));
            n = writeCode5(n, 3, 3, readCode5(l, 3, 0));
        }
            
        bb.clear();
        bb.putLong(n);
        bb.clear();
        bb.get(block, 2, 6).flip();

        assert c0 == block[0] && c1 == block[1];
    }

    private static void flipDXT3Block(byte[] block, int h){
        if (h == 1)
            return;

        // first row
        byte tmp0 = block[0];
        byte tmp1 = block[1];

        if (h == 2){
            block[0] = block[2];
            block[1] = block[3];

            block[2] = tmp0;
            block[3] = tmp1;
        }else{
            // write last row to first row
            block[0] = block[6];
            block[1] = block[7];

            // write first row to last row
            block[6] = tmp0;
            block[7] = tmp1;

            // 2nd row
            tmp0 = block[2];
            tmp1 = block[3];

            // write 3rd row to 2nd
            block[2] = block[4];
            block[3] = block[5];

            // write 2nd row to 3rd
            block[4] = tmp0;
            block[5] = tmp1;
        }
    }

    /**
     * Flips a DXT color block or a DXT3 alpha block
     * @param block
     * @param h
     */
    private static void flipDXT1orDXTA3Block(byte[] block, int h){
        byte tmp;
        switch (h){
            case 1:
                return;
            case 2:
                // keep header intact (the two colors)
                // header takes 4 bytes

                // flip only two top rows
                tmp = block[4+1];
                block[4+1] = block[4+0];
                block[4+0] = tmp;
                return;
            default:
                // keep header intact (the two colors)
                // header takes 4 bytes

                // flip first & fourth row
                tmp = block[4+3];
                block[4+3] = block[4+0];
                block[4+0] = tmp;

                // flip second and third row
                tmp = block[4+2];
                block[4+2] = block[4+1];
                block[4+1] = tmp;
                return;
        }
    }

    public static ByteBuffer flipDXT(ByteBuffer img, int w, int h, Format format){
        int originalLimit = img.limit();
        int blocksX = (int) FastMath.ceil((float)w / 4f);
        int blocksY = (int) FastMath.ceil((float)h / 4f);

        int type;
        switch (format){
            case DXT1:
            case DXT1A:
                type = 1;
                break;
            case DXT3:
                type = 2;
                break;
            case DXT5:
                type = 3;
                break;
            /*
            case LATC:
                type = 4;
                break;
            case LTC:
                type = 5;
                break;
            */
            default:
                throw new IllegalArgumentException();
        }

        // DXT1 uses 8 bytes per block,
        // DXT3, DXT5, LATC use 16 bytes per block
        int bpb = type == 1 || type == 5 ? 8 : 16;

        ByteBuffer retImg = BufferUtils.createByteBuffer(blocksX * blocksY * bpb);
        if (h == 1){
            retImg.put(img);
            retImg.rewind();
        }else if (h == 2){
            byte[] colorBlock = new byte[8];
            byte[] alphaBlock = type != 1 && type != 5 ? new byte[8] : null;
            for (int x = 0; x < blocksX; x++){
                // prepeare for block reading
                int blockByteOffset = x * bpb;
                img.position(blockByteOffset);
                img.limit(blockByteOffset + bpb);

                if (alphaBlock != null){
                    img.get(alphaBlock);
                    switch (type){
                        case 2:
                            flipDXT3Block(alphaBlock, h); 
                            break;
                        case 3:
                        case 4: 
                            flipDXT5Block(alphaBlock, h);
                            break;
                    }
                    retImg.put(alphaBlock);
                }
                
                img.get(colorBlock);
                if (type == 4 || type == 5)
                    flipDXT5Block(colorBlock, h);
                else
                    flipDXT1orDXTA3Block(colorBlock, h);

                // write block (no need to flip block indexes, only pixels
                // inside block
                retImg.put(colorBlock);
            }
            retImg.rewind();
        }else if (h >= 4){
            byte[] colorBlock = new byte[8];
            byte[] alphaBlock = type != 1 && type != 5 ? new byte[8] : null;
            for (int y = 0; y < blocksY; y++){
                for (int x = 0; x < blocksX; x++){
                    // prepeare for block reading
                    int blockIdx = y * blocksX + x;
                    int blockByteOffset = blockIdx * bpb;

                    img.position(blockByteOffset);
                    img.limit(blockByteOffset + bpb);

                    blockIdx = (blocksY - y - 1) * blocksX + x;
                    blockByteOffset = blockIdx * bpb;

                    retImg.position(blockByteOffset);
                    retImg.limit(blockByteOffset + bpb);

                    if (alphaBlock != null){
                        img.get(alphaBlock);
                        switch (type){
                            case 2:
                                flipDXT3Block(alphaBlock, h);
                                break;
                            case 3:
                            case 4:
                                flipDXT5Block(alphaBlock, h);
                                break;
                        }
                        retImg.put(alphaBlock);
                    }

                    img.get(colorBlock);
                    if (type == 4 || type == 5)
                        flipDXT5Block(colorBlock, h);
                    else
                        flipDXT1orDXTA3Block(colorBlock, h);
                    
                    retImg.put(colorBlock);
                }
            }
            retImg.limit(retImg.capacity());
            retImg.position(0);
        } else {
            return null;
        }
        img.limit(originalLimit); // make sure to restore original limit.
        return retImg;
    }

}
