/******************************************************
 *     ROMVault2 is written by Gordon J.              *
 *     Contact gordon@romvault.com                    *
 *     Copyright 2014                                 *
 ******************************************************/

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using ROMVault2.IO;
using File = System.IO.File;
using Path = System.IO.Path;

namespace ROMVault2.SupportedFiles.CHD
{
    public static class CHD
    {
        private const int MaxHeader = 124;

        public static int CheckFile(IO.FileInfo ofile, out byte[] SHA1CHD, out byte[] MD5CHD, out uint? version)
        {

            SHA1CHD = null;
            MD5CHD = null;
            version = null;

            string ext = IO.Path.GetExtension(ofile.Name).ToLower();
            if (ext != ".chd")
                return 0;

            
            if (ofile.Length < MaxHeader)
                return 0;


            Stream s;
            int retval = IO.FileStream.OpenFileRead(ofile.FullName, out s);
            if (retval != 0)
                return retval;
            if (s == null) return 1;

            CheckFile(s, out SHA1CHD, out MD5CHD, out version);

            s.Close();
            s.Dispose();

            return 0;
        }


        private static void CheckFile(Stream s, out  byte[] SHA1CHD, out  byte[] MD5CHD, out uint? version)
        {
            using (BinaryReader br = new BinaryReader(s))
            {
                byte[] buff = br.ReadBytes(MaxHeader);

                CheckFile(buff, out SHA1CHD, out MD5CHD, out version);
            }
        }

