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

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Xml;
using ROMVault2.RvDB;
using ROMVault2.Utils;

namespace ROMVault2.DatReaders
{
    public static class DatReader
    {
        private static BackgroundWorker _bgw;

        public static RvDir ReadInDatFile(RvDat datFile, BackgroundWorker bgw)
        {
            _bgw = bgw;

            RvDir newDir = new RvDir(FileType.Dir);

            string datFullName = datFile.GetData(RvDat.DatData.DatFullName);

            System.IO.Stream fs;
            int errorCode = IO.FileStream.OpenFileRead(datFullName, out fs);
            if (errorCode != 0)
            {
                _bgw.ReportProgress(0, new bgwShowError(datFullName, errorCode + ": " + new Win32Exception(errorCode).Message));
                return null;
            }

            System.IO.StreamReader myfile = new System.IO.StreamReader(fs, Program.Enc);
            string strLine = myfile.ReadLine();
            myfile.Close();
            fs.Close();
            fs.Dispose();

            if (strLine == null)
                return null;

            if (strLine.ToLower().IndexOf("xml", StringComparison.Ordinal) >= 0)
            {
                if (!ReadXMLDat(ref newDir, datFullName))
                    return null;
            }

            else if (strLine.ToLower().IndexOf("clrmamepro", StringComparison.Ordinal) >= 0 || strLine.ToLower().IndexOf("romvault", StringComparison.Ordinal) >= 0 || strLine.ToLower().IndexOf("game", StringComparison.Ordinal) >= 0)
            {
                if (!DatCmpReader.ReadDat(ref newDir, datFullName))
                    return null;
            }
            else if (strLine.ToLower().IndexOf("doscenter", StringComparison.Ordinal) >= 0)
            {
                if (!DatDOSReader.ReadDat(ref newDir, datFullName))
                    return null;
            }
            else
            {
                _bgw.ReportProgress(0, new bgwShowError(datFullName, "Invalid DAT File"));
                return null;
            }

            if (newDir.Dat == null)
            {
                _bgw.ReportProgress(0, new bgwShowError(datFullName, "Invalid Header"));
                return null;
            }

            newDir.Dat.AddData(RvDat.DatData.DatFullName, datFullName);
            newDir.Dat.TimeStamp = datFile.TimeStamp;
            newDir.Dat.Status = DatUpdateStatus.Correct;

            DatSetRemoveUnneededDirs(newDir);
            DatSetCheckParentSets(newDir);
            DatSetRenameAndRemoveDups(newDir);

            if (newDir.Dat.GetData(RvDat.DatData.MergeType).ToLower() == "full")
                DatSetMergeSets(newDir);

            DatSetCheckCollect(newDir);

            DatSetCreateSubDirs(newDir);

            if (newDir.Dat.GetData(RvDat.DatData.DirSetup).ToLower() == "nogame")
            {
                DatSetRemoveGameDir(newDir);
            }

            return newDir;
        }




        private static bool ReadXMLDat(ref RvDir tDat, string strFilename)
        {
            System.IO.Stream fs;
            int errorCode = IO.FileStream.OpenFileRead(strFilename, out fs);
            if (errorCode != 0)
            {
                _bgw.ReportProgress(0, new bgwShowError(strFilename, errorCode + ": " + new Win32Exception(errorCode).Message));
                return false;
            }

            XmlDocument doc = new XmlDocument { XmlResolver = null };
            try
            {
                doc.Load(fs);
            }
            catch (Exception e)
            {
                fs.Close();
                fs.Dispose();
                _bgw.ReportProgress(0, new bgwShowError(strFilename, string.Format("Error Occured Reading Dat:\r\n{0}\r\n", e.Message)));
                return false;
            }
            fs.Close();
            fs.Dispose();

            if (doc.DocumentElement == null)
                return false;

            XmlNode mame = doc.SelectSingleNode("mame");
            if (mame != null)
                return DatXmlReader.ReadMameDat(ref tDat, doc);

            if (doc.DocumentElement != null)
            {
                XmlNode head = doc.DocumentElement.SelectSingleNode("header");
                if (head != null)
                    return DatXmlReader.ReadDat(ref tDat, doc);
            }

            XmlNodeList headList = doc.SelectNodes("softwarelist");
            if (headList != null)
                return DatMessXmlReader.ReadDat(ref tDat, doc);

            return false;
        }

