//! \file       ArcGRP.cs
//! \date       Sun Mar 20 02:07:17 2016
//! \brief      Ankh resource archive.
//
// 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;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using GameRes.Compression;
using GameRes.Utility;

namespace GameRes.Formats.Ankh
{
    [Export(typeof(ArchiveFormat))]
    [ExportMetadata("Priority", -1)]
    public class GrpOpener : ArchiveFormat
    {
        public override string         Tag { get { return "GRP/ICE"; } }
        public override string Description { get { return "Ice Soft resource archive"; } }
        public override uint     Signature { get { return 0; } }
        public override bool  IsHierarchic { get { return false; } }
        public override bool      CanWrite { get { return false; } }

        public GrpOpener ()
        {
            Extensions = new string[] { "grp", "bin", "dat", "vc" };
        }

        public override ArcFile TryOpen (ArcView file)
        {
            uint first_offset = file.View.ReadUInt32 (0);
            if (first_offset < 8 || first_offset >= file.MaxOffset || 0 != (first_offset & 3))
                return null;
            int count = (int)(first_offset - 4) / 4;
            if (!IsSaneCount (count))
                return null;

            var base_name = Path.GetFileNameWithoutExtension (file.Name);
            uint index_offset = 0;
            uint next_offset = first_offset;
            var dir = new List<Entry> (count);
            for (int i = 0; i < count && next_offset < file.MaxOffset; ++i)
            {
                var entry = new PackedEntry { Offset = next_offset };
                index_offset += 4;
                next_offset = file.View.ReadUInt32 (index_offset);
                if (next_offset < entry.Offset)
                    return null;
                entry.Size = (uint)(next_offset - entry.Offset);
                entry.UnpackedSize = entry.Size;
                if (entry.Size != 0)
                {
                    if (!entry.CheckPlacement (file.MaxOffset))
                        return null;
                    entry.Name = string.Format ("{0}#{1:D4}", base_name, i);
                    dir.Add (entry);
                }
            }
            if (0 == dir.Count)
                return null;
            DetectFileTypes (file, dir);
            return new ArcFile (file, this, dir);
        }

        internal void DetectFileTypes (ArcView file, List<Entry> dir)
        {
            var header = new byte[16];
            foreach (PackedEntry entry in dir)
            {
                if (entry.Size <= 8)
                    continue;
                file.View.Read (entry.Offset, header, 0, 16);
                if (header.AsciiEqual ("TPW"))
                {
                    entry.IsPacked =header[3] != 0;
                    long start_offset = entry.Offset+4;
                    if (entry.IsPacked)
                    {
                        entry.UnpackedSize = file.View.ReadUInt32 (start_offset);
                        start_offset = entry.Offset+11;
                    }
                    else
                    {
                        entry.Offset = start_offset;
                        entry.Size -= 4;
                    }
                    if (file.View.AsciiEqual (start_offset, "BM"))
                        entry.ChangeType (ImageFormat.Bmp);
                }
                else if (header.AsciiEqual (4, "HDJ\0"))
                {
                    if (header.AsciiEqual (12, "BM"))
                        entry.ChangeType (ImageFormat.Bmp);
                    else if (header.AsciiEqual (12, "MThd"))
                        entry.Name = Path.ChangeExtension (entry.Name, "mid");

                    entry.UnpackedSize = header.ToUInt32 (0);
                    entry.IsPacked = true;
                }
                else if (header.AsciiEqual (0, "zfd "))
                {
                    entry.ChangeType (ImageFormat.Tga);
                    entry.UnpackedSize = header.ToUInt32 (4);
                    entry.IsPacked = true;
                }
                else if (header.AsciiEqual (4, "OggS"))
                {
                    entry.ChangeType (OggAudio.Instance);
                    entry.Offset += 4;
                    entry.Size   -= 4;
                }
                else if (entry.Size > 12 &&
                         (header.AsciiEqual (8, "RIFF") ||
                          ((header[4] & 0xF) == 0xF && header.AsciiEqual (5, "RIFF"))))
                {
                    entry.ChangeType (AudioFormat.Wav);
                    entry.UnpackedSize = header.ToUInt32 (0);
                    entry.IsPacked = true;
                }
                else
                {
                    uint signature = header.ToUInt32 (0);
                    var res = AutoEntry.DetectFileType (signature);
                    if (res != null)
                    {
                        entry.ChangeType (res);
                    }
                    else if ((signature & 0xFFFF) == 0xFBFF)
                    {
                        entry.ChangeType (Mp3Format.Value);
                    }
                    else if (entry.Size > 0x16 && IsAudioEntry (file, entry))
                    {
                        entry.Type = "audio";
                    }
                }
            }
        }

