using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Win32;
using Indy.IL2CPU;
using Indy.IL2CPU.IL.X86;

namespace Cosmos.Build.Windows
{
    public class Builder
    {
        public readonly string BuildPath;
        public readonly string ToolsPath;

        public Builder()
        {
            BuildPath = GetBuildPath();
            ToolsPath = BuildPath + @"Tools\";
            // MtW: leave this here, otherwise VS wont copy required dependencies!
            typeof(X86OpCodeMap).Equals(null);
        }

        /// <summary>
        /// Retrieves the path to the Build directory.
        /// Looks first in the Registry. If no match found there it will use the Current Directory to calculate the
        /// path to the Build directory.
        /// </summary>
        /// <returns>Full path to the Build directory.</returns>
        protected static string GetBuildPath()
        {
            try
            {
                string xResult = "";
                RegistryKey xKey = Registry.CurrentUser.OpenSubKey(@"Software\Cosmos");

                if (xKey != null)
                { // Use Registry
                    xResult = (string)xKey.GetValue("Build Path");
                }
                if (string.IsNullOrEmpty(xResult))
                {
                    xResult = Directory.GetCurrentDirectory().ToLowerInvariant();
                    int xPos = xResult.LastIndexOf("source");
                    if (xPos == -1)
                    {
                        throw new Exception("Unable to find directory named 'source' when using CurrentDirectory.");
                    }
                    xResult = xResult.Substring(0, xPos) + @"Build\";
                }

                if (string.IsNullOrEmpty(xResult))
                {
                    throw new Exception("Cannot find Cosmos build path either in the Registry or using current directory search.");
                }
                if (!xResult.EndsWith(@"\"))
                {
                    xResult = xResult + @"\";
                }

                return xResult;
            }
            catch (Exception E)
            {
                throw new Exception("Error while getting Cosmos Build Path!", E);
            }
        }

        protected void RemoveFile(string aPathname)
        {
            if (File.Exists(aPathname))
            {
                RemoveReadOnlyAttribute(aPathname);
                File.Delete(aPathname);
            }
        }

        protected void CopyFile(string aFrom, string aTo)
        {
            string xDir = Path.GetDirectoryName(aTo);
            if (!Directory.Exists(xDir))
            {
                Directory.CreateDirectory(xDir);
            }
            File.Copy(aFrom, aTo);
        }

        protected void RemoveReadOnlyAttribute(string aPathname)
        {
            var xAttribs = File.GetAttributes(aPathname);
            if ((xAttribs & FileAttributes.ReadOnly) > 0)
            {
                // This works because we only do this if Read only is already set
                File.SetAttributes(aPathname, xAttribs ^ FileAttributes.ReadOnly);
            }
        }

        public void MakeISO()
        {
            string xPath = BuildPath + @"ISO\";
            RemoveFile(BuildPath + "cosmos.iso");
            RemoveFile(xPath + "output.bin");
            CopyFile(BuildPath + "output.bin", xPath + "output.bin");
            // From TFS its read only, mkisofs doesnt like that
            RemoveReadOnlyAttribute(xPath + "isolinux.bin");
            Global.Call(ToolsPath + @"mkisofs.exe", @"-R -b isolinux.bin -no-emul-boot -boot-load-size 4 -boot-info-table -o ..\Cosmos.iso .", xPath);
        }

        private void DoDebugLog(LogSeverityEnum aSeverity, string aMessage)
        {
            if (DebugLog != null)
            {
                DebugLog(aSeverity, aMessage);
            }
        }

        public event DebugLogHandler DebugLog;
        public event Action<int, int> ProgressChanged;

        public void Compile(DebugModeEnum aDebugMode, byte aDebugComport)
        {
            string xAsmPath = ToolsPath + @"asm\";
            if (!Directory.Exists(xAsmPath))
            {
                Directory.CreateDirectory(xAsmPath);
            }
            Assembly xTarget = System.Reflection.Assembly.GetEntryAssembly();
            Stopwatch xSW = new Stopwatch();
            xSW.Start();
            var xEngine = new Engine();
            xEngine.ProgressChanged += delegate()
            {
                if (ProgressChanged != null)
                {
                    ProgressChanged(xEngine.ProgressMax, xEngine.ProgressCurrent);
                }
            };
            xEngine.DebugLog += DoDebugLog;
            xEngine.Execute(xTarget.Location, TargetPlatformEnum.X86, g => Path.Combine(xAsmPath, g + ".asm"), false,
                new string[]
                    {
                        Path.Combine(Path.Combine(ToolsPath, "Cosmos.Kernel.Plugs"), "Cosmos.Kernel.Plugs.dll"), 
                        Path.Combine(Path.Combine(ToolsPath, "Cosmos.Sys.Plugs"), "Cosmos.Sys.Plugs.dll")
                    }, aDebugMode, aDebugComport, xAsmPath);
            xSW.Stop();
            Console.WriteLine("IL2CPU Run took " + xSW.Elapsed.ToString());

            RemoveFile(BuildPath + "output.obj");
            Global.Call(ToolsPath + @"nasm\nasm.exe", String.Format("-g -f elf -F stabs -o \"{0}\" \"{1}\"", BuildPath + "output.obj", xAsmPath + "main.asm"), BuildPath);

            RemoveFile(BuildPath + "output.bin");
            Global.Call(ToolsPath + @"cygwin\ld.exe", String.Format("-Ttext 0x500000 -Tdata 0x200000 -e Kernel_Start -o \"{0}\" \"{1}\"", "output.bin", "output.obj"), BuildPath);
            RemoveFile(BuildPath + "output.obj");
        }

        public void BuildKernel()
        {
        }

        public void MakeVPC()
        {
            MakeISO();
            string xPath = BuildPath + @"VPC\";
            RemoveReadOnlyAttribute(xPath + "Cosmos.vmc");
            RemoveReadOnlyAttribute(xPath + "hda.vhd");
            Process.Start(xPath + "Cosmos.vmc");
        }

        public void MakeVMWare(bool useVMWareServer)
        {
            MakeISO();
            string xPath = BuildPath + @"VMWare\";

            if (useVMWareServer)
                xPath += @"Server\";
            else
                xPath += @"Workstation\";

            RemoveReadOnlyAttribute(xPath + "Cosmos.nvram");
            RemoveReadOnlyAttribute(xPath + "Cosmos.vmsd");
            RemoveReadOnlyAttribute(xPath + "Cosmos.vmx");
            RemoveReadOnlyAttribute(xPath + "Cosmos.vmxf");
            RemoveReadOnlyAttribute(xPath + "hda.vmdk");

            Process.Start(xPath + @"Cosmos.vmx");
        }

        public void MakeQEMU(bool aUseHDImage, bool aGDB, bool aWaitSerialTCP, bool aDebugger, bool aUseNetworkTap, object aNetworkCard, object aAudioCard)
        {
            MakeISO();
            RemoveFile(BuildPath + "serial-debug.txt");
            // QEMU Docs - http://fabrice.bellard.free.fr/qemu/qemu-doc.html
            Global.Call(ToolsPath + @"qemu\qemu.exe"
                // HD image
                , (aUseHDImage ? "-hda \"" + BuildPath + "hda.img\"" : "")
                // Path for BIOS, VGA BIOS, and keymaps
                + " -L ."
                // CD ROM image
                + " -cdrom \"" + BuildPath + "Cosmos.iso\""
                // Boot CD ROM
                + " -boot d"
                // Audio hardware
                + " -soundhw " + Enum.Parse(typeof(QemuAudioCard), aAudioCard.ToString())
                // Setup serial port
                // Might allow serial file later for post debugging of CPU
                // etc since serial to TCP on a byte level is likely highly innefficient
                // with the packet overhead
                //
                // COM1
                + (aDebugger ?
                 (aWaitSerialTCP ? " -serial tcp::4444,server" + (aWaitSerialTCP ? "" : ",nowait") : "")
                 : " -serial \"file:" + BuildPath + "COM1-output.dbg\" ")
                // COM2
                + " -serial \"file:" + BuildPath + "COM2-output.dbg\" "
                // Enable acceleration if we are not using GDB
                + (aGDB ? " -S -s" : " -kernel-kqemu")
                // Ethernet card
                + string.Format(" -net nic,model={0},macaddr=52:54:00:12:34:57", Enum.Parse(typeof(QemuNetworkCard), aNetworkCard.ToString()))
                //+ " -redir tcp:5555::23" //use f.instance 'telnet localhost 5555' or 'http://localhost:5555/' to access machine
                + (aUseNetworkTap ? " -net tap,ifname=CosmosTAP" : "") //requires TAP installed on development computer
                + " -net user"
                , ToolsPath + @"qemu\", false, true);

            if (aGDB)
            {
                //TODO: If the host is really busy, sometimes GDB can run before QEMU finishes loading.
                //in this case, GDB says "program not running". Not sure how to fix this properly.
                Global.Call(ToolsPath + "gdb.exe"
                    , BuildPath + @"output.bin" + " --eval-command=\"target remote:1234\" --eval-command=\"b _CODE_REQUESTED_BREAK_\" --eval-command=\"c\""
                    , ToolsPath + @"qemu\", false, false);
            }
        }
        ///<summary>
        ///The virtual audio cards supported by Qemu
        ///</summary>
        public enum QemuAudioCard
        {
            sb16,
            es1370,
            adlib
        }


        /// <summary>
        /// The virtual network cards supported by Qemu
        /// </summary>
        public enum QemuNetworkCard
        {
            ne2k_pci,
            rtl8139,
            pcnet
        }

        public void MakeUSB(char aDrive)
        {
            string xPath = BuildPath + @"USB\";
            RemoveFile(xPath + @"output.bin");
            File.Move(BuildPath + @"output.bin", xPath + @"output.bin");
            // Copy to USB device
            RemoveFile(aDrive + @":\output.bin");
            File.Copy(xPath + @"output.bin", aDrive + @":\output.bin");
            RemoveFile(aDrive + @":\mboot.c32");
            File.Copy(xPath + @"mboot.c32", aDrive + @":\mboot.c32");
            RemoveFile(aDrive + @":\syslinux.cfg");
            File.Copy(xPath + @"syslinux.cfg", aDrive + @":\syslinux.cfg");
            // Set MBR
            Global.Call(ToolsPath + "syslinux.exe", "-fma " + aDrive + ":", ToolsPath, true, true);
        }

        public void MakePXE()
        {
            string xPath = BuildPath + @"PXE\";
            RemoveFile(xPath + @"Boot\output.bin");
            File.Move(BuildPath + "output.bin", xPath + @"Boot\output.bin");
            // *Must* set working dir so tftpd32 will set itself to proper dir
            Global.Call(xPath + "tftpd32.exe", "", xPath, false, true);
        }
    }
}