        private static void DatSetRenameAndRemoveDups(RvDir tDat)
        {
            for (int g = 0; g < tDat.ChildCount; g++)
            {
                RvDir tDir = (RvDir)tDat.Child(g);
                if (tDir.Game == null)
                {
                    DatSetRenameAndRemoveDups(tDir);
                }
                else
                {
                    for (int r = 0; r < tDir.ChildCount - 1; r++)
                    {
                        RvFile f0 = (RvFile)tDir.Child(r);
                        RvFile f1 = (RvFile)tDir.Child(r + 1);

                        if (f0.Name != f1.Name)
                            continue;

                        if (f0.Size != f1.Size || !ArrByte.bCompare(f0.CRC, f1.CRC))
                        {
                            tDir.ChildRemove(r + 1); // remove F1
                            f1.Name = f1.Name + "_" + ArrByte.ToString(f1.CRC); // rename F1;
                            int pos = tDir.ChildAdd(f1);
                            if (pos < r)
                                r = pos;
                            // if this rename moved the File back up the list, start checking again from that file.
                        }
                        else
                        {
                            tDir.ChildRemove(r + 1);
                        }
                        r--;
                    }
                }
            }
        }
        private static void DatSetRemoveUnneededDirs(RvDir tDat)
        {
            for (int g = 0; g < tDat.ChildCount; g++)
            {
                RvDir tGame = (RvDir)tDat.Child(g);
                if (tGame.Game == null)
                {
                    DatSetRemoveUnneededDirs(tGame);
                }
                else
                {
                    for (int r = 0; r < tGame.ChildCount - 1; r++)
                    {
                        // first find any directories, zero length with filename ending in a '/'
                        // there are RvFiles that are really directories (probably inside a zip file)
                        RvFile f0 = (RvFile)tGame.Child(r);
                        if (f0.Name.Length == 0)
                            continue;
                        if (f0.Name.Substring(f0.Name.Length - 1, 1) != "/")
                            continue;

                        // if the next file contains that found directory, then the directory file can be deleted
                        RvFile f1 = (RvFile)tGame.Child(r + 1);
                        if (f1.Name.Length <= f0.Name.Length)
                            continue;

                        if (f0.Name != f1.Name.Substring(0, f0.Name.Length))
                            continue;

                        tGame.ChildRemove(r);
                        r--;
                    }
                }
            }
        }