        private static void CheckFile(byte[] buff, out byte[] SHA1CHD, out byte[] MD5CHD, out uint? version)
        {

            SHA1CHD = null;
            MD5CHD = null;
            version = null;
            UInt32 compression = 0;

            byte[] header = new[] { (byte)'M', (byte)'C', (byte)'o', (byte)'m', (byte)'p', (byte)'r', (byte)'H', (byte)'D' };

            for (int i = 0; i < header.Length; i++)
            {
                if (buff[i] != header[i])
                    return;
            }

            UInt32 length = ReadUInt32(buff, 8);
            version = ReadUInt32(buff, 12);

            if (version > 5)
                return;

            switch (version)
            {
                case 1:
                    if (length != 76) return;

                    //    V1 header:
                    //    [  0] char   tag[8];        // 'MComprHD'
                    //    [  8] UINT32 length;        // length of header (including tag and length fields)
                    //    [ 12] UINT32 version;       // drive format version
                    //    [ 16] UINT32 flags;         // flags (see below)
                    //    [ 20] UINT32 compression;   // compression type
                    compression = ReadUInt32(buff, 20);
                    if (compression == 0) return;

                    //    [ 24] UINT32 hunksize;      // 512-byte sectors per hunk
                    //    [ 28] UINT32 totalhunks;    // total # of hunks represented
                    //    [ 32] UINT32 cylinders;     // number of cylinders on hard disk
                    //    [ 36] UINT32 heads;         // number of heads on hard disk
                    //    [ 40] UINT32 sectors;       // number of sectors on hard disk
                    //    [ 44] UINT8  md5[16];       // MD5 checksum of raw data
                    MD5CHD = ReadBytes(buff, 44, 16);
                    //    [ 60] UINT8  parentmd5[16]; // MD5 checksum of parent file
                    //    [ 76] (V1 header length)

                    return;

                case 2:
                    if (length != 80) return;

                    //    V2 header:
                    //    [  0] char   tag[8];        // 'MComprHD'
                    //    [  8] UINT32 length;        // length of header (including tag and length fields)
                    //    [ 12] UINT32 version;       // drive format version
                    //    [ 16] UINT32 flags;         // flags (see below)
                    //    [ 20] UINT32 compression;   // compression type
                    compression = ReadUInt32(buff, 20);
                    if (compression == 0) return;

                    //    [ 24] UINT32 hunksize;      // seclen-byte sectors per hunk
                    //    [ 28] UINT32 totalhunks;    // total # of hunks represented
                    //    [ 32] UINT32 cylinders;     // number of cylinders on hard disk
                    //    [ 36] UINT32 heads;         // number of heads on hard disk
                    //    [ 40] UINT32 sectors;       // number of sectors on hard disk
                    //    [ 44] UINT8  md5[16];       // MD5 checksum of raw data
                    MD5CHD = ReadBytes(buff, 44, 16);
                    //    [ 60] UINT8  parentmd5[16]; // MD5 checksum of parent file
                    //    [ 76] UINT32 seclen;        // number of bytes per sector
                    //    [ 80] (V2 header length)
                    return;

                case 3:
                    if (length != 120) return;

                    //    V3 header:
                    //    [  0] char   tag[8];        // 'MComprHD'
                    //    [  8] UINT32 length;        // length of header (including tag and length fields)
                    //    [ 12] UINT32 version;       // drive format version
                    //    [ 16] UINT32 flags;         // flags (see below)
                    //    [ 20] UINT32 compression;   // compression type
                    compression = ReadUInt32(buff, 20);
                    if (compression == 0) return;
                    //    [ 24] UINT32 totalhunks;    // total # of hunks represented
                    //    [ 28] UINT64 logicalbytes;  // logical size of the data (in bytes)
                    //    [ 36] UINT64 metaoffset;    // offset to the first blob of metadata
                    //    [ 44] UINT8  md5[16];       // MD5 checksum of raw data
                    MD5CHD = ReadBytes(buff, 44, 16);
                    //    [ 60] UINT8  parentmd5[16]; // MD5 checksum of parent file
                    //    [ 76] UINT32 hunkbytes;     // number of bytes per hunk
                    //    [ 80] UINT8  sha1[20];      // SHA1 checksum of raw data
                    SHA1CHD = ReadBytes(buff, 80, 20);
                    //    [100] UINT8  parentsha1[20];// SHA1 checksum of parent file
                    //    [120] (V3 header length)
                    return;

                case 4:
                    if (length != 108) return;

                    //    V4 header:
                    //    [  0] char   tag[8];        // 'MComprHD'
                    //    [  8] UINT32 length;        // length of header (including tag and length fields)
                    //    [ 12] UINT32 version;       // drive format version
                    //    [ 16] UINT32 flags;         // flags (see below)
                    //    [ 20] UINT32 compression;   // compression type
                    compression = ReadUInt32(buff, 20);
                    if (compression == 0) return;
                    //    [ 24] UINT32 totalhunks;    // total # of hunks represented
                    //    [ 28] UINT64 logicalbytes;  // logical size of the data (in bytes)
                    //    [ 36] UINT64 metaoffset;    // offset to the first blob of metadata
                    //    [ 44] UINT32 hunkbytes;     // number of bytes per hunk
                    //    [ 48] UINT8  sha1[20];      // combined raw+meta SHA1
                    SHA1CHD = ReadBytes(buff, 48, 20);
                    //    [ 68] UINT8  parentsha1[20];// combined raw+meta SHA1 of parent
                    //    [ 88] UINT8  rawsha1[20];   // raw data SHA1
                    //    [108] (V4 header length)
                    return;

                case 5:
                    if (length != 124) return;

                    //    V5 header:
                    //    [  0] char   tag[8];        // 'MComprHD'
                    //    [  8] UINT32 length;        // length of header (including tag and length fields)
                    //    [ 12] UINT32 version;       // drive format version
                    //    [ 16] UINT32 compressors[4];// which custom compressors are used?
                    UInt32 compression0 = ReadUInt32(buff, 16);
                    UInt32 compression1 = ReadUInt32(buff, 20);
                    UInt32 compression2 = ReadUInt32(buff, 24);
                    UInt32 compression3 = ReadUInt32(buff, 28);
                    if (compression0 == 0 && compression1 == 0 && compression2 == 0 && compression3 == 0) return;
                    //    [ 32] UINT64 logicalbytes;  // logical size of the data (in bytes)
                    //    [ 40] UINT64 mapoffset;     // offset to the map
                    //    [ 48] UINT64 metaoffset;    // offset to the first blob of metadata
                    //    [ 56] UINT32 hunkbytes;     // number of bytes per hunk (512k maximum)
                    //    [ 60] UINT32 unitbytes;     // number of bytes per unit within each hunk
                    //    [ 64] UINT8  rawsha1[20];   // raw data SHA1
                    //    [ 84] UINT8  sha1[20];      // combined raw+meta SHA1
                    SHA1CHD = ReadBytes(buff, 84, 20);
                    //    [104] UINT8  parentsha1[20];// combined raw+meta SHA1 of parent
                    //    [124] (V5 header length)
                    return;
            }
        }


