//! \file       ImageSeraph.cs
//! \date       Sat Jul 18 12:16:42 2015
//! \brief      Seraphim engine images.
//
// Copyright (C) 2015 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;
using System.ComponentModel.Composition;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using GameRes.Utility;

namespace GameRes.Formats.Seraphim
{
    internal class SeraphMetaData : ImageMetaData
    {
        public int PackedSize;
        public int Colors;
    }

    [Export(typeof(ImageFormat))]
    public class SeraphCfImage : ImageFormat
    {
        public override string         Tag { get { return "CF"; } }
        public override string Description { get { return "Seraphim engine image format"; } }
        public override uint     Signature { get { return 0x4643; } }

        public SeraphCfImage ()
        {
            Signatures = new [] { 0x4643u, 0x024643u, 0x044643u, 0x074643u, 0x094643u, 0x144643u, 0u };
            Extensions = new [] { "cts" };
        }

        public override ImageMetaData ReadMetaData (IBinaryStream stream)
        {
            var header = stream.ReadHeader (0x10);
            if ('C' != header[0] || 'F' != header[1] || 0 != header[3])
                return null;
            int packed_size = header.ToInt32 (12);
            if (packed_size <= 0 || packed_size > stream.Length-0x10)
                return null;
            uint width  = header.ToUInt16 (8);
            uint height = header.ToUInt16 (10);
            if (0 == width || 0 == height)
                return null;
            return new SeraphMetaData
            {
                OffsetX = header.ToInt16 (4),
                OffsetY = header.ToInt16 (6),
                Width   = width,
                Height  = height,
                BPP     = 24,
                PackedSize = packed_size,
            };
        }

        public override ImageData Read (IBinaryStream stream, ImageMetaData info)
        {
            var meta = (SeraphMetaData)info;
            var reader = new SeraphReader (stream.AsStream, meta);
            reader.UnpackCf();
            return ImageData.Create (info, reader.Format, null, reader.Data);
        }

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

    [Export(typeof(ImageFormat))]
    public class SeraphCtImage : SeraphCfImage
    {
        public override string         Tag { get { return "CT"; } }
        public override uint     Signature { get { return 0x5443; } }

        public override ImageMetaData ReadMetaData (IBinaryStream stream)
        {
            var info = base.ReadMetaData (stream);
            if (info != null)
                info.BPP = 32;
            return info;
        }

        public override ImageData Read (IBinaryStream stream, ImageMetaData info)
        {
            var meta = (SeraphMetaData)info;
            var reader = new SeraphReader (stream.AsStream, meta);
            reader.UnpackCt();
            return ImageData.Create (info, reader.Format, null, reader.Data);
        }

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

    [Export(typeof(ImageFormat))]
    public class SeraphCbImage : ImageFormat
    {
        public override string         Tag { get { return "CB"; } }
        public override string Description { get { return "Seraphim engine image format"; } }
        public override uint     Signature { get { return 0; } }

        public SeraphCbImage ()
        {
            // common case for 256-colors images
            Signatures = new uint[] { 0x01004243, 0 };
            Extensions = new string[] { "CB", "CLB" };
        }

        public override ImageMetaData ReadMetaData (IBinaryStream stream)
        {
            var header = stream.ReadHeader (0x10);
            if ('C' != header[0] || 'B' != header[1])
                return null;
            int colors = header.ToUInt16 (2);
            int packed_size = header.ToInt32 (12);
            if (packed_size <= 0 /*|| packed_size > stream.Length-0x10*/)
                return null;
            int width  = header.ToInt16 (8);
            int height = header.ToInt16 (10);
            if (width <= 0 || height <= 0 || colors > 0x100)
                return null;
            return new SeraphMetaData
            {
                OffsetX = header.ToInt16 (4),
                OffsetY = header.ToInt16 (6),
                Width   = (uint)width,
                Height  = (uint)height,
                BPP     = 8,
                PackedSize = packed_size,
                Colors  = colors,
            };
        }

        public override ImageData Read (IBinaryStream stream, ImageMetaData info)
        {
            var meta = (SeraphMetaData)info;
            var reader = new SeraphReader (stream.AsStream, meta, 1);
            reader.UnpackCb();
            return ImageData.Create (info, reader.Format, reader.Palette, reader.Data);
        }

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

    [Export(typeof(ImageFormat))]
    public class SeraphCxImage : SeraphCfImage
    {
        public override string         Tag { get { return "CX"; } }
        public override uint     Signature { get { return 0x5843; } } // 'CX'