        private static void DatSetCheckParentSets(RvDir tDat)
        {
            // First we are going to try and fix any missing CRC information by checking for roms with the same names
            // in Parent and Child sets, and if the same named rom is found and one has a CRC and the other does not
            // then we will set the missing CRC by using the CRC in the other set.

            // we keep trying to find fixes until no more fixes are found.
            // this is need as the first time round a fix could be found in a parent set from one child set.
            // then the second time around that fixed parent set could fix another of its childs sets.

            for (int g = 0; g < tDat.ChildCount; g++)
            {
                RvDir mGame = (RvDir)tDat.Child(g);
                if (mGame.Game == null)
                    // this is a directory so recuse into it
                    DatSetCheckParentSets(mGame);
            }

            bool fix = true;
            while (fix)
            {
                fix = false;

                // loop around every ROM Set looking for fixes.
                for (int g = 0; g < tDat.ChildCount; g++)
                {

                    // get a list of that ROM Sets parents.
                    RvDir mGame = (RvDir)tDat.Child(g);

                    if (mGame.Game == null)
                        continue;

                    List<RvDir> lstParentGames = new List<RvDir>();
                    FindParentSet(mGame, tDat, ref lstParentGames);

                    // if this set have parents
                    if (lstParentGames.Count == 0)
                        continue;

                    // now loop every ROM in the current set.
                    for (int r = 0; r < mGame.ChildCount; r++)
                    {
                        // and loop every ROM of every parent set of this current set.
                        // and see if anything can be fixed.
                        bool found = false;

                        // loop the parent sets
                        foreach (RvDir romofGame in lstParentGames)
                        {
                            // loop the ROMs in the parent sets
                            for (int r1 = 0; r1 < romofGame.ChildCount; r1++)
                            {
                                // don't search fixes for files marked as nodump
                                if (((RvFile)mGame.Child(r)).Status == "nodump" || ((RvFile)romofGame.Child(r1)).Status == "nodump")
                                    continue;

                                // only find fixes if the Name and the Size of the ROMs are the same
                                if (mGame.Child(r).Name != romofGame.Child(r1).Name || ((RvFile)mGame.Child(r)).Size != ((RvFile)romofGame.Child(r1)).Size)
                                    continue;

                                // now check if one of the matching roms has missing or incorrect CRC information
                                bool b1 = ((RvFile)mGame.Child(r)).CRC == null;
                                bool b2 = ((RvFile)romofGame.Child(r1)).CRC == null;

                                // if one has correct information and the other does not, fix the missing one
                                if (b1 == b2)
                                    continue;

                                if (b1)
                                {
                                    ((RvFile)mGame.Child(r)).CRC = ((RvFile)romofGame.Child(r1)).CRC;
                                    ((RvFile)mGame.Child(r)).FileStatusSet(FileStatus.CRCFromDAT);
                                    ((RvFile)mGame.Child(r)).Status = "(CRCFound)";
                                }
                                else
                                {
                                    ((RvFile)romofGame.Child(r1)).CRC = ((RvFile)mGame.Child(r)).CRC;
                                    ((RvFile)romofGame.Child(r1)).FileStatusSet(FileStatus.CRCFromDAT);
                                    ((RvFile)romofGame.Child(r1)).Status = "(CRCFound)";
                                }

                                // flag that a fix was found so that we will go all the way around again.
                                fix = true;
                                found = true;
                                break;
                            }
                            if (found) break;
                        }
                    }
                }
            }
        }

        private static void DatSetMergeSets(RvDir tDat)
        {
            for (int g = tDat.ChildCount - 1; g >= 0; g--)
            {
                RvDir mGame = (RvDir)tDat.Child(g);

                if (mGame.Game == null)
                {
                    DatSetMergeSets(mGame);
                    continue;
                }

                List<RvDir> lstParentGames = new List<RvDir>();
                FindParentSet(mGame, tDat, ref lstParentGames);
                while (lstParentGames.Count > 0 && lstParentGames[lstParentGames.Count - 1].Game.GetData(RvGame.GameData.IsBios).ToLower() == "yes")
                    lstParentGames.RemoveAt(lstParentGames.Count - 1);

                if (lstParentGames.Count <= 0) continue;

                RvDir romofGame = lstParentGames[lstParentGames.Count - 1];

                bool founderror = false;
                for (int r = 0; r < mGame.ChildCount; r++)
                {
                    string name = mGame.Child(r).Name;
                    string mergename = ((RvFile)mGame.Child(r)).Merge;

                    for (int r1 = 0; r1 < romofGame.ChildCount; r1++)
                    {
                        if ((name == romofGame.Child(r1).Name || mergename == romofGame.Child(r1).Name) &&
                             (ArrByte.iCompare(((RvFile)mGame.Child(r)).CRC, ((RvFile)romofGame.Child(r1)).CRC) != 0 ||
                             ((RvFile)mGame.Child(r)).Size != ((RvFile)romofGame.Child(r1)).Size))
                            founderror = true;

                    }
                }
                if (founderror)
                {
                    mGame.Game.DeleteData(RvGame.GameData.RomOf);
                    continue;
                }

                for (int r = 0; r < mGame.ChildCount; r++)
                {
                    string name = mGame.Child(r).Name;
                    string mergename = ((RvFile)mGame.Child(r)).Merge;

                    bool found = false;
                    for (int r1 = 0; r1 < romofGame.ChildCount; r1++)
                    {
                        if ((name == romofGame.Child(r1).Name || mergename == romofGame.Child(r1).Name) &&
                            (ArrByte.iCompare(((RvFile)mGame.Child(r)).CRC, ((RvFile)romofGame.Child(r1)).CRC) == 0 &&
                             ((RvFile)mGame.Child(r)).Size == ((RvFile)romofGame.Child(r1)).Size))
                        {
                            found = true;
                            break;
                        }
                    }
                    if (!found)
                        romofGame.ChildAdd(mGame.Child(r));
                }
                tDat.ChildRemove(g);
            }
        }