        internal static ResourceInstance<AudioFormat> Mp3Format = new ResourceInstance<AudioFormat> ("MP3");

        bool IsAudioEntry (ArcView file, Entry entry)
        {
            uint signature = file.View.ReadUInt32 (entry.Offset);
            if (signature != 0x010001 && signature != 0x020001)
                return false;
            int extra = file.View.ReadUInt16 (entry.Offset+0x10);
            if (extra != 0)
                return false;
            uint size = file.View.ReadUInt32 (entry.Offset+0x12);
            return 0x16 + size == entry.Size;
        }

        public override Stream OpenEntry (ArcFile arc, Entry entry)
        {
            var pent = entry as PackedEntry;
            if (pent != null && pent.IsPacked && pent.Size > 8)
            {
                try
                {
                    if (arc.File.View.AsciiEqual (entry.Offset, "TPW"))
                        return OpenTpw (arc, pent);
                    if (arc.File.View.AsciiEqual (entry.Offset+4, "HDJ\0"))
                        return OpenImage (arc, pent);
                    if (arc.File.View.AsciiEqual (entry.Offset, "zfd "))
                        return OpenZfd (arc, pent);
                    if (entry.Size > 12)
                    {
                        byte type = arc.File.View.ReadByte (entry.Offset+4);
                        if ('W' == type
                            && arc.File.View.AsciiEqual (entry.Offset+8, "RIFF"))
                            return OpenAudio (arc, entry);
                        if ((type & 0xF) == 0xF
                            && arc.File.View.AsciiEqual (entry.Offset+5, "RIFF"))
                        {
                            var input = arc.File.CreateStream (entry.Offset+4, entry.Size-4);
                            return new LzssStream (input);
                        }
                    }
                }
                catch (Exception X)
                {
                    System.Diagnostics.Trace.WriteLine (X.Message, "[GRP]");
                }
            }
            return base.OpenEntry (arc, entry);
        }

        Stream OpenImage (ArcFile arc, PackedEntry entry)
        {
            using (var packed = arc.File.CreateStream (entry.Offset+8, entry.Size-8))
            using (var reader = new GrpUnpacker (packed))
            {
                var unpacked = new byte[entry.UnpackedSize];
                reader.UnpackHDJ (unpacked, 0);
                return new BinMemoryStream (unpacked, entry.Name);
            }
        }

        Stream OpenAudio (ArcFile arc, Entry entry)
        {
            int unpacked_size = arc.File.View.ReadInt32 (entry.Offset);
            byte pack_type = arc.File.View.ReadByte (entry.Offset+5);
            byte channels = arc.File.View.ReadByte (entry.Offset+6);
            byte header_size = arc.File.View.ReadByte (entry.Offset+7);
            if (unpacked_size <= 0 || header_size > unpacked_size
                || !('A' == pack_type || 'S' == pack_type))
                return base.OpenEntry (arc, entry);
            var unpacked = new byte[unpacked_size];
            arc.File.View.Read (entry.Offset+8, unpacked, 0, header_size);
            uint packed_size = entry.Size - 8 - header_size;
            using (var packed = arc.File.CreateStream (entry.Offset+8+header_size, packed_size))
            using (var reader = new GrpUnpacker (packed))
            {
                if ('A' == pack_type)
                    reader.UnpackA (unpacked, header_size, channels);
                else
                    reader.UnpackS (unpacked, header_size, channels);
                return new BinMemoryStream (unpacked, entry.Name);
            }
        }

        Stream OpenTpw (ArcFile arc, PackedEntry entry)
        {
            using (var input = arc.File.CreateStream (entry.Offset, entry.Size))
            {
                var output = new byte[entry.UnpackedSize];
                UnpackTpw (input, output);
                return new BinMemoryStream (output, entry.Name);
            }
        }

        Stream OpenZfd (ArcFile arc, PackedEntry entry)
        {
            var input = arc.File.CreateStream (entry.Offset+8, entry.Size-8);
            return new ZLibStream (input, CompressionMode.Decompress);
        }