        public override ImageMetaData ReadMetaData (IBinaryStream stream)
        {
            var info = base.ReadMetaData (stream);
            if (info != null)
                info.BPP = 32;
            return info;
        }

        public override ImageData Read (IBinaryStream stream, ImageMetaData info)
        {
            var reader = new SeraphReader (stream.AsStream, (SeraphMetaData)info, 4);
            reader.UnpackCx();
            return ImageData.Create (info, reader.Format, null, reader.Data);
        }

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

    internal class SeraphReader
    {
        Stream      m_input;
        byte[]      m_output;
        int         m_width;
        int         m_height;
        int         m_stride;
        int         m_colors;
        int         m_packed_size;
        int         m_pixel_size;

        public byte[]           Data { get { return m_output; } }
        public PixelFormat    Format { get; private set; }
        public BitmapPalette Palette { get; private set; }
        public ImageMetaData    Info { get; private set; }

        public SeraphReader (Stream input, SeraphMetaData info, int pixel_size = 3)
        {
            Info = info;
            m_input = input;
            m_input.Position = 0x10;
            m_width = (int)info.Width;
            m_height = (int)info.Height;
            m_stride = m_width * pixel_size;
            m_output = new byte[m_stride * m_height];
            m_packed_size = info.PackedSize;
            m_colors = info.Colors;
            m_pixel_size = pixel_size;
            if (1 == pixel_size && m_colors > 0)
                Palette = ReadPalette (m_colors);
        }

        public BitmapPalette ReadPalette (int colors)
        {
            return ImageFormat.ReadPalette (m_input, Math.Min (colors, 0x100), PaletteFormat.Rgb);
        }

        public void UnpackCb ()
        {
            var pixels = UnpackBytes();
            int dst = 0;
            for (int src = (m_height-1) * m_width; src >= 0; src -= m_width)
            {
                Buffer.BlockCopy (pixels, src, m_output, dst, m_width);
                dst += m_width;
            }
            Format = PixelFormats.Indexed8;
        }

        public void UnpackCt ()
        {
            UnpackRgb();
            m_input.Position = 0x10 + m_packed_size + 4;
            var alpha = UnpackBytes();
            var pixels = new byte[m_width*m_height*4];
            int dst = 0;
            for (int y = m_height-1; y >= 0; --y)
            {
                int rgb = y * m_stride;
                int a   = y * m_width;
                for (int x = 0; x < m_width; ++x)
                {
                    pixels[dst++] = m_output[rgb++];
                    pixels[dst++] = m_output[rgb++];
                    pixels[dst++] = m_output[rgb++];
                    int v = Math.Min (alpha[a++] * 0xff / 0x64, 0xff);
                    pixels[dst++] = (byte)~v;
                }
            }
            m_output = pixels;
            Format = PixelFormats.Bgra32;
        }

        public void UnpackCf ()
        {
            UnpackRgb();
            FlipPixels();
            Format = PixelFormats.Bgr24;
        }

        public void UnpackCx ()
        {
            UnpackRgb();
            FlipPixels();
            Format = PixelFormats.Bgra32;
        }

        private void UnpackRgb () // sub_404250
        {
            int dst = 0;
            while (dst < m_output.Length)
            {
                int count;
                int ctl = m_input.ReadByte();
                if (-1 == ctl)
                    break;
                if ((ctl & 0xF0) == 0xF0)
                    throw new InvalidFormatException();

                if (0 == (ctl & 0x80))
                {
                    if (0 != (ctl & 0x40))
                    {
                        count = (ctl & 0x3F) + 2;
                        FillBytes (dst, (byte)m_input.ReadByte(), count);
                    }
                    else
                    {
                        count = (ctl & 0x3F) + 1;
                        if (count != m_input.Read (m_output, dst, count))
                            break;
                    }
                }
                else if (0 == (ctl & 0x40))
                {
                    count = m_input.ReadByte() | ((ctl & 0xF) << 8);
                    switch ((ctl >> 4) & 3)
                    {
                    case 0:
                        count += 2;
                        FillBytes (dst, (byte)m_input.ReadByte(), count);
                        break;
                    case 1:
                        ++count;
                        Binary.CopyOverlapped (m_output, dst-m_stride, dst, count);
                        break;
                    case 2:
                        ++count;
                        Binary.CopyOverlapped (m_output, dst-2*m_stride, dst, count);
                        break;
                    case 3:
                        ++count;
                        Binary.CopyOverlapped (m_output, dst-4*m_stride, dst, count);
                        break;
                    }
                }
                else if (0 == (ctl & 0x30))
                {
                    count = m_input.ReadByte() + ((ctl & 7) << 8) + 1;
                    int x = m_pixel_size;
                    if (0 != (ctl & 8))
                        x *= 2;
                    m_input.Read (m_output, dst, x);
                    Binary.CopyOverlapped (m_output, dst, dst+x, count*x);
                    ++count;
                    count *= x;
                }
                else if (0 == (ctl & 0x20))
                {
                    int offset = m_input.ReadByte() + ((ctl & 0xF) << 8) + 1;
                    count = m_input.ReadByte() + 1;
                    int src = dst - m_pixel_size * offset;
                    count = Math.Min (count * m_pixel_size, m_output.Length - dst);
                    Binary.CopyOverlapped (m_output, src, dst, count);
                }
                else
                {
                    int offset = m_input.ReadByte() + ((ctl & 0xF) << 8) + 1;
                    count = m_input.ReadByte() + 1;
                    int src = dst - offset;
                    Binary.CopyOverlapped (m_output, src, dst, count);
                }
                if (0 == count)
                    throw new InvalidFormatException();
                dst += count;
            }
        }