        private static void DatSetCheckCollect(RvDir tDat)
        {
            // now look for merged roms.
            // check if a rom exists in a parent set where the Name,Size and CRC all match.

            for (int g = 0; g < tDat.ChildCount; g++)
            {
                RvDir mGame = (RvDir)tDat.Child(g);

                if (mGame.Game == null)
                    DatSetCheckCollect(mGame);
                else
                {
                    List<RvDir> lstParentGames = new List<RvDir>();
                    FindParentSet(mGame, tDat, ref lstParentGames);

                    if (lstParentGames.Count == 0)
                    {
                        for (int r = 0; r < mGame.ChildCount; r++)
                            RomCheckCollect((RvFile)mGame.Child(r), false);
                    }
                    else
                    {
                        for (int r = 0; r < mGame.ChildCount; r++)
                        {
                            bool found = false;
                            foreach (RvDir romofGame in lstParentGames)
                            {
                                for (int r1 = 0; r1 < romofGame.ChildCount; r1++)
                                {
                                    if (mGame.Child(r).Name != romofGame.Child(r1).Name) continue;

                                    ulong? Size0 = ((RvFile)mGame.Child(r)).Size;
                                    ulong? Size1 = ((RvFile)romofGame.Child(r1)).Size;
                                    if (Size0 != null && Size1 != null && Size0 != Size1) continue;

                                    byte[] CRC0 = ((RvFile)mGame.Child(r)).CRC;
                                    byte[] CRC1 = ((RvFile)romofGame.Child(r1)).CRC;
                                    if (CRC0 != null && CRC1 != null && !ArrByte.bCompare(CRC0, CRC1)) continue;

                                    byte[] SHA0 = ((RvFile)mGame.Child(r)).SHA1;
                                    byte[] SHA1 = ((RvFile)romofGame.Child(r1)).SHA1;
                                    if (SHA0 != null && SHA1 != null && !ArrByte.bCompare(SHA0, SHA1)) continue;

                                    byte[] MD50 = ((RvFile)mGame.Child(r)).MD5;
                                    byte[] MD51 = ((RvFile)romofGame.Child(r1)).MD5;
                                    if (MD50 != null && MD51 != null && !ArrByte.bCompare(MD50, MD51)) continue;

                                    byte[] chdSHA0 = ((RvFile)mGame.Child(r)).SHA1CHD;
                                    byte[] chdSHA1 = ((RvFile)romofGame.Child(r1)).SHA1CHD;
                                    if (chdSHA0 != null && chdSHA1 != null && !ArrByte.bCompare(chdSHA0, chdSHA1)) continue;

                                    byte[] chdMD50 = ((RvFile)mGame.Child(r)).MD5CHD;
                                    byte[] chdMD51 = ((RvFile)romofGame.Child(r1)).MD5CHD;
                                    if (chdMD50 != null && chdMD51 != null && !ArrByte.bCompare(chdMD50, chdMD51)) continue;

                                    // don't merge if only one of the ROM is nodump
                                    if ((((RvFile)romofGame.Child(r1)).Status == "nodump") != (((RvFile)mGame.Child(r)).Status == "nodump")) continue;

                                    found = true;
                                    break;
                                }
                                if (found) break;
                            }
                            RomCheckCollect((RvFile)mGame.Child(r), found);
                        }
                    }
                }
            }
        }
        private static void FindParentSet(RvDir searchGame, RvDir parentDir, ref List<RvDir> lstParentGames)
        {
            if (searchGame.Game == null)
                return;

            string parentName = searchGame.Game.GetData(RvGame.GameData.RomOf);
            if (String.IsNullOrEmpty(parentName) || parentName == searchGame.Name)
                parentName = searchGame.Game.GetData(RvGame.GameData.CloneOf);
            if (String.IsNullOrEmpty(parentName) || parentName == searchGame.Name)
                return;

            int intIndex;
            int intResult = parentDir.ChildNameSearch(new RvDir(searchGame.FileType) { Name = parentName }, out intIndex);
            if (intResult == 0)
            {
                RvDir parentGame = (RvDir)parentDir.Child(intIndex);
                lstParentGames.Add(parentGame);
                FindParentSet(parentGame, parentDir, ref lstParentGames);
            }
        }


