//! \file       ImageFCB.cs
//! \date       Thu Aug 11 04:21:02 2016
//! \brief      Caramel BOX compressed image format.
//
// Copyright (C) 2016 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//

using System.ComponentModel.Composition;
using System.IO;
using System.Windows.Media;
using GameRes.Compression;
using GameRes.Utility;

namespace GameRes.Formats.CaramelBox
{
    internal class FcbMetaData : ImageMetaData
    {
        public int Method;
    }

    [Export(typeof(ImageFormat))]
    public class FcbFormat : ImageFormat
    {
        public override string         Tag { get { return "FCB"; } }
        public override string Description { get { return "Caramel BOX image format"; } }
        public override uint     Signature { get { return 0x31626366; } } // 'fcb1'

        public override ImageMetaData ReadMetaData (IBinaryStream stream)
        {
            var header = stream.ReadHeader (0x10);
            return new FcbMetaData
            {
                Width  = header.ToUInt32 (4),
                Height = header.ToUInt32 (8),
                Method = header.ToInt32 (12),
                BPP    = 32,
            };
        }

        public override ImageData Read (IBinaryStream stream, ImageMetaData info)
        {
            var meta = (FcbMetaData)info;
            byte[] input;
            if (1 == meta.Method)
            {
                stream.Position = 0x14;
                int unpacked_size = Binary.BigEndian (stream.ReadInt32());
                stream.ReadInt32(); // packed_size
                input = new byte[unpacked_size];
                using (var z = new ZLibStream (stream.AsStream, CompressionMode.Decompress, true))
                    if (unpacked_size != z.Read (input, 0, unpacked_size))
                        throw new EndOfStreamException();
            }
            else if (0 == meta.Method)
            {
                stream.Position = 0x10;
                using (var tz = new TzCompression (stream.AsStream))
                    input = tz.Unpack();
            }
            else
                throw new InvalidFormatException();
            var pixels = Unpack (input, info);
            return ImageData.Create (info, PixelFormats.Bgra32, null, pixels);
        }

        public override void Write (Stream file, ImageData image)
        {
            throw new System.NotImplementedException ("FcbFormat.Write not implemented");
        }

        byte[] Unpack (byte[] input, ImageMetaData info)
        {
            byte[] ref_pixel = { 0x80, 0x80, 0x80, 0xFF };
            var pixel = new byte[4];
            var delta = new int[4];

            var output = new byte[info.Width * info.Height * 4];
            int src = 0;
            int dst = 0;
            for (uint y = 0; y < info.Height; ++y)
            {
                pixel[0] = ref_pixel[0];
                pixel[1] = ref_pixel[1];
                pixel[2] = ref_pixel[2];
                pixel[3] = ref_pixel[3];

                for (uint x = 0; x < info.Width; ++x)
                {
                    int v = input[src++];
                    if (0 != (v & 0x80))
                    {
                        if (0 != (v & 0x40))
                        {
                            if (0 != (v & 0x20))
                            {
                                if (0 != (v & 0x10))
                                {
                                    if (0 != (v & 0x08))
                                    {
                                        if (v == 0xFE)
                                        {
                                            delta[0] = input[src++] - 128;
                                            delta[1] = input[src++] - 128;
                                            delta[2] = input[src++] - 128;
                                            delta[3] = 0;
                                        }
                                        else
                                        {
                                            delta[0] = input[src++] - 128;
                                            delta[1] = input[src++] - 128;
                                            delta[2] = input[src++] - 128;
                                            delta[3] = input[src++] - 128;
                                        }
                                    }
                                    else
                                    {
                                        v = input[src++] | v << 8;
                                        v = input[src++] | v << 8;
                                        v = input[src++] | v << 8;
                                        delta[0] = ((v >> 20) & 0x7F) - 64;
                                        delta[1] = ((v >> 14) & 0x3F) - 32;
                                        delta[2] = ((v >> 8)  & 0x3F) - 32;
                                        delta[3] = v - 128;
                                    }
                                }
                                else
                                {
                                    v = input[src++] | v << 8;
                                    v = input[src++] | v << 8;
                                    delta[0] = ((v >> 14) & 0x3F) - 32;
                                    delta[1] = ((v >> 10) & 0x0F) - 8;
                                    delta[2] = ((v >> 6)  & 0x0F) - 8;
                                    delta[3] = (v & 0x3F) - 32;
                                }
                            }
                            else
                            {
                                v = input[src++] | v << 8;
                                v = input[src++] | v << 8;
                                delta[0] = ((v >> 13) & 0xFF) - 128;
                                delta[1] = ((v >> 7)  & 0x3F) - 32;
                                delta[2] = (v & 0x7F) - 64;
                                delta[3] = 0;
                            }
                        }
                        else
                        {
                            v = input[src++] | v << 8;
                            delta[0] = ((v >> 8) & 0x3F) - 32;
                            delta[1] = ((v >> 4) & 0x0F) - 8;
                            delta[2] = (v & 0xf) - 8;
                            delta[3] = 0;
                        }
                    }
                    else
                    {
                        delta[0] = ((v >> 4) & 7) - 4;
                        delta[1] = ((v >> 2) & 3) - 2;
                        delta[2] = (v & 3) - 2;
                        delta[3] = 0;
                    }

                    pixel[0] += (byte)(delta[0] + delta[1]);
                    pixel[1] += (byte)delta[0];
                    pixel[2] += (byte)(delta[0] + delta[2]);
                    pixel[3] += (byte)delta[3];

                    output[dst++] = pixel[0];
                    output[dst++] = pixel[1];
                    output[dst++] = pixel[2];
                    output[dst++] = pixel[3];

                    if (0 == x)
                    {
                        ref_pixel[0] = pixel[0];
                        ref_pixel[1] = pixel[1];
                        ref_pixel[2] = pixel[2];
                        ref_pixel[3] = pixel[3];
                    }
                }
            }
            return output;
        }
    }
}