        internal static void UnpackTpw (IBinaryStream input, byte[] output)
        {
            input.Position = 8;
            var offsets = new int[4];
            offsets[0] = input.ReadUInt16();
            offsets[1] = offsets[0] * 2;
            offsets[2] = offsets[0] * 3;
            offsets[3] = offsets[0] * 4;
            int dst = 0;
            while (dst < output.Length)
            {
                byte ctl = input.ReadUInt8();
                if (0 == ctl)
                    break;
                int remaining = output.Length - dst;
                int count;
                if (ctl < 0x40)
                {
                    count = Math.Min (ctl, remaining);
                    input.Read (output, dst, count);
                    dst += count;
                }
                else if (ctl <= 0x6F)
                {
                    if (0x6F == ctl)
                        count = input.ReadUInt16();
                    else
                        count = (byte)(ctl - 0x3D);
                    count = Math.Min (count, remaining);
                    byte v = input.ReadUInt8();
                    while (count --> 0)
                        output[dst++] = v;
                }
                else if (ctl <= 0x9F)
                {
                    if (ctl == 0x9F)
                        count = input.ReadUInt16();
                    else 
                        count = (byte)(ctl - 0x6E);
                    dst += input.Read (output, dst, Math.Min (2, remaining));
                    --count;
                    if (count > 0 && remaining > 2)
                    {
                        count = Math.Min (count * 2, remaining - 2);
                        Binary.CopyOverlapped (output, dst-2, dst, count);
                        dst += count;
                    }
                }
                else if (ctl <= 0xBF)
                {
                    if (ctl == 0xBF)
                        count = input.ReadUInt16();
                    else
                        count = (byte)(ctl - 0x9E);
                    dst += input.Read (output, dst, Math.Min (3, remaining));
                    --count;
                    if (count > 0 && remaining > 3)
                    {
                        count = Math.Min (count * 3, remaining - 3);
                        Binary.CopyOverlapped (output, dst-3, dst, count);
                        dst += count;
                    }
                }
                else
                {
                    count = Math.Min ((ctl & 0x3F) + 3, remaining);
                    int offset = input.ReadUInt8();
                    offset = (offset & 0x3F) - offsets[offset >> 6];
                    Binary.CopyOverlapped (output, dst+offset, dst, count);
                    dst += count;
                }
            }
        }
    }

    internal sealed class GrpUnpacker : IDisposable
    {
        IBinaryStream       m_input;
        uint                m_bits;
        int                 m_cached_bits;

        public GrpUnpacker (IBinaryStream input)
        {
            m_input = input;
        }

        // Different games have slightly different formats using exact same headers,
        // so have to invent some flawed format recognition here.
        enum GrpVariant { Default, BoD };

        static GrpVariant LastUsedMethod = GrpVariant.Default;

        static GrpVariant GetOppositeVariant ()
        {
            return GrpVariant.Default == LastUsedMethod ? GrpVariant.BoD : GrpVariant.Default;
        }

        public void UnpackHDJ (byte[] output, int dst)
        {
            try
            {
                if (UnpackHDJVariant (output, dst, LastUsedMethod))
                    return;
            }
            catch { /* ignore unpack errors */ }
            var method = GetOppositeVariant();
            m_input.Position = 0;
            if (!UnpackHDJVariant (output, dst, method))
                throw new InvalidFormatException();
            LastUsedMethod = method;
        }

        private bool UnpackHDJVariant (byte[] output, int dst, GrpVariant method)
        {
            ResetBits();
            int word_count = 0;
            int byte_count = 0;
            uint next_byte = 0;
            uint next_word = 0;
            while (dst < output.Length)
            {
                if (GetNextBit() != 0)
                {
                    int count = 0;
                    bool long_count = false;
                    int offset;
                    if (GetNextBit() != 0)
                    {
                        if (0 == word_count)
                        {
                            next_word = m_input.ReadUInt32();
                            word_count = 2;
                        }
                        count = (int)((next_word >> 13) & 7) + 3;
                        offset = (int)(next_word | 0xFFFFE000);
                        next_word >>= 16;
                        --word_count;
                        long_count = 10 == count;
                    }
                    else
                    {
                        if (method == GrpVariant.Default)
                            count = GetBits (2);
                        if (0 == byte_count)
                        {
                            next_byte = m_input.ReadUInt32();
                            byte_count = 4;
                        }
                        if (method != GrpVariant.Default)
                            count = GetBits (2);
                        count += 2;
                        long_count = 5 == count;
                        offset = (int)(next_byte | 0xFFFFFF00);
                        next_byte >>= 8;
                        --byte_count;
                    }
                    if (long_count)
                    {
                        int n = 0;
                        while (GetNextBit() != 0)
                            ++n;

                        if (n != 0)
                            count += GetBits (n) + 1;
                    }
                    int src = dst + offset;
                    if (src < 0 || src >= dst || dst + count > output.Length)
                        return false;
                    Binary.CopyOverlapped (output, src, dst, count);
                    dst += count;
                }
                else
                {
                    if (0 == byte_count)
                    {
                        next_byte = m_input.ReadUInt32();
                        byte_count = 4;
                    }
                    output[dst++] = (byte)next_byte;
                    next_byte >>= 8;
                    --byte_count;
                }
            }
            return true;
        }