        /*
         * In the mame Dat:
         * status="nodump" has a size but no CRC
         * status="baddump" has a size and crc
         */


        private static void RomCheckCollect(RvFile tRom, bool merge)
        {
            if (merge)
            {
                if (string.IsNullOrEmpty(tRom.Merge))
                    tRom.Merge = "(Auto Merged)";

                tRom.DatStatus = DatStatus.InDatMerged;
                return;
            }

            if (!string.IsNullOrEmpty(tRom.Merge))
                tRom.Merge = "(No-Merge) " + tRom.Merge;

            if (tRom.Status == "nodump")
            {
                tRom.CRC = null;
                tRom.DatStatus = DatStatus.InDatBad;
                return;
            }

            if (ArrByte.bCompare(tRom.CRC, new byte[] { 0, 0, 0, 0 }) && tRom.Size == 0)
            {
                tRom.DatStatus = DatStatus.InDatCollect;
                return;
            }

            /*
            if (ArrByte.bCompare(tRom.CRC, new byte[] { 0, 0, 0, 0 }) || (tRom.CRC.Length != 8))
            {
                tRom.CRC = null;
                tRom.DatStatus = DatStatus.InDatBad;
                return;
            }
            */

            tRom.DatStatus = DatStatus.InDatCollect;
        }

        private static void DatSetCreateSubDirs(RvDir tDat)
        {
            for (int g = 0; g < tDat.ChildCount; g++)
            {
                if (tDat.Child(g).FileType == FileType.Zip)
                    continue;

                RvDir datGame = (RvDir)tDat.Child(g);

                // first do a quick check to see if anything needs done.
                bool fixNeeded = false;
                for (int r = 0; r < datGame.ChildCount; r++)
                {
                    fixNeeded = datGame.Child(r).Name.Contains("/");

                    if (fixNeeded)
                        break;
                }
                // if nothing needs done skip to next game
                if (!fixNeeded)
                    continue;


                RvDir fixedGame = new RvDir(FileType.Dir);
                while (datGame.ChildCount > 0)
                {
                    RvBase nextChild = datGame.Child(0);
                    datGame.ChildRemove(0);
                    if (nextChild.GetType() == typeof(RvFile))
                    {
                        RvFile tFile = (RvFile)nextChild;
                        if (tFile.Name.Contains("/"))
                        {
                            RvDir tBase = fixedGame;
                            Debug.WriteLine("tFile " + tFile.TreeFullName);
                            while (tFile.Name.Contains("/"))
                            {
                                int dirIndex = tFile.Name.IndexOf("/", StringComparison.Ordinal);
                                string dirName = tFile.Name.Substring(0, dirIndex);
                                RvDir tDir = new RvDir(FileType.Dir)
                                               {
                                                   Name = dirName,
                                                   DatStatus = DatStatus.InDatCollect,
                                                   Dat = datGame.Dat
                                               };
                                int index;
                                if (tBase.ChildNameSearch(tDir, out index) != 0)
                                    tBase.ChildAdd(tDir, index);
                                tBase = (RvDir)tBase.Child(index);
                                tFile.Name = tFile.Name.Substring(tFile.Name.IndexOf("/", StringComparison.Ordinal) + 1);
                            }
                            tBase.ChildAdd(tFile);
                        }
                        else
                            fixedGame.ChildAdd(nextChild);
                    }
                    else
                        fixedGame.ChildAdd(nextChild);


                }

                for (int r = 0; r < fixedGame.ChildCount; r++)
                    datGame.ChildAdd(fixedGame.Child(r), r);
            }
        }


        private static void DatSetRemoveGameDir(RvDir newDir)
        {
            if (newDir.ChildCount != 1)
                return;

            RvDir child = newDir.Child(0) as RvDir;
            if (child.FileType != FileType.Dir)
                return;

            if (child.Game == null)
                return;

            newDir.ChildRemove(0);
            newDir.Game = child.Game;
            for (int i = 0; i < child.ChildCount; i++)
                newDir.ChildAdd(child.Child(i), i);
        }
    }
}