        private static UInt32 ReadUInt32(byte[] b, int p)
        {
            return
                ((UInt32)b[p + 0]) << 24 |
                ((UInt32)b[p + 1]) << 16 |
                ((UInt32)b[p + 2]) << 8 |
                (b[p + 3]);
        }

        private static byte[] ReadBytes(byte[] b, int p, int length)
        {
            byte[] ret = new byte[length];
            for (int i = 0; i < length; i++)
                ret[i] = b[p + i];
            return ret;
        }

        private static string _result;
        private static BackgroundWorker _bgw;
        private static CHDManCheck _resultType;

        public enum CHDManCheck
        {
            Unset,
            Good,
            Corrupt,
            CHDNotFound,
            ChdmanNotFound,
            CHDUnknownError,
            CHDReturnError
        }



        public static CHDManCheck ChdmanCheck(string filename, BackgroundWorker bgw, out string result)
        {
            _bgw = bgw;
            _result = "";
            _resultType = CHDManCheck.Unset;

            string chdExe = "chdman.exe";
            if (Settings.IsUnix)
                chdExe = "chdman";

            string chdPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, chdExe);
            if (!File.Exists(chdPath))
            {
                result = chdExe + " Not Found.";
                return CHDManCheck.ChdmanNotFound;
            }

            if (!IO.File.Exists(filename))
            {
                result = filename + " Not Found.";
                return CHDManCheck.CHDNotFound;
            }

            string shortName = NameFix.GetShortPath(filename);


            using (Process exeProcess = new Process())
            {
                exeProcess.StartInfo.FileName = chdPath;
                ReportError.LogOut("CHD: FileName :" + exeProcess.StartInfo.FileName);


                exeProcess.StartInfo.Arguments = "verify -i \"" + shortName + "\"";
                ReportError.LogOut("CHD: Arguments :" + exeProcess.StartInfo.Arguments);

                // Set UseShellExecute to false for redirection.
                exeProcess.StartInfo.UseShellExecute = false;
                // Stops the Command window from popping up.
                exeProcess.StartInfo.CreateNoWindow = true;

                // Redirect the standard output.
                // This stream is read asynchronously using an event handler.
                exeProcess.StartInfo.RedirectStandardOutput = true;
                exeProcess.StartInfo.RedirectStandardError = true;

                // Set our event handler to asynchronously read the process output.
                exeProcess.OutputDataReceived += CHDOutputHandler;
                exeProcess.ErrorDataReceived += CHDErrorHandler;

                _outputLineCount = 0;
                _errorLines=0;

                ReportError.LogOut("CHD: Scanning Starting");
                exeProcess.Start();

                // Start the asynchronous read of the process output stream.
                exeProcess.BeginOutputReadLine();
                exeProcess.BeginErrorReadLine();

                // Wait for the process finish.
                exeProcess.WaitForExit();
                ReportError.LogOut("CHD: Scanning Finished");

            }

            result = _result;

            if (_resultType == CHDManCheck.Unset)
                _resultType = CHDManCheck.Good;

            _bgw.ReportProgress(0, new bgwText3(""));

            ReportError.LogOut("CHD: returning result " + _resultType + " " + result);