        private byte[] UnpackBytes () // sub_403ED0
        {
            int total = m_width * m_height;
            var output = new byte[total + m_width];
            int dst = 0;
            while ( dst < total )
            {
                int count;
                int next = m_input.ReadByte();
                if (-1 == next)
                    break;
                if ((next & 0xF0) == 0xF0)
                    throw new InvalidFormatException();

                if (0 == (next & 0x80))
                {
                    if (0 != (next & 0x40))
                    {
                        count = (next & 0x3F) + 2;
                        byte v = (byte)m_input.ReadByte();
                        for (int i = 0; i < count; ++i)
                            output[dst+i] = v;
                    }
                    else
                    {
                        count = (next & 0x3F) + 1;
                        if (count != m_input.Read (output, dst, count))
                            break;
                    }
                }
                else if (0 == (next & 0x40))
                {
                    count = m_input.ReadByte() | ((next & 0xF) << 8);
                    switch ((next >> 4) & 3)
                    {
                    case 0:
                        {
                            count += 2;
                            byte v = (byte)m_input.ReadByte();
                            for (int i = 0; i < count; ++i)
                                output[dst+i] = v;
                            break;
                        }
                    case 1:
                        ++count;
                        Binary.CopyOverlapped (output, dst-m_width, dst, count);
                        break;
                    case 2:
                        ++count;
                        Binary.CopyOverlapped (output, dst-2*m_width, dst, count);
                        break;
                    case 3:
                        ++count;
                        Binary.CopyOverlapped (output, dst-4*m_width, dst, count);
                        break;
                    }
                }
                else if (0 == (next & 0x20))
                {
                    count = m_input.ReadByte() + ((next & 7) << 8) + 1;
                    switch ((next >> 3) & 3)
                    {
                    case 0:
                        m_input.Read (output, dst, 2);
                        Binary.CopyOverlapped (output, dst, dst+2, count*2);
                        ++count;
                        count *= 2;
                        break;
                    case 1:
                        m_input.Read (output, dst, 4);
                        Binary.CopyOverlapped (output, dst, dst+4, count*4);
                        ++count;
                        count *= 4;
                        break;
                    case 2:
                        m_input.Read (output, dst, 8);
                        Binary.CopyOverlapped (output, dst, dst+8, count*8);
                        ++count;
                        count *= 8;
                        break;
                    case 3:
                        m_input.Read (output, dst, 16);
                        Binary.CopyOverlapped (output, dst, dst+16, count*16);
                        ++count;
                        count *= 16;
                        break;
                    }
                }
                else
                {
                    int offset = m_input.ReadByte() | ((next & 0xF) << 8);
                    count = m_input.ReadByte() + 1;
                    int src = dst - 1 - offset;
                    Binary.CopyOverlapped (output, src, dst, count);
                }
                dst += count;
            }
            return output;
        }

        private void FlipPixels ()
        {
            // flip pixels vertically
            var pixels = new byte[m_output.Length];
            int dst = 0;
            for (int src = m_stride * (m_height-1); src >= 0; src -= m_stride)
            {
                Buffer.BlockCopy (m_output, src, pixels, dst, m_stride);
                dst += m_stride;
            }
            m_output = pixels;
        }

        void FillBytes (int dst, byte value, int count)
        {
            for (int i = 0; i < count; ++i)
                m_output[dst+i] = value;
        }
    }
}