        public void UnpackS (byte[] output, int dst, int channels)
        {
            try
            {
                if (UnpackSVariant (output, dst, channels, LastUsedMethod))
                    return;
            }
            catch { /* ignore parse errors */ }
            var method = GetOppositeVariant();
            m_input.Position = 0;
            if (UnpackSVariant (output, dst, channels, method))
                LastUsedMethod = method;
        }

        bool UnpackSVariant (byte[] output, int dst, int channels, GrpVariant method)
        {
            if (GrpVariant.Default == method)
                UnpackSv2 (output, dst, channels);
            else
                UnpackSv1 (output, dst, channels);
            return m_input.PeekByte() == -1; // rather loose test, but whatever
        }

        void UnpackSv1 (byte[] output, int dst, int channels)
        {
            ResetBits();
            short last_word = 0;
            while (dst < output.Length)
            {
                int word;
                if (GetNextBit() != 0)
                {
                    if (GetNextBit() != 0)
                        word = GetBits (10) << 6;
                    else
                        word = 0;
                }
                else
                {
                    int adjust = GetBits (5) << 6;
                    if (0 != (adjust & 0x400))
                        adjust = -(adjust & 0x3FF);
                    word = last_word + adjust;
                }
                last_word = (short)word;
                LittleEndian.Pack (last_word, output, dst);
                dst += 2;
            }
        }

        void UnpackSv2 (byte[] output, int dst, int channels)
        {
            if (channels != 1)
                m_input.Seek ((channels-1) * 4, SeekOrigin.Current);
            int step = channels * 2;
            for (int i = 0; i < channels; ++i)
            {
                ResetBits();
                int pos = dst;
                short last_word = 0;
                while (pos < output.Length)
                {
                    int word;
                    if (GetNextBit() != 0)
                    {
                        if (GetNextBit() != 0)
                        {
                            word = GetBits (10) << 6;
                        }
                        else
                        {
                            int repeat;
                            if (GetNextBit() != 0)
                            {
                                int bit_length = 0;
                                do
                                {
                                    ++bit_length;
                                }
                                while (GetNextBit() != 0);
                                repeat = GetBits (bit_length) + 4;
                            }
                            else
                            {
                                repeat = GetBits (2);
                            }
                            word = 0;
                            while (repeat --> 0)
                            {
                                output[pos]   = 0;
                                output[pos+1] = 0;
                                pos += step;
                            }
                        }
                    }
                    else
                    {
                        int adjust = (short)(GetBits (5) << 11) >> 5;
                        word = last_word + adjust;
                    }
                    LittleEndian.Pack ((short)word, output, pos);
                    last_word = (short)word;
                    pos += step;
                }
                dst += 2;
            }
        }

        public void UnpackA (byte[] output, int dst, int channels)
        {
            if (channels != 1)
                m_input.Seek ((channels-1) * 4, SeekOrigin.Current);
            int step = 2 * channels;
            for (int i = 0; i < channels; ++i)
            {
                int pos = dst;
                ResetBits();
                while (pos < output.Length)
                {
                    int word = GetBits (10) << 6;;
                    LittleEndian.Pack ((short)word, output, pos);
                    pos += step;
                }
                dst += 2;
            }
        }

        void ResetBits ()
        {
            m_cached_bits = 0;
        }

        int GetNextBit ()
        {
            return GetBits (1);
        }

        int GetBits (int count)
        {
            if (0 == m_cached_bits)
            {
                m_bits = m_input.ReadUInt32();
                m_cached_bits = 32;
            }
            uint val;
            if (m_cached_bits < count)
            {
                uint next_bits = m_input.ReadUInt32();
                val = (m_bits | (next_bits >> m_cached_bits)) >> (32 - count);
                m_bits = next_bits << (count - m_cached_bits);
                m_cached_bits = 32 - (count - m_cached_bits);
            }
            else
            {
                val = m_bits >> (32 - count);
                m_bits <<= count;
                m_cached_bits -= count;
            }
            return (int)val;
        }

        #region IDisposable Members
        bool _disposed = false;
        public void Dispose ()
        {
            if (!_disposed)
            {
                m_input.Dispose();
                _disposed = true;
            }
        }
        #endregion
    }
}