            return _resultType;
        }

        /*
        chdman - MAME Compressed Hunks of Data (CHD) manager 0.147 (Sep 18 2012)
        Raw SHA1 verification successful!
        Overall SHA1 verification successful!
        */

        private static int _outputLineCount;

        private static void CHDOutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
        {
            // Collect the process command output.
            if (String.IsNullOrEmpty(outLine.Data)) return;

            string sOut = outLine.Data;
            //ReportError.LogOut("CHDOutput: " + _outputLineCount + " : " + sOut);
            switch (_outputLineCount)
            {
                case 0:
                    if (!System.Text.RegularExpressions.Regex.IsMatch(sOut, @"^chdman - MAME Compressed Hunks of Data \(CHD\) manager ([0-9\.]+) \(.*\)")) 
                    {
                        _result = "Incorrect startup of CHDMan :" + sOut;
                        _resultType = CHDManCheck.CHDReturnError;
                    }
                    break;
                case 1:
                    if (sOut != "Raw SHA1 verification successful!")
                    {
                        _result = "Raw SHA1 check failed :" + sOut;
                        _resultType = CHDManCheck.CHDReturnError;
                    }
                    break;
                case 2:
                    if (sOut != "Overall SHA1 verification successful!")
                    {
                        _result = "Overall SHA1 check failed :" + sOut;
                        _resultType = CHDManCheck.CHDReturnError;
                    }
                    break;
                default:
                    _result = "Unexpected output from chdman :" + sOut;
                    _resultType = CHDManCheck.CHDUnknownError;
                    break;
            }

            _outputLineCount++;
        }

        private static int _errorLines;

        private static void CHDErrorHandler(object sendingProcess, DataReceivedEventArgs outLine)
        {
            // Collect the process command output.
            if (String.IsNullOrEmpty(outLine.Data)) return;

            // We can get fed multiple lines worth of data because of \r line feeds
            string[] sLines = outLine.Data.Split(new string[] { "\r" }, StringSplitOptions.None);

            foreach (string sLine in sLines) {

                if (string.IsNullOrEmpty(sLine)) continue;

                ReportError.LogOut("CHDError: " + sLine);
                _bgw.ReportProgress(0, new bgwText3(sLine));
 
                if (_resultType != CHDManCheck.Unset)
                {
                    if (_errorLines>0)
                    {
                        _errorLines -= 1;
                        _result += "\r\n" + sLine;
                    }
                }
                else if (System.Text.RegularExpressions.Regex.IsMatch(sLine, @"^No verification to be done; CHD has (uncompressed|no checksum)"))
                {
                    _result = sLine;
                    _resultType = CHDManCheck.Corrupt;
                } 
                else if (System.Text.RegularExpressions.Regex.IsMatch(sLine, @"^Error (opening|reading) CHD file.*")) 
                {
                     _result = sLine;
                    _resultType = CHDManCheck.Corrupt;
                }
                else if (System.Text.RegularExpressions.Regex.IsMatch(sLine, @"^Error opening parent CHD file .*:")) 
                {
                    _result = sLine;
                    _resultType = CHDManCheck.Corrupt;
                }
                else if (System.Text.RegularExpressions.Regex.IsMatch(sLine, @"^Error: (Raw|Overall) SHA1 in header"))
                {
                    _result = sLine;
                    _resultType = CHDManCheck.Corrupt;
                }
                else if (System.Text.RegularExpressions.Regex.IsMatch(sLine, @"^Out of memory"))
                {
                    _result = sLine;
                    _resultType = CHDManCheck.Corrupt;
                }
                // Verifying messages are a non-error
                else if (System.Text.RegularExpressions.Regex.IsMatch(sLine, @"Verifying, \d+\.\d+\% complete\.\.\."))
                {

                }
                else
                {
                    _result = "Unknown message : " + sLine;
                    _resultType = CHDManCheck.CHDUnknownError;
                }
            }
        }


    }
}
